changeset 690:e4cf08a87f8c

integration attach-custom-data->mainline
author Sebastien Jodogne <s.jodogne@gmail.com>
date Fri, 13 Jun 2025 15:16:52 +0200
parents 1975d5f63b34 (current diff) 431aab517a46 (diff)
children 540918f7417b
files
diffstat 56 files changed, 1968 insertions(+), 491 deletions(-) [+]
line wrap: on
line diff
--- a/Framework/Common/BinaryStringValue.cpp	Mon May 19 11:18:43 2025 +0200
+++ b/Framework/Common/BinaryStringValue.cpp	Fri Jun 13 15:16:52 2025 +0200
@@ -31,6 +31,20 @@
 
 namespace OrthancDatabases
 {
+  BinaryStringValue::BinaryStringValue(const void* data,
+                                       size_t size)
+  {
+    if (data == NULL && size > 0)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+    else
+    {
+      content_.assign(reinterpret_cast<const char*>(data), size);
+    }
+  }
+
+
   IValue* BinaryStringValue::Convert(ValueType target) const
   {
     switch (target)
--- a/Framework/Common/BinaryStringValue.h	Mon May 19 11:18:43 2025 +0200
+++ b/Framework/Common/BinaryStringValue.h	Fri Jun 13 15:16:52 2025 +0200
@@ -35,11 +35,18 @@
     std::string  content_;
 
   public:
+    BinaryStringValue()
+    {
+    }
+
     explicit BinaryStringValue(const std::string& content) :
       content_(content)
     {
     }
 
+    BinaryStringValue(const void* data,
+                      size_t size);
+
     const std::string& GetContent() const
     {
       return content_;
@@ -55,6 +62,11 @@
       return content_.size();
     }
 
+    void Swap(std::string& other)
+    {
+      content_.swap(other);
+    }
+
     virtual ValueType GetType() const ORTHANC_OVERRIDE
     {
       return ValueType_BinaryString;
--- a/Framework/Common/DatabaseManager.cpp	Mon May 19 11:18:43 2025 +0200
+++ b/Framework/Common/DatabaseManager.cpp	Fri Jun 13 15:16:52 2025 +0200
@@ -99,7 +99,7 @@
                                                          const Query& query)
   {
     LOG(TRACE) << "Caching statement from " << statementId.GetFile() << ":" << statementId.GetLine() << "" << statementId.GetDynamicStatement();
-      
+    
     std::unique_ptr<IPrecompiledStatement> statement(GetDatabase().Compile(query));
       
     IPrecompiledStatement* tmp = statement.get();
@@ -394,6 +394,17 @@
   }
 
 
+  void DatabaseManager::StatementBase::SetParametersTypes(const Dictionary& parameters)
+  {
+    Dictionary::Types parametersTypes;
+    parameters.GetParametersType(parametersTypes);
+
+    for (Dictionary::Types::const_iterator it = parametersTypes.begin(); it != parametersTypes.end(); ++it)
+    {
+      SetParameterType(it->first, it->second);
+    }
+  }
+
   void DatabaseManager::StatementBase::SetParameterType(const std::string& parameter,
                                                         ValueType type)
   {
@@ -550,6 +561,18 @@
     }
   }
   
+  std::string DatabaseManager::StatementBase::ReadStringOrNull(size_t field) const
+  {
+    if (IsNull(field))
+    {
+      return std::string();
+    }
+    else
+    {
+      return ReadString(field);
+    }
+  }
+  
   DatabaseManager::CachedStatement::CachedStatement(const StatementId& statementId,
                                                     DatabaseManager& manager,
                                                     const std::string& sql) :
@@ -589,6 +612,9 @@
 
   void DatabaseManager::CachedStatement::ExecuteInternal(const Dictionary& parameters, bool withResults)
   {
+    // the query parameters_ type can not be trusted (they are all Utf8String by default), we need a parameters dico with the actual types
+    SetParametersTypes(parameters);
+
     try
     {
       std::unique_ptr<Query> query(ReleaseQuery());
@@ -597,7 +623,7 @@
       {
         // Register the newly-created statement
         assert(statement_ == NULL);
-        statement_ = &GetManager().CacheStatement(statementId_, *query);
+        statement_ = &GetManager().CacheStatement(statementId_, *query);  
       }
         
       assert(statement_ != NULL);
@@ -679,6 +705,9 @@
 
   void DatabaseManager::StandaloneStatement::ExecuteInternal(const Dictionary& parameters, bool withResults)
   {
+    // the query parameters_ type can not be trusted (they are all Utf8String by default), we need a parameters dico with the actual types
+    SetParametersTypes(parameters);
+
     try
     {
       std::unique_ptr<Query> query(ReleaseQuery());
--- a/Framework/Common/DatabaseManager.h	Mon May 19 11:18:43 2025 +0200
+++ b/Framework/Common/DatabaseManager.h	Fri Jun 13 15:16:52 2025 +0200
@@ -155,6 +155,8 @@
         return query_.release();
       }
 
+      void SetParametersTypes(const Dictionary& parameters);
+      
     public:
       explicit StatementBase(DatabaseManager& manager);
 
@@ -188,6 +190,8 @@
 
       std::string ReadString(size_t field) const;
 
+      std::string ReadStringOrNull(size_t field) const;
+
       bool IsNull(size_t field) const;
 
       void PrintResult(std::ostream& stream)
--- a/Framework/Common/Dictionary.cpp	Mon May 19 11:18:43 2025 +0200
+++ b/Framework/Common/Dictionary.cpp	Fri Jun 13 15:16:52 2025 +0200
@@ -106,6 +106,14 @@
   }
 
   
+  void Dictionary::SetBinaryValue(const std::string& key,
+                                  const void* data,
+                                  size_t size)
+  {
+    SetValue(key, new BinaryStringValue(data, size));
+  }
+
+
   void Dictionary::SetFileValue(const std::string& key,
                                 const std::string& file)
   {
@@ -134,9 +142,9 @@
     SetValue(key, new Integer32Value(value));
   }
 
-  void Dictionary::SetNullValue(const std::string& key)
+  void Dictionary::SetUtf8NullValue(const std::string& key)
   {
-    SetValue(key, new NullValue);
+    SetValue(key, new Utf8StringValue());
   }
 
   
--- a/Framework/Common/Dictionary.h	Mon May 19 11:18:43 2025 +0200
+++ b/Framework/Common/Dictionary.h	Fri Jun 13 15:16:52 2025 +0200
@@ -33,6 +33,8 @@
 {
   class Dictionary : public boost::noncopyable
   {
+  public:
+    typedef std::map<std::string, ValueType> Types;
   private:
     typedef std::map<std::string, IValue*>   Values;
 
@@ -56,9 +58,15 @@
     void SetUtf8Value(const std::string& key,
                       const std::string& utf8);
 
+    void SetUtf8NullValue(const std::string& key);
+
     void SetBinaryValue(const std::string& key,
                         const std::string& binary);
 
+    void SetBinaryValue(const std::string& key,
+                        const void* data,
+                        size_t size);
+
     void SetFileValue(const std::string& key,
                       const std::string& file);
 
@@ -72,10 +80,10 @@
     void SetInteger32Value(const std::string& key,
                            int32_t value);
 
-    void SetNullValue(const std::string& key);
-
     const IValue& GetValue(const std::string& key) const;
 
     void GetParametersType(Query::Parameters& target) const;
+
+    void GetParametersTypes(Types& target) const;
   };
 }
--- a/Framework/Common/IValue.h	Mon May 19 11:18:43 2025 +0200
+++ b/Framework/Common/IValue.h	Fri Jun 13 15:16:52 2025 +0200
@@ -40,5 +40,10 @@
     virtual ValueType GetType() const = 0;
 
     virtual IValue* Convert(ValueType target) const = 0;
+
+    virtual bool IsNull() const  // TODO: right now, only the Utf8StringValue implements nullable values
+    {
+      return false;
+    }
   };
 }
--- a/Framework/Common/Utf8StringValue.cpp	Mon May 19 11:18:43 2025 +0200
+++ b/Framework/Common/Utf8StringValue.cpp	Fri Jun 13 15:16:52 2025 +0200
@@ -57,7 +57,14 @@
         break;
 
       case ValueType_Utf8String:
-        return new Utf8StringValue(utf8_);
+        if (IsNull())
+        {
+          return new Utf8StringValue();
+        }
+        else
+        {
+          return new Utf8StringValue(utf8_);
+        }
 
       default:
         throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
--- a/Framework/Common/Utf8StringValue.h	Mon May 19 11:18:43 2025 +0200
+++ b/Framework/Common/Utf8StringValue.h	Fri Jun 13 15:16:52 2025 +0200
@@ -34,13 +34,25 @@
   {
   private:
     std::string  utf8_;
+    bool isNull_;
 
   public:
     explicit Utf8StringValue(const std::string& utf8) :
-      utf8_(utf8)
+      utf8_(utf8),
+      isNull_(false)
     {
     }
 
+    explicit Utf8StringValue() :
+      isNull_(true)
+    {
+    }
+
+    virtual bool IsNull() const ORTHANC_OVERRIDE
+    {
+      return isNull_;
+    }
+    
     const std::string& GetContent() const
     {
       return utf8_;
--- a/Framework/MySQL/MySQLStatement.cpp	Mon May 19 11:18:43 2025 +0200
+++ b/Framework/MySQL/MySQLStatement.cpp	Fri Jun 13 15:16:52 2025 +0200
@@ -468,62 +468,64 @@
         throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentItem);
       }
 
-      ValueType type = formatter_.GetParameterType(i);
-
       const IValue& value = parameters.GetValue(name);
-      if (value.GetType() != type)
-      {
-        LOG(ERROR) << "Bad type of argument provided to a SQL query: " << name;
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadParameterType);
-      }
 
-      // https://dev.mysql.com/doc/refman/8.0/en/c-api-prepared-statement-type-codes.html
-      switch (type)
+      if (value.GetType() == ValueType_Null)
       {
-        case ValueType_Integer64:
+        inputs[i].buffer = NULL;
+        inputs[i].buffer_type = MYSQL_TYPE_NULL;
+      }
+      else
+      {
+        ValueType type = formatter_.GetParameterType(i);
+  
+        if (value.GetType() != type)
         {
-          int64Parameters.push_back(dynamic_cast<const Integer64Value&>(value).GetValue());
-          inputs[i].buffer = &int64Parameters.back();
-          inputs[i].buffer_type = MYSQL_TYPE_LONGLONG;
-          break;
+          LOG(ERROR) << "Bad type of argument provided to a SQL query: " << name;
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadParameterType);
         }
 
-        case ValueType_Utf8String:
+        // https://dev.mysql.com/doc/refman/8.0/en/c-api-prepared-statement-type-codes.html
+        switch (type)
         {
-          const std::string& utf8 = dynamic_cast<const Utf8StringValue&>(value).GetContent();
-          inputs[i].buffer = const_cast<char*>(utf8.c_str());
-          inputs[i].buffer_length = utf8.size();
-          inputs[i].buffer_type = MYSQL_TYPE_STRING;
-          break;
-        }
+          case ValueType_Integer64:
+          {
+            int64Parameters.push_back(dynamic_cast<const Integer64Value&>(value).GetValue());
+            inputs[i].buffer = &int64Parameters.back();
+            inputs[i].buffer_type = MYSQL_TYPE_LONGLONG;
+            break;
+          }
 
-        case ValueType_BinaryString:
-        {
-          const std::string& content = dynamic_cast<const BinaryStringValue&>(value).GetContent();
-          inputs[i].buffer = const_cast<char*>(content.c_str());
-          inputs[i].buffer_length = content.size();
-          inputs[i].buffer_type = MYSQL_TYPE_BLOB;
-          break;
-        }
+          case ValueType_Utf8String:
+          {
+            const std::string& utf8 = dynamic_cast<const Utf8StringValue&>(value).GetContent();
+            inputs[i].buffer = const_cast<char*>(utf8.c_str());
+            inputs[i].buffer_length = utf8.size();
+            inputs[i].buffer_type = MYSQL_TYPE_STRING;
+            break;
+          }
 
-        case ValueType_InputFile:
-        {
-          const std::string& content = dynamic_cast<const InputFileValue&>(value).GetContent();
-          inputs[i].buffer = const_cast<char*>(content.c_str());
-          inputs[i].buffer_length = content.size();
-          inputs[i].buffer_type = MYSQL_TYPE_BLOB;
-          break;
-        }
+          case ValueType_BinaryString:
+          {
+            const std::string& content = dynamic_cast<const BinaryStringValue&>(value).GetContent();
+            inputs[i].buffer = const_cast<char*>(content.c_str());
+            inputs[i].buffer_length = content.size();
+            inputs[i].buffer_type = MYSQL_TYPE_BLOB;
+            break;
+          }
 
-        case ValueType_Null:
-        {
-          inputs[i].buffer = NULL;
-          inputs[i].buffer_type = MYSQL_TYPE_NULL;
-          break;
+          case ValueType_InputFile:
+          {
+            const std::string& content = dynamic_cast<const InputFileValue&>(value).GetContent();
+            inputs[i].buffer = const_cast<char*>(content.c_str());
+            inputs[i].buffer_length = content.size();
+            inputs[i].buffer_type = MYSQL_TYPE_BLOB;
+            break;
+          }
+
+          default:
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
         }
-
-        default:
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
       }
     }
 
--- a/Framework/Plugins/DatabaseBackendAdapterV2.cpp	Mon May 19 11:18:43 2025 +0200
+++ b/Framework/Plugins/DatabaseBackendAdapterV2.cpp	Fri Jun 13 15:16:52 2025 +0200
@@ -187,7 +187,8 @@
                                          const std::string& uncompressedHash,
                                          int32_t            compressionType,
                                          uint64_t           compressedSize,
-                                         const std::string& compressedHash) ORTHANC_OVERRIDE
+                                         const std::string& compressedHash,
+                                         const std::string& /*customData*/) ORTHANC_OVERRIDE
     {
       OrthancPluginAttachment attachment;
       attachment.uuid = uuid.c_str();
@@ -219,7 +220,8 @@
                                   const std::string& uncompressedHash,
                                   int32_t            compressionType,
                                   uint64_t           compressedSize,
-                                  const std::string& compressedHash) ORTHANC_OVERRIDE
+                                  const std::string& compressedHash,
+                                  const std::string& /*customData*/) ORTHANC_OVERRIDE
     {
       if (allowedAnswers_ != AllowedAnswers_All &&
           allowedAnswers_ != AllowedAnswers_Attachment)
--- a/Framework/Plugins/DatabaseBackendAdapterV3.cpp	Mon May 19 11:18:43 2025 +0200
+++ b/Framework/Plugins/DatabaseBackendAdapterV3.cpp	Fri Jun 13 15:16:52 2025 +0200
@@ -421,7 +421,8 @@
                                          const std::string& uncompressedHash,
                                          int32_t            compressionType,
                                          uint64_t           compressedSize,
-                                         const std::string& compressedHash) ORTHANC_OVERRIDE
+                                         const std::string& compressedHash,
+                                         const std::string& /*customData*/) ORTHANC_OVERRIDE
     {
       OrthancPluginDatabaseEvent event;
       event.type = OrthancPluginDatabaseEventType_DeletedAttachment;
@@ -467,7 +468,8 @@
                                   const std::string& uncompressedHash,
                                   int32_t            compressionType,
                                   uint64_t           compressedSize,
-                                  const std::string& compressedHash) ORTHANC_OVERRIDE
+                                  const std::string& compressedHash,
+                                  const std::string& /*customData*/) ORTHANC_OVERRIDE
     {
       SetupAnswerType(_OrthancPluginDatabaseAnswerType_Attachment);
 
--- a/Framework/Plugins/DatabaseBackendAdapterV4.cpp	Mon May 19 11:18:43 2025 +0200
+++ b/Framework/Plugins/DatabaseBackendAdapterV4.cpp	Fri Jun 13 15:16:52 2025 +0200
@@ -195,7 +195,8 @@
                                          const std::string& uncompressedHash,
                                          int32_t            compressionType,
                                          uint64_t           compressedSize,
-                                         const std::string& compressedHash) ORTHANC_OVERRIDE
+                                         const std::string& compressedHash,
+                                         const std::string& customData) ORTHANC_OVERRIDE
     {
       Orthanc::DatabasePluginMessages::FileInfo* attachment;
 
@@ -224,6 +225,10 @@
       attachment->set_compression_type(compressionType);
       attachment->set_compressed_size(compressedSize);
       attachment->set_compressed_hash(compressedHash);
+
+#if ORTHANC_PLUGINS_HAS_ATTACHMENTS_CUSTOM_DATA == 1
+      attachment->set_custom_data(customData);
+#endif
     }
 
     virtual void SignalDeletedResource(const std::string& publicId,
@@ -269,7 +274,8 @@
                                   const std::string& uncompressedHash,
                                   int32_t            compressionType,
                                   uint64_t           compressedSize,
-                                  const std::string& compressedHash) ORTHANC_OVERRIDE
+                                  const std::string& compressedHash,
+                                  const std::string& customData) ORTHANC_OVERRIDE
     {
       if (lookupAttachment_ != NULL)
       {
@@ -286,6 +292,9 @@
         lookupAttachment_->mutable_attachment()->set_compression_type(compressionType);
         lookupAttachment_->mutable_attachment()->set_compressed_size(compressedSize);
         lookupAttachment_->mutable_attachment()->set_compressed_hash(compressedHash);
+#if ORTHANC_PLUGINS_HAS_ATTACHMENTS_CUSTOM_DATA == 1
+        lookupAttachment_->mutable_attachment()->set_custom_data(customData);
+#endif
       }
       else
       {
@@ -445,6 +454,18 @@
         response.mutable_get_system_information()->set_has_extended_changes(accessor.GetBackend().HasExtendedChanges());
 #endif
 
+#if ORTHANC_PLUGINS_HAS_ATTACHMENTS_CUSTOM_DATA == 1
+        response.mutable_get_system_information()->set_has_attachment_custom_data(accessor.GetBackend().HasAttachmentCustomDataSupport());
+#endif
+
+#if ORTHANC_PLUGINS_HAS_QUEUES == 1
+        response.mutable_get_system_information()->set_supports_queues(accessor.GetBackend().HasQueues());
+#endif
+
+#if ORTHANC_PLUGINS_HAS_KEY_VALUE_STORES == 1
+        response.mutable_get_system_information()->set_supports_key_value_stores(accessor.GetBackend().HasKeyValueStores());
+#endif
+
         break;
       }
 
@@ -472,16 +493,12 @@
       }
 
       case Orthanc::DatabasePluginMessages::OPERATION_CLOSE:
-      {
         pool.CloseConnections();
         break;
-      }
 
       case Orthanc::DatabasePluginMessages::OPERATION_FLUSH_TO_DISK:
-      {
         // Raise an exception since "set_supports_flush_to_disk(false)"
         throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
-      }
 
       case Orthanc::DatabasePluginMessages::OPERATION_START_TRANSACTION:
       {
@@ -669,16 +686,12 @@
     switch (request.operation())
     {
       case Orthanc::DatabasePluginMessages::OPERATION_ROLLBACK:
-      {
         manager.RollbackTransaction();
         break;
-      }
       
       case Orthanc::DatabasePluginMessages::OPERATION_COMMIT:
-      {
         manager.CommitTransaction();
         break;
-      }
       
       case Orthanc::DatabasePluginMessages::OPERATION_ADD_ATTACHMENT:
       {
@@ -690,22 +703,24 @@
         attachment.compressionType = request.add_attachment().attachment().compression_type();
         attachment.compressedSize = request.add_attachment().attachment().compressed_size();
         attachment.compressedHash = request.add_attachment().attachment().compressed_hash().c_str();
-        
+
+#if ORTHANC_PLUGINS_HAS_ATTACHMENTS_CUSTOM_DATA == 1
+        backend.AddAttachment(manager, request.add_attachment().id(), attachment, request.add_attachment().revision(),
+                              request.add_attachment().attachment().custom_data());
+#else
         backend.AddAttachment(manager, request.add_attachment().id(), attachment, request.add_attachment().revision());
+#endif
+
         break;
       }
-      
+
       case Orthanc::DatabasePluginMessages::OPERATION_CLEAR_CHANGES:
-      {
         backend.ClearChanges(manager);
         break;
-      }
       
       case Orthanc::DatabasePluginMessages::OPERATION_CLEAR_EXPORTED_RESOURCES:
-      {
         backend.ClearExportedResources(manager);
         break;
-      }
       
       case Orthanc::DatabasePluginMessages::OPERATION_DELETE_ATTACHMENT:
       {
@@ -715,10 +730,8 @@
       }
       
       case Orthanc::DatabasePluginMessages::OPERATION_DELETE_METADATA:
-      {
         backend.DeleteMetadata(manager, request.delete_metadata().id(), request.delete_metadata().type());
         break;
-      }
       
       case Orthanc::DatabasePluginMessages::OPERATION_DELETE_RESOURCE:
       {
@@ -788,6 +801,7 @@
         response.mutable_get_changes()->set_done(done);
         break;
       }
+
 #if ORTHANC_PLUGINS_HAS_CHANGES_EXTENDED == 1
       case Orthanc::DatabasePluginMessages::OPERATION_GET_CHANGES_EXTENDED:
       {
@@ -895,16 +909,12 @@
       }
       
       case Orthanc::DatabasePluginMessages::OPERATION_GET_TOTAL_COMPRESSED_SIZE:
-      {
         response.mutable_get_total_compressed_size()->set_size(backend.GetTotalCompressedSize(manager));
         break;
-      }
       
       case Orthanc::DatabasePluginMessages::OPERATION_GET_TOTAL_UNCOMPRESSED_SIZE:
-      {
         response.mutable_get_total_uncompressed_size()->set_size(backend.GetTotalUncompressedSize(manager));
         break;
-      }
       
       case Orthanc::DatabasePluginMessages::OPERATION_IS_PROTECTED_PATIENT:
       {
@@ -928,16 +938,13 @@
       }
       
       case Orthanc::DatabasePluginMessages::OPERATION_LOG_CHANGE:
-      {
         backend.LogChange(manager, request.log_change().change_type(),
                           request.log_change().resource_id(),
                           Convert(request.log_change().resource_type()),
                           request.log_change().date().c_str());
         break;
-      }
       
       case Orthanc::DatabasePluginMessages::OPERATION_LOG_EXPORTED_RESOURCE:
-      {
         backend.LogExportedResource(manager,
                                     Convert(request.log_exported_resource().resource_type()),
                                     request.log_exported_resource().public_id().c_str(),
@@ -948,7 +955,6 @@
                                     request.log_exported_resource().series_instance_uid().c_str(),
                                     request.log_exported_resource().sop_instance_uid().c_str());
         break;
-      }
       
       case Orthanc::DatabasePluginMessages::OPERATION_LOOKUP_ATTACHMENT:
       {
@@ -1094,34 +1100,26 @@
       }
       
       case Orthanc::DatabasePluginMessages::OPERATION_SET_GLOBAL_PROPERTY:
-      {
         backend.SetGlobalProperty(manager, request.set_global_property().server_id().c_str(),
                                   request.set_global_property().property(),
                                   request.set_global_property().value().c_str());
         break;
-      }
       
       case Orthanc::DatabasePluginMessages::OPERATION_CLEAR_MAIN_DICOM_TAGS:
-      {
         backend.ClearMainDicomTags(manager, request.clear_main_dicom_tags().id());
         break;
-      }
       
       case Orthanc::DatabasePluginMessages::OPERATION_SET_METADATA:
-      {
         backend.SetMetadata(manager, request.set_metadata().id(),
                             request.set_metadata().metadata_type(),
                             request.set_metadata().value().c_str(),
                             request.set_metadata().revision());
         break;
-      }
       
       case Orthanc::DatabasePluginMessages::OPERATION_SET_PROTECTED_PATIENT:
-      {
         backend.SetProtectedPatient(manager, request.set_protected_patient().patient_id(),
                                     request.set_protected_patient().protected_patient());
         break;
-      }
       
       case Orthanc::DatabasePluginMessages::OPERATION_IS_DISK_SIZE_ABOVE:
       {
@@ -1131,10 +1129,8 @@
       }
       
       case Orthanc::DatabasePluginMessages::OPERATION_LOOKUP_RESOURCES:
-      {
         ApplyLookupResources(*response.mutable_lookup_resources(), request.lookup_resources(), backend, manager);
         break;
-      }
       
       case Orthanc::DatabasePluginMessages::OPERATION_CREATE_INSTANCE:
       {
@@ -1236,10 +1232,8 @@
       }
       
       case Orthanc::DatabasePluginMessages::OPERATION_GET_LAST_CHANGE_INDEX:
-      {
         response.mutable_get_last_change_index()->set_result(backend.GetLastChangeIndex(manager));
         break;
-      }
       
       case Orthanc::DatabasePluginMessages::OPERATION_LOOKUP_RESOURCE_AND_PARENT:
       {
@@ -1288,16 +1282,12 @@
       }
       
       case Orthanc::DatabasePluginMessages::OPERATION_ADD_LABEL:
-      {
         backend.AddLabel(manager, request.add_label().id(), request.add_label().label());
         break;
-      }
       
       case Orthanc::DatabasePluginMessages::OPERATION_REMOVE_LABEL:
-      {
         backend.RemoveLabel(manager, request.remove_label().id(), request.remove_label().label());
         break;
-      }
       
       case Orthanc::DatabasePluginMessages::OPERATION_LIST_LABELS:
       {
@@ -1323,16 +1313,96 @@
       
 #if ORTHANC_PLUGINS_HAS_INTEGRATED_FIND == 1
       case Orthanc::DatabasePluginMessages::OPERATION_FIND:
+        backend.ExecuteFind(response, manager, request.find());
+        break;
+
+      case Orthanc::DatabasePluginMessages::OPERATION_COUNT_RESOURCES:
+        backend.ExecuteCount(response, manager, request.find());
+        break;
+#endif
+
+#if ORTHANC_PLUGINS_HAS_KEY_VALUE_STORES == 1
+      case Orthanc::DatabasePluginMessages::OPERATION_STORE_KEY_VALUE:
+        backend.StoreKeyValue(manager, 
+                              request.store_key_value().store_id(),
+                              request.store_key_value().key(),
+                              request.store_key_value().value());
+        break;
+
+      case Orthanc::DatabasePluginMessages::OPERATION_GET_KEY_VALUE:
       {
-        backend.ExecuteFind(response, manager, request.find());
+        std::string value;
+        bool found = backend.GetKeyValue(value, manager,
+                                         request.get_key_value().store_id(),
+                                         request.get_key_value().key());
+        response.mutable_get_key_value()->set_found(found);
+
+        if (found)
+        {
+          response.mutable_get_key_value()->set_value(value);
+        }
         break;
       }
 
-      case Orthanc::DatabasePluginMessages::OPERATION_COUNT_RESOURCES:
+      case Orthanc::DatabasePluginMessages::OPERATION_DELETE_KEY_VALUE:
+        backend.DeleteKeyValue(manager, 
+                               request.delete_key_value().store_id(),
+                               request.delete_key_value().key());
+        break;
+
+      case Orthanc::DatabasePluginMessages::OPERATION_LIST_KEY_VALUES:
+        backend.ListKeysValues(response, manager, request.list_keys_values());
+        break;
+
+#endif
+
+#if ORTHANC_PLUGINS_HAS_QUEUES == 1
+      case Orthanc::DatabasePluginMessages::OPERATION_ENQUEUE_VALUE:
+        backend.EnqueueValue(manager,
+                             request.enqueue_value().queue_id(),
+                             request.enqueue_value().value());
+        break;
+
+      case Orthanc::DatabasePluginMessages::OPERATION_DEQUEUE_VALUE:
       {
-        backend.ExecuteCount(response, manager, request.find());
+        std::string value;
+        bool found = backend.DequeueValue(value, manager,
+                                          request.dequeue_value().queue_id(),
+                                          request.dequeue_value().origin() == Orthanc::DatabasePluginMessages::QUEUE_ORIGIN_FRONT);
+        response.mutable_dequeue_value()->set_found(found);
+        
+        if (found)
+        {
+          response.mutable_dequeue_value()->set_value(value);
+        }
+
         break;
       }
+
+      case Orthanc::DatabasePluginMessages::OPERATION_GET_QUEUE_SIZE:
+      {
+        uint64_t size = backend.GetQueueSize(manager,
+                                             request.get_queue_size().queue_id());
+        response.mutable_get_queue_size()->set_size(size);
+        break;
+      }
+
+#endif
+
+#if ORTHANC_PLUGINS_HAS_ATTACHMENTS_CUSTOM_DATA == 1
+      case Orthanc::DatabasePluginMessages::OPERATION_GET_ATTACHMENT_CUSTOM_DATA:
+      {
+        std::string customData;
+        backend.GetAttachmentCustomData(customData, manager, request.get_attachment_custom_data().uuid());
+        response.mutable_get_attachment_custom_data()->set_custom_data(customData);
+        break;
+      }
+
+      case Orthanc::DatabasePluginMessages::OPERATION_SET_ATTACHMENT_CUSTOM_DATA:
+        backend.SetAttachmentCustomData(manager,
+                                        request.set_attachment_custom_data().uuid(),
+                                        request.set_attachment_custom_data().custom_data());
+        break;
 #endif
 
       default:
@@ -1381,8 +1451,8 @@
         }
           
         default:
-          LOG(ERROR) << "Not implemented request type from protobuf: " << request.type();
-          break;
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented, "Not implemented request type from protobuf: " +
+                                          boost::lexical_cast<std::string>(request.type()));
       }
 
       std::string s;
--- a/Framework/Plugins/IDatabaseBackend.h	Mon May 19 11:18:43 2025 +0200
+++ b/Framework/Plugins/IDatabaseBackend.h	Fri Jun 13 15:16:52 2025 +0200
@@ -59,11 +59,26 @@
 
     virtual bool HasRevisionsSupport() const = 0;
 
+    virtual bool HasAttachmentCustomDataSupport() const = 0;
+
+    virtual bool HasKeyValueStores() const = 0;
+
+    virtual bool HasQueues() const = 0;
+
     virtual void AddAttachment(DatabaseManager& manager,
                                int64_t id,
                                const OrthancPluginAttachment& attachment,
                                int64_t revision) = 0;
 
+#if ORTHANC_PLUGINS_HAS_ATTACHMENTS_CUSTOM_DATA == 1
+    // New in Orthanc 1.12.8
+    virtual void AddAttachment(DatabaseManager& manager,
+                               int64_t id,
+                               const OrthancPluginAttachment& attachment,
+                               int64_t revision,
+                               const std::string& customData) = 0;
+#endif
+
     virtual void AttachChild(DatabaseManager& manager,
                              int64_t parent,
                              int64_t child) = 0;
@@ -400,6 +415,50 @@
                               const Orthanc::DatabasePluginMessages::Find_Request& request) = 0;
 #endif
 
+#if ORTHANC_PLUGINS_HAS_KEY_VALUE_STORES == 1
+    virtual void StoreKeyValue(DatabaseManager& manager,
+                               const std::string& storeId,
+                               const std::string& key,
+                               const std::string& value) = 0;
+
+    virtual void DeleteKeyValue(DatabaseManager& manager,
+                                const std::string& storeId,
+                                const std::string& key) = 0;
+
+    virtual bool GetKeyValue(std::string& value,
+                             DatabaseManager& manager,
+                             const std::string& storeId,
+                             const std::string& key) = 0;
+
+    virtual void ListKeysValues(Orthanc::DatabasePluginMessages::TransactionResponse& response,
+                                DatabaseManager& manager,
+                                const Orthanc::DatabasePluginMessages::ListKeysValues_Request& request) = 0;
+#endif
+
+#if ORTHANC_PLUGINS_HAS_QUEUES == 1
+    virtual void EnqueueValue(DatabaseManager& manager,
+                              const std::string& queueId,
+                              const std::string& value) = 0;
+
+    virtual bool DequeueValue(std::string& value,
+                              DatabaseManager& manager,
+                              const std::string& queueId,
+                              bool fromFront) = 0;
+
+    virtual uint64_t GetQueueSize(DatabaseManager& manager,
+                                  const std::string& queueId) = 0;
+#endif
+
+#if ORTHANC_PLUGINS_HAS_ATTACHMENTS_CUSTOM_DATA == 1
+    virtual void GetAttachmentCustomData(std::string& customData,
+                                         DatabaseManager& manager,
+                                         const std::string& attachmentUuid) = 0;
+
+    virtual void SetAttachmentCustomData(DatabaseManager& manager,
+                                         const std::string& attachmentUuid,
+                                         const std::string& customData) = 0;
+#endif
+
     virtual bool HasPerformDbHousekeeping() = 0;
 
     virtual void PerformDbHousekeeping(DatabaseManager& manager) = 0;
--- a/Framework/Plugins/IDatabaseBackendOutput.h	Mon May 19 11:18:43 2025 +0200
+++ b/Framework/Plugins/IDatabaseBackendOutput.h	Fri Jun 13 15:16:52 2025 +0200
@@ -56,7 +56,8 @@
                                          const std::string& uncompressedHash,
                                          int32_t            compressionType,
                                          uint64_t           compressedSize,
-                                         const std::string& compressedHash) = 0;
+                                         const std::string& compressedHash,
+                                         const std::string& customData) = 0;
 
     virtual void SignalDeletedResource(const std::string& publicId,
                                        OrthancPluginResourceType resourceType) = 0;
@@ -70,7 +71,8 @@
                                   const std::string& uncompressedHash,
                                   int32_t            compressionType,
                                   uint64_t           compressedSize,
-                                  const std::string& compressedHash) = 0;
+                                  const std::string& compressedHash,
+                                  const std::string& customData) = 0;
 
     virtual void AnswerChange(int64_t                    seq,
                               int32_t                    changeType,
--- a/Framework/Plugins/IndexBackend.cpp	Mon May 19 11:18:43 2025 +0200
+++ b/Framework/Plugins/IndexBackend.cpp	Fri Jun 13 15:16:52 2025 +0200
@@ -23,8 +23,6 @@
 
 #include "IndexBackend.h"
 
-#include "../Common/BinaryStringValue.h"
-#include "../Common/Integer64Value.h"
 #include "../Common/Utf8StringValue.h"
 #include "DatabaseBackendAdapterV2.h"
 #include "DatabaseBackendAdapterV3.h"
@@ -270,11 +268,13 @@
     DatabaseManager::CachedStatement statement(
       STATEMENT_FROM_HERE, manager,
       "SELECT uuid, fileType, uncompressedSize, uncompressedHash, compressionType, "
-      "compressedSize, compressedHash FROM DeletedFiles");
+      "compressedSize, compressedHash, revision, customData FROM DeletedFiles");
 
     statement.SetReadOnly(true);
     statement.Execute();
 
+    statement.SetResultFieldType(8, ValueType_BinaryString);
+
     while (!statement.IsDone())
     {
       output.SignalDeletedAttachment(statement.ReadString(0),
@@ -283,7 +283,8 @@
                                      statement.ReadString(3),
                                      statement.ReadInteger32(4),
                                      statement.ReadInteger64(5),
-                                     statement.ReadString(6));
+                                     statement.ReadString(6),
+                                     statement.ReadStringOrNull(8));
       
       statement.Next();
     }
@@ -354,12 +355,26 @@
     }
   }
 
-
-  static void ExecuteAddAttachment(DatabaseManager::CachedStatement& statement,
-                                   Dictionary& args,
+  static void ExecuteAddAttachment(DatabaseManager& manager,
                                    int64_t id,
-                                   const OrthancPluginAttachment& attachment)
+                                   const char* uuid,
+                                   int32_t     contentType,
+                                   uint64_t    uncompressedSize,
+                                   const char* uncompressedHash,
+                                   int32_t     compressionType,
+                                   uint64_t    compressedSize,
+                                   const char* compressedHash,
+                                   const void* customData,
+                                   size_t      customDataSize,
+                                   int64_t     revision)
   {
+    DatabaseManager::CachedStatement statement(
+      STATEMENT_FROM_HERE, manager,
+      "INSERT INTO AttachedFiles VALUES(${id}, ${type}, ${uuid}, ${compressed}, "
+      "${uncompressed}, ${compression}, ${hash}, ${hash-compressed}, ${revision}, ${custom-data})");
+
+    Dictionary args;
+
     statement.SetParameterType("id", ValueType_Integer64);
     statement.SetParameterType("type", ValueType_Integer64);
     statement.SetParameterType("uuid", ValueType_Utf8String);
@@ -368,52 +383,50 @@
     statement.SetParameterType("compression", ValueType_Integer64);
     statement.SetParameterType("hash", ValueType_Utf8String);
     statement.SetParameterType("hash-compressed", ValueType_Utf8String);
+    statement.SetParameterType("revision", ValueType_Integer64);
+    statement.SetParameterType("custom-data", ValueType_Utf8String);
 
     args.SetIntegerValue("id", id);
-    args.SetIntegerValue("type", attachment.contentType);
-    args.SetUtf8Value("uuid", attachment.uuid);
-    args.SetIntegerValue("compressed", attachment.compressedSize);
-    args.SetIntegerValue("uncompressed", attachment.uncompressedSize);
-    args.SetIntegerValue("compression", attachment.compressionType);
-    args.SetUtf8Value("hash", attachment.uncompressedHash);
-    args.SetUtf8Value("hash-compressed", attachment.compressedHash);
+    args.SetIntegerValue("type", contentType);
+    args.SetUtf8Value("uuid", uuid);
+    args.SetIntegerValue("compressed", compressedSize);
+    args.SetIntegerValue("uncompressed", uncompressedSize);
+    args.SetIntegerValue("compression", compressionType);
+    args.SetUtf8Value("hash", uncompressedHash);
+    args.SetUtf8Value("hash-compressed", compressedHash);
+    args.SetIntegerValue("revision", revision);
+    args.SetBinaryValue("custom-data", customData, customDataSize);
 
     statement.Execute(args);
   }
 
-  
+
   void IndexBackend::AddAttachment(DatabaseManager& manager,
                                    int64_t id,
                                    const OrthancPluginAttachment& attachment,
                                    int64_t revision)
   {
-    if (HasRevisionsSupport())
-    {
-      DatabaseManager::CachedStatement statement(
-        STATEMENT_FROM_HERE, manager,
-        "INSERT INTO AttachedFiles VALUES(${id}, ${type}, ${uuid}, ${compressed}, "
-        "${uncompressed}, ${compression}, ${hash}, ${hash-compressed}, ${revision})");
-
-      Dictionary args;
-
-      statement.SetParameterType("revision", ValueType_Integer64);
-      args.SetIntegerValue("revision", revision);
-      
-      ExecuteAddAttachment(statement, args, id, attachment);
-    }
-    else
-    {
-      DatabaseManager::CachedStatement statement(
-        STATEMENT_FROM_HERE, manager,
-        "INSERT INTO AttachedFiles VALUES(${id}, ${type}, ${uuid}, ${compressed}, "
-        "${uncompressed}, ${compression}, ${hash}, ${hash-compressed})");
-
-      Dictionary args;
-      ExecuteAddAttachment(statement, args, id, attachment);
-    }
+    assert(HasRevisionsSupport()); // all plugins support these features now
+    ExecuteAddAttachment(manager, id, attachment.uuid, attachment.contentType, attachment.uncompressedSize, attachment.uncompressedHash,
+                         attachment.compressionType, attachment.compressedSize, attachment.compressedHash, NULL, 0, revision);
   }
 
-    
+
+#if ORTHANC_PLUGINS_HAS_ATTACHMENTS_CUSTOM_DATA == 1
+  void IndexBackend::AddAttachment(DatabaseManager& manager,
+                                   int64_t id,
+                                   const OrthancPluginAttachment& attachment,
+                                   int64_t revision,
+                                   const std::string& customData)
+  {
+    assert(HasRevisionsSupport() && HasAttachmentCustomDataSupport());
+    ExecuteAddAttachment(manager, id, attachment.uuid, attachment.contentType, attachment.uncompressedSize, attachment.uncompressedHash,
+                         attachment.compressionType, attachment.compressedSize, attachment.compressedHash,
+                         customData.empty() ? NULL : customData.c_str(), customData.size(), revision);
+  }
+#endif
+
+
   void IndexBackend::AttachChild(DatabaseManager& manager,
                                  int64_t parent,
                                  int64_t child)
@@ -957,7 +970,7 @@
 
     if (statement.IsDone())
     {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource, "No resource type found for internal id.");
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource, "No resource type found for internal id");
     }
     else
     {
@@ -1178,12 +1191,20 @@
     statement.Execute(args);
   }
 
-
-  static bool ExecuteLookupAttachment(DatabaseManager::CachedStatement& statement,
-                                      IDatabaseBackendOutput& output,
+    
+  bool IndexBackend::LookupAttachment(IDatabaseBackendOutput& output,
+                                      int64_t& revision /*out*/,
+                                      DatabaseManager& manager,
                                       int64_t id,
                                       int32_t contentType)
   {
+    assert(HasRevisionsSupport() && HasAttachmentCustomDataSupport()); // we have forced all v4 plugins to support both ! 
+    DatabaseManager::CachedStatement statement(
+      STATEMENT_FROM_HERE, manager,
+      "SELECT uuid, uncompressedSize, compressionType, compressedSize, uncompressedHash, "
+      "compressedHash, revision, customData FROM AttachedFiles WHERE id=${id} AND fileType=${type}");
+
+
     statement.SetReadOnly(true);
     statement.SetParameterType("id", ValueType_Integer64);
     statement.SetParameterType("type", ValueType_Integer64);
@@ -1193,6 +1214,7 @@
     args.SetIntegerValue("type", static_cast<int>(contentType));
 
     statement.Execute(args);
+    statement.SetResultFieldType(7, ValueType_BinaryString);
 
     if (statement.IsDone())
     {
@@ -1200,64 +1222,31 @@
     }
     else
     {
+      if (statement.GetResultField(6).GetType() == ValueType_Null)
+      {
+        // "NULL" can happen with a database created by PostgreSQL
+        // plugin <= 3.3 (because of "ALTER TABLE AttachedFiles")
+        revision = 0;
+      }
+      else
+      {
+        revision = statement.ReadInteger64(6);
+      }
+
+      std::string customData;
+      customData = statement.ReadStringOrNull(7);
+
       output.AnswerAttachment(statement.ReadString(0),
                               contentType,
                               statement.ReadInteger64(1),
                               statement.ReadString(4),
                               statement.ReadInteger32(2),
                               statement.ReadInteger64(3),
-                              statement.ReadString(5));
+                              statement.ReadString(5),
+                              customData);
       return true;
     }
-  }
-                                      
-  
-    
-  /* Use GetOutput().AnswerAttachment() */
-  bool IndexBackend::LookupAttachment(IDatabaseBackendOutput& output,
-                                      int64_t& revision /*out*/,
-                                      DatabaseManager& manager,
-                                      int64_t id,
-                                      int32_t contentType)
-  {
-    if (HasRevisionsSupport())
-    {
-      DatabaseManager::CachedStatement statement(
-        STATEMENT_FROM_HERE, manager,
-        "SELECT uuid, uncompressedSize, compressionType, compressedSize, uncompressedHash, "
-        "compressedHash, revision FROM AttachedFiles WHERE id=${id} AND fileType=${type}");
-      
-      if (ExecuteLookupAttachment(statement, output, id, contentType))
-      {
-        if (statement.GetResultField(6).GetType() == ValueType_Null)
-        {
-          // "NULL" can happen with a database created by PostgreSQL
-          // plugin <= 3.3 (because of "ALTER TABLE AttachedFiles")
-          revision = 0;
-        }
-        else
-        {
-          revision = statement.ReadInteger64(6);
-        }
-        
-        return true;
-      }
-      else
-      {
-        return false;
-      }
-    }
-    else
-    {
-      DatabaseManager::CachedStatement statement(
-        STATEMENT_FROM_HERE, manager,
-        "SELECT uuid, uncompressedSize, compressionType, compressedSize, uncompressedHash, "
-        "compressedHash FROM AttachedFiles WHERE id=${id} AND fileType=${type}");
-      
-      revision = 0;
-
-      return ExecuteLookupAttachment(statement, output, id, contentType);
-    }
+
   }
 
 
@@ -3253,11 +3242,12 @@
 #define C3_STRING_1 3
 #define C4_STRING_2 4
 #define C5_STRING_3 5
-#define C6_INT_1 6
-#define C7_INT_2 7
-#define C8_INT_3 8
-#define C9_BIG_INT_1 9
-#define C10_BIG_INT_2 10
+#define C6_STRING_4 6
+#define C7_INT_1 7
+#define C8_INT_2 8
+#define C9_INT_3 9
+#define C10_BIG_INT_1 10
+#define C11_BIG_INT_2 11
 
 #define QUERY_LOOKUP 1
 #define QUERY_MAIN_DICOM_TAGS 2
@@ -3431,11 +3421,11 @@
     std::string revisionInC7;
     if (HasRevisionsSupport())
     {
-      revisionInC7 = "  revision AS c7_int2, ";
+      revisionInC7 = "  revision AS c8_int2, ";
     }
     else
     {
-      revisionInC7 = "  0 AS C7_int2, ";
+      revisionInC7 = "  0 AS c8_int2, ";
     }
 
 
@@ -3446,11 +3436,12 @@
           "  Lookup.publicId AS c3_string1, "
           "  " + formatter.FormatNull("TEXT") + " AS c4_string2, "
           "  " + formatter.FormatNull("TEXT") + " AS c5_string3, "
-          "  " + formatter.FormatNull("INT") + " AS c6_int1, "
-          "  " + formatter.FormatNull("INT") + " AS c7_int2, "
-          "  " + formatter.FormatNull("INT") + " AS c8_int3, "
-          "  " + formatter.FormatNull("BIGINT") + " AS c9_big_int1, "
-          "  " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 "
+          "  " + formatter.FormatNull("BYTEA") + " AS c6_string4, "
+          "  " + formatter.FormatNull("INT") + " AS c7_int1, "
+          "  " + formatter.FormatNull("INT") + " AS c8_int2, "
+          "  " + formatter.FormatNull("INT") + " AS c9_int3, "
+          "  " + formatter.FormatNull("BIGINT") + " AS c10_big_int1, "
+          "  " + formatter.FormatNull("BIGINT") + " AS c11_big_int2 "
           "  FROM Lookup ";
 
     // need MainDicomTags from resource ?
@@ -3463,11 +3454,12 @@
              "  value AS c3_string1, "
              "  " + formatter.FormatNull("TEXT") + " AS c4_string2, "
              "  " + formatter.FormatNull("TEXT") + " AS c5_string3, "
-             "  tagGroup AS c6_int1, "
-             "  tagElement AS c7_int2, "
-             "  " + formatter.FormatNull("INT") + " AS c8_int3, "
-             "  " + formatter.FormatNull("BIGINT") + " AS c9_big_int1, "
-             "  " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 "
+             "  " + formatter.FormatNull("BYTEA") + " AS c6_string4, "
+             "  tagGroup AS c7_int1, "
+             "  tagElement AS c8_int2, "
+             "  " + formatter.FormatNull("INT") + " AS c9_int3, "
+             "  " + formatter.FormatNull("BIGINT") + " AS c10_big_int1, "
+             "  " + formatter.FormatNull("BIGINT") + " AS c11_big_int2 "
              "FROM Lookup "
              "INNER JOIN MainDicomTags ON MainDicomTags.id = Lookup.internalId ";
     }
@@ -3482,11 +3474,12 @@
              "  value AS c3_string1, "
              "  " + formatter.FormatNull("TEXT") + " AS c4_string2, "
              "  " + formatter.FormatNull("TEXT") + " AS c5_string3, "
-             "  type AS c6_int1, "
+             "  " + formatter.FormatNull("BYTEA") + " AS c6_string4, "
+             "  type AS c7_int1, "
              + revisionInC7 +
-             "  " + formatter.FormatNull("INT") + " AS c8_int3, "
-             "  " + formatter.FormatNull("BIGINT") + " AS c9_big_int1, "
-             "  " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 "
+             "  " + formatter.FormatNull("INT") + " AS c9_int3, "
+             "  " + formatter.FormatNull("BIGINT") + " AS c10_big_int1, "
+             "  " + formatter.FormatNull("BIGINT") + " AS c11_big_int2 "
              "FROM Lookup "
              "INNER JOIN Metadata ON Metadata.id = Lookup.internalId ";
     }
@@ -3501,11 +3494,12 @@
              "  uuid AS c3_string1, "
              "  uncompressedHash AS c4_string2, "
              "  compressedHash AS c5_string3, "
-             "  fileType AS c6_int1, "
+             "  customData AS c6_string4, "
+             "  fileType AS c7_int1, "
              + revisionInC7 +
-             "  compressionType AS c8_int3, "
-             "  compressedSize AS c9_big_int1, "
-             "  uncompressedSize AS c10_big_int2 "
+             "  compressionType AS c9_int3, "
+             "  compressedSize AS c10_big_int1, "
+             "  uncompressedSize AS c11_big_int2 "
              "FROM Lookup "
              "INNER JOIN AttachedFiles ON AttachedFiles.id = Lookup.internalId ";
     }
@@ -3520,11 +3514,12 @@
              "  label AS c3_string1, "
              "  " + formatter.FormatNull("TEXT") + " AS c4_string2, "
              "  " + formatter.FormatNull("TEXT") + " AS c5_string3, "
-             "  " + formatter.FormatNull("INT") + " AS c6_int1, "
-             "  " + formatter.FormatNull("INT") + " AS c7_int2, "
-             "  " + formatter.FormatNull("INT") + " AS c8_int3, "
-             "  " + formatter.FormatNull("BIGINT") + " AS c9_big_int1, "
-             "  " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 "
+             "  " + formatter.FormatNull("BYTEA") + " AS c6_string4, "
+             "  " + formatter.FormatNull("INT") + " AS c7_int1, "
+             "  " + formatter.FormatNull("INT") + " AS c8_int2, "
+             "  " + formatter.FormatNull("INT") + " AS c9_int3, "
+             "  " + formatter.FormatNull("BIGINT") + " AS c10_big_int1, "
+             "  " + formatter.FormatNull("BIGINT") + " AS c11_big_int2 "
              "FROM Lookup "
              "INNER JOIN Labels ON Labels.id = Lookup.internalId ";
     }
@@ -3558,11 +3553,12 @@
                "  value AS c3_string1, "
                "  " + formatter.FormatNull("TEXT") + " AS c4_string2, "
                "  " + formatter.FormatNull("TEXT") + " AS c5_string3, "
-               "  tagGroup AS c6_int1, "
-               "  tagElement AS c7_int2, "
-               "  " + formatter.FormatNull("INT") + " AS c8_int3, "
-               "  " + formatter.FormatNull("BIGINT") + " AS c9_big_int1, "
-               "  " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 "
+               "  " + formatter.FormatNull("BYTEA") + " AS c6_string4, "
+               "  tagGroup AS c7_int1, "
+               "  tagElement AS c8_int2, "
+               "  " + formatter.FormatNull("INT") + " AS c9_int3, "
+               "  " + formatter.FormatNull("BIGINT") + " AS c10_big_int1, "
+               "  " + formatter.FormatNull("BIGINT") + " AS c11_big_int2 "
                "FROM Lookup "
                "INNER JOIN Resources currentLevel ON Lookup.internalId = currentLevel.internalId "
                "INNER JOIN MainDicomTags ON MainDicomTags.id = currentLevel.parentId ";
@@ -3577,11 +3573,12 @@
                "  value AS c3_string1, "
                "  " + formatter.FormatNull("TEXT") + " AS c4_string2, "
                "  " + formatter.FormatNull("TEXT") + " AS c5_string3, "
-               "  type AS c6_int1, "
+               "  " + formatter.FormatNull("BYTEA") + " AS c6_string4, "
+               "  type AS c7_int1, "
                + revisionInC7 +
-               "  " + formatter.FormatNull("INT") + " AS c8_int3, "
-               "  " + formatter.FormatNull("BIGINT") + " AS c9_big_int1, "
-               "  " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 "
+               "  " + formatter.FormatNull("INT") + " AS c9_int3, "
+               "  " + formatter.FormatNull("BIGINT") + " AS c10_big_int1, "
+               "  " + formatter.FormatNull("BIGINT") + " AS c11_big_int2 "
                "FROM Lookup "
                "INNER JOIN Resources currentLevel ON Lookup.internalId = currentLevel.internalId "
                "INNER JOIN Metadata ON Metadata.id = currentLevel.parentId ";
@@ -3613,11 +3610,12 @@
                "  value AS c3_string1, "
                "  " + formatter.FormatNull("TEXT") + " AS c4_string2, "
                "  " + formatter.FormatNull("TEXT") + " AS c5_string3, "
-               "  tagGroup AS c6_int1, "
-               "  tagElement AS c7_int2, "
-               "  " + formatter.FormatNull("INT") + " AS c8_int3, "
-               "  " + formatter.FormatNull("BIGINT") + " AS c9_big_int1, "
-               "  " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 "
+               "  " + formatter.FormatNull("BYTEA") + " AS c6_string4, "
+               "  tagGroup AS c7_int1, "
+               "  tagElement AS c8_int2, "
+               "  " + formatter.FormatNull("INT") + " AS c9_int3, "
+               "  " + formatter.FormatNull("BIGINT") + " AS c10_big_int1, "
+               "  " + formatter.FormatNull("BIGINT") + " AS c11_big_int2 "
                "FROM Lookup "
                "INNER JOIN Resources currentLevel ON Lookup.internalId = currentLevel.internalId "
                "INNER JOIN Resources parentLevel ON currentLevel.parentId = parentLevel.internalId "
@@ -3633,11 +3631,12 @@
                 "  value AS c3_string1, "
                 "  " + formatter.FormatNull("TEXT") + " AS c4_string2, "
                 "  " + formatter.FormatNull("TEXT") + " AS c5_string3, "
-                "  type AS c6_int1, "
+                "  " + formatter.FormatNull("BYTEA") + " AS c6_string4, "
+                "  type AS c7_int1, "
                 + revisionInC7 +
-                "  " + formatter.FormatNull("INT") + " AS c8_int3, "
-                "  " + formatter.FormatNull("BIGINT") + " AS c9_big_int1, "
-                "  " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 "
+                "  " + formatter.FormatNull("INT") + " AS c9_int3, "
+                "  " + formatter.FormatNull("BIGINT") + " AS c10_big_int1, "
+                "  " + formatter.FormatNull("BIGINT") + " AS c11_big_int2 "
                 "FROM Lookup "
                 "INNER JOIN Resources currentLevel ON Lookup.internalId = currentLevel.internalId "
                 "INNER JOIN Resources parentLevel ON currentLevel.parentId = parentLevel.internalId "
@@ -3660,11 +3659,12 @@
                "  value AS c3_string1, "
                "  " + formatter.FormatNull("TEXT") + " AS c4_string2, "
                "  " + formatter.FormatNull("TEXT") + " AS c5_string3, "
-               "  tagGroup AS c6_int1, "
-               "  tagElement AS c7_int2, "
-               "  " + formatter.FormatNull("INT") + " AS c8_int3, "
-               "  " + formatter.FormatNull("BIGINT") + " AS c9_big_int1, "
-               "  " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 "
+               "  " + formatter.FormatNull("BYTEA") + " AS c6_string4, "
+               "  tagGroup AS c7_int1, "
+               "  tagElement AS c8_int2, "
+               "  " + formatter.FormatNull("INT") + " AS c9_int3, "
+               "  " + formatter.FormatNull("BIGINT") + " AS c10_big_int1, "
+               "  " + formatter.FormatNull("BIGINT") + " AS c11_big_int2 "
                "FROM Lookup "
                "  INNER JOIN Resources childLevel ON childLevel.parentId = Lookup.internalId "
                "  INNER JOIN MainDicomTags ON MainDicomTags.id = childLevel.internalId AND (tagGroup, tagElement) IN (" + JoinRequestedTags(childrenSpec) + ")";
@@ -3680,11 +3680,12 @@
                "  childLevel.publicId AS c3_string1, "
                "  " + formatter.FormatNull("TEXT") + " AS c4_string2, "
                "  " + formatter.FormatNull("TEXT") + " AS c5_string3, "
-               "  " + formatter.FormatNull("INT") + " AS c6_int1, "
-               "  " + formatter.FormatNull("INT") + " AS c7_int2, "
-               "  " + formatter.FormatNull("INT") + " AS c8_int3, "
-               "  " + formatter.FormatNull("BIGINT") + " AS c9_big_int1, "
-               "  " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 "
+               "  " + formatter.FormatNull("BYTEA") + " AS c6_string4, "
+               "  " + formatter.FormatNull("INT") + " AS c7_int1, "
+               "  " + formatter.FormatNull("INT") + " AS c8_int2, "
+               "  " + formatter.FormatNull("INT") + " AS c9_int3, "
+               "  " + formatter.FormatNull("BIGINT") + " AS c10_big_int1, "
+               "  " + formatter.FormatNull("BIGINT") + " AS c11_big_int2 "
                "FROM Lookup "
                "  INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId ";
       }
@@ -3715,16 +3716,17 @@
                 "  " + formatter.FormatNull("TEXT") + " AS c3_string1, "
                 "  " + formatter.FormatNull("TEXT") + " AS c4_string2, "
                 "  " + formatter.FormatNull("TEXT") + " AS c5_string3, "
-                "  " + formatter.FormatNull("INT") + " AS c6_int1, "
-                "  " + formatter.FormatNull("INT") + " AS c7_int2, "
-                "  " + formatter.FormatNull("INT") + " AS c8_int3, "
+                "  " + formatter.FormatNull("BYTEA") + " AS c6_string4, "
+                "  " + formatter.FormatNull("INT") + " AS c7_int1, "
+                "  " + formatter.FormatNull("INT") + " AS c8_int2, "
+                "  " + formatter.FormatNull("INT") + " AS c9_int3, "
                 "  COALESCE("
                 "           (" + getResourcesChildCount + "),"
                 "        		(SELECT COUNT(childLevel.internalId)"
                 "            FROM Resources AS childLevel"
                 "            WHERE Lookup.internalId = childLevel.parentId"
-                "           )) AS c9_big_int1, "
-                "  " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 "
+                "           )) AS c10_big_int1, "
+                "  " + formatter.FormatNull("BIGINT") + " AS c11_big_int2 "
                 "FROM Lookup "
                 "LEFT JOIN Resources ON Lookup.internalId = Resources.internalId ";
         }
@@ -3737,11 +3739,12 @@
                 "  " + formatter.FormatNull("TEXT") + " AS c3_string1, "
                 "  " + formatter.FormatNull("TEXT") + " AS c4_string2, "
                 "  " + formatter.FormatNull("TEXT") + " AS c5_string3, "
-                "  " + formatter.FormatNull("INT") + " AS c6_int1, "
-                "  " + formatter.FormatNull("INT") + " AS c7_int2, "
-                "  " + formatter.FormatNull("INT") + " AS c8_int3, "
-                "  COUNT(childLevel.internalId) AS c9_big_int1, "
-                "  " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 "
+                "  " + formatter.FormatNull("BYTEA") + " AS c6_string4, "
+                "  " + formatter.FormatNull("INT") + " AS c7_int1, "
+                "  " + formatter.FormatNull("INT") + " AS c8_int2, "
+                "  " + formatter.FormatNull("INT") + " AS c9_int3, "
+                "  COUNT(childLevel.internalId) AS c10_big_int1, "
+                "  " + formatter.FormatNull("BIGINT") + " AS c11_big_int2 "
                 "FROM Lookup "
                 "  INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId GROUP BY Lookup.internalId ";
         }
@@ -3756,11 +3759,12 @@
                 "  value AS c3_string1, "
                 "  " + formatter.FormatNull("TEXT") + " AS c4_string2, "
                 "  " + formatter.FormatNull("TEXT") + " AS c5_string3, "
-                "  type AS c6_int1, "
+                "  " + formatter.FormatNull("BYTEA") + " AS c6_string4, "
+                "  type AS c7_int1, "
                 + revisionInC7 +
-                "  " + formatter.FormatNull("INT") + " AS c8_int3, "
-                "  " + formatter.FormatNull("BIGINT") + " AS c9_big_int1, "
-                "  " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 "
+                "  " + formatter.FormatNull("INT") + " AS c9_int3, "
+                "  " + formatter.FormatNull("BIGINT") + " AS c10_big_int1, "
+                "  " + formatter.FormatNull("BIGINT") + " AS c11_big_int2 "
                 "FROM Lookup "
                 "  INNER JOIN Resources childLevel ON childLevel.parentId = Lookup.internalId "
                 "  INNER JOIN Metadata ON Metadata.id = childLevel.internalId AND Metadata.type IN (" + JoinRequestedMetadata(childrenSpec) + ") ";
@@ -3780,11 +3784,12 @@
                 "  grandChildLevel.publicId AS c3_string1, "
                 "  " + formatter.FormatNull("TEXT") + " AS c4_string2, "
                 "  " + formatter.FormatNull("TEXT") + " AS c5_string3, "
-                "  " + formatter.FormatNull("INT") + " AS c6_int1, "
-                "  " + formatter.FormatNull("INT") + " AS c7_int2, "
-                "  " + formatter.FormatNull("INT") + " AS c8_int3, "
-                "  " + formatter.FormatNull("BIGINT") + " AS c9_big_int1, "
-                "  " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 "
+                "  " + formatter.FormatNull("BYTEA") + " AS c6_string4, "
+                "  " + formatter.FormatNull("INT") + " AS c7_int1, "
+                "  " + formatter.FormatNull("INT") + " AS c8_int2, "
+                "  " + formatter.FormatNull("INT") + " AS c9_int3, "
+                "  " + formatter.FormatNull("BIGINT") + " AS c10_big_int1, "
+                "  " + formatter.FormatNull("BIGINT") + " AS c11_big_int2 "
                 "FROM Lookup "
                 "INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId "
                 "INNER JOIN Resources grandChildLevel ON childLevel.internalId = grandChildLevel.parentId ";
@@ -3793,6 +3798,7 @@
         {
           if (HasChildCountColumn())
           {
+
             // we get the count value either from the childCount column if it has been computed or from the Resources table
             std::string getResourcesGrandChildCount;
             
@@ -3815,23 +3821,25 @@
 
             // we get the count value either from the childCount column if it has been computed or from the Resources table
             sql += "UNION ALL SELECT "
+
                     "  " TOSTRING(QUERY_GRAND_CHILDREN_COUNT) " AS c0_queryId, "
                     "  Lookup.internalId AS c1_internalId, "
                     "  " + formatter.FormatNull("BIGINT") + " AS c2_rowNumber, "
                     "  " + formatter.FormatNull("TEXT") + " AS c3_string1, "
                     "  " + formatter.FormatNull("TEXT") + " AS c4_string2, "
                     "  " + formatter.FormatNull("TEXT") + " AS c5_string3, "
-                    "  " + formatter.FormatNull("INT") + " AS c6_int1, "
-                    "  " + formatter.FormatNull("INT") + " AS c7_int2, "
-                    "  " + formatter.FormatNull("INT") + " AS c8_int3, "
+                    "  " + formatter.FormatNull("BYTEA") + " AS c6_string4, "
+                    "  " + formatter.FormatNull("INT") + " AS c7_int1, "
+                    "  " + formatter.FormatNull("INT") + " AS c8_int2, "
+                    "  " + formatter.FormatNull("INT") + " AS c9_int3, "
                     "  COALESCE("
                     "           (" + getResourcesGrandChildCount + "),"
                     "        		(SELECT COUNT(grandChildLevel.internalId)"
                     "            FROM Resources AS childLevel"
                     "            INNER JOIN Resources AS grandChildLevel ON childLevel.internalId = grandChildLevel.parentId"
                     "            WHERE Lookup.internalId = childLevel.parentId"
-                    "           )) AS c9_big_int1, "
-                    "  " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 "
+                    "           )) AS c10_big_int1, "
+                    "  " + formatter.FormatNull("BIGINT") + " AS c11_big_int2 "
                     "FROM Lookup ";
           }
           else
@@ -3843,11 +3851,12 @@
                   "  " + formatter.FormatNull("TEXT") + " AS c3_string1, "
                   "  " + formatter.FormatNull("TEXT") + " AS c4_string2, "
                   "  " + formatter.FormatNull("TEXT") + " AS c5_string3, "
-                  "  " + formatter.FormatNull("INT") + " AS c6_int1, "
-                  "  " + formatter.FormatNull("INT") + " AS c7_int2, "
-                  "  " + formatter.FormatNull("INT") + " AS c8_int3, "
-                  "  COUNT(grandChildLevel.internalId) AS c9_big_int1, "
-                  "  " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 "
+                  "  " + formatter.FormatNull("BYTEA") + " AS c6_string4, "
+                  "  " + formatter.FormatNull("INT") + " AS c7_int1, "
+                  "  " + formatter.FormatNull("INT") + " AS c8_int2, "
+                  "  " + formatter.FormatNull("INT") + " AS c9_int3, "
+                  "  COUNT(grandChildLevel.internalId) AS c10_big_int1, "
+                  "  " + formatter.FormatNull("BIGINT") + " AS c11_big_int2 "
                   "FROM Lookup "
                   "  INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId "
                   "  INNER JOIN Resources grandChildLevel ON childLevel.internalId = grandChildLevel.parentId GROUP BY Lookup.internalId ";
@@ -3863,11 +3872,12 @@
                  "  value AS c3_string1, "
                  "  " + formatter.FormatNull("TEXT") + " AS c4_string2, "
                  "  " + formatter.FormatNull("TEXT") + " AS c5_string3, "
-                 "  tagGroup AS c6_int1, "
-                 "  tagElement AS c7_int2, "
-                 "  " + formatter.FormatNull("INT") + " AS c8_int3, "
-                 "  " + formatter.FormatNull("BIGINT") + " AS c9_big_int1, "
-                 "  " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 "
+                 "  " + formatter.FormatNull("BYTEA") + " AS c6_string4, "
+                 "  tagGroup AS c7_int1, "
+                 "  tagElement AS c8_int2, "
+                 "  " + formatter.FormatNull("INT") + " AS c9_int3, "
+                 "  " + formatter.FormatNull("BIGINT") + " AS c10_big_int1, "
+                 "  " + formatter.FormatNull("BIGINT") + " AS c11_big_int2 "
                  "FROM Lookup "
                  "  INNER JOIN Resources childLevel ON childLevel.parentId = Lookup.internalId "
                  "  INNER JOIN Resources grandChildLevel ON grandChildLevel.parentId = childLevel.internalId "
@@ -3883,11 +3893,12 @@
                  "  value AS c3_string1, "
                  "  " + formatter.FormatNull("TEXT") + " AS c4_string2, "
                  "  " + formatter.FormatNull("TEXT") + " AS c5_string3, "
-                 "  type AS c6_int1, "
+                 "  " + formatter.FormatNull("BYTEA") + " AS c6_string4, "
+                 "  type AS c7_int1, "
                  + revisionInC7 +
-                 "  " + formatter.FormatNull("INT") + " AS c8_int3, "
-                 "  " + formatter.FormatNull("BIGINT") + " AS c9_big_int1, "
-                 "  " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 "
+                 "  " + formatter.FormatNull("INT") + " AS c9_int3, "
+                 "  " + formatter.FormatNull("BIGINT") + " AS c10_big_int1, "
+                 "  " + formatter.FormatNull("BIGINT") + " AS c11_big_int2 "
                  "FROM Lookup "
                  "  INNER JOIN Resources childLevel ON childLevel.parentId = Lookup.internalId "
                  "  INNER JOIN Resources grandChildLevel ON grandChildLevel.parentId = childLevel.internalId "
@@ -3908,11 +3919,12 @@
                   "  grandGrandChildLevel.publicId AS c3_string1, "
                   "  " + formatter.FormatNull("TEXT") + " AS c4_string2, "
                   "  " + formatter.FormatNull("TEXT") + " AS c5_string3, "
-                  "  " + formatter.FormatNull("INT") + " AS c6_int1, "
-                  "  " + formatter.FormatNull("INT") + " AS c7_int2, "
-                  "  " + formatter.FormatNull("INT") + " AS c8_int3, "
-                  "  " + formatter.FormatNull("BIGINT") + " AS c9_big_int1, "
-                  "  " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 "
+                  "  " + formatter.FormatNull("BYTEA") + " AS c6_string4, "
+                  "  " + formatter.FormatNull("INT") + " AS c7_int1, "
+                  "  " + formatter.FormatNull("INT") + " AS c8_int2, "
+                  "  " + formatter.FormatNull("INT") + " AS c9_int3, "
+                  "  " + formatter.FormatNull("BIGINT") + " AS c10_big_int1, "
+                  "  " + formatter.FormatNull("BIGINT") + " AS c11_big_int2 "
                   "FROM Lookup "
                   "INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId "
                   "INNER JOIN Resources grandChildLevel ON childLevel.internalId = grandChildLevel.parentId "
@@ -3952,9 +3964,10 @@
                     "  " + formatter.FormatNull("TEXT") + " AS c3_string1, "
                     "  " + formatter.FormatNull("TEXT") + " AS c4_string2, "
                     "  " + formatter.FormatNull("TEXT") + " AS c5_string3, "
-                    "  " + formatter.FormatNull("INT") + " AS c6_int1, "
-                    "  " + formatter.FormatNull("INT") + " AS c7_int2, "
-                    "  " + formatter.FormatNull("INT") + " AS c8_int3, "
+                    "  " + formatter.FormatNull("BYTEA") + " AS c6_string4, "
+                    "  " + formatter.FormatNull("INT") + " AS c7_int1, "
+                    "  " + formatter.FormatNull("INT") + " AS c8_int2, "
+                    "  " + formatter.FormatNull("INT") + " AS c9_int3, "
                     "  COALESCE("
                     "           (" + getResourcesGrandGrandChildCount + "),"
                     "        		(SELECT COUNT(grandGrandChildLevel.internalId)"
@@ -3962,8 +3975,8 @@
                     "            INNER JOIN Resources AS grandChildLevel ON childLevel.internalId = grandChildLevel.parentId"
                     "            INNER JOIN Resources AS grandGrandChildLevel ON grandChildLevel.internalId = grandGrandChildLevel.parentId"
                     "            WHERE Lookup.internalId = childLevel.parentId"
-                    "           )) AS c9_big_int1, "
-                    "  " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 "
+                    "           )) AS c10_big_int1, "
+                    "  " + formatter.FormatNull("BIGINT") + " AS c11_big_int2 "
                     "FROM Lookup ";
             }
             else
@@ -3975,11 +3988,12 @@
                     "  " + formatter.FormatNull("TEXT") + " AS c3_string1, "
                     "  " + formatter.FormatNull("TEXT") + " AS c4_string2, "
                     "  " + formatter.FormatNull("TEXT") + " AS c5_string3, "
-                    "  " + formatter.FormatNull("INT") + " AS c6_int1, "
-                    "  " + formatter.FormatNull("INT") + " AS c7_int2, "
-                    "  " + formatter.FormatNull("INT") + " AS c8_int3, "
-                    "  COUNT(grandChildLevel.internalId) AS c9_big_int1, "
-                    "  " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 "
+                    "  " + formatter.FormatNull("BYTEA") + " AS c6_string4, "
+                    "  " + formatter.FormatNull("INT") + " AS c7_int1, "
+                    "  " + formatter.FormatNull("INT") + " AS c8_int2, "
+                    "  " + formatter.FormatNull("INT") + " AS c9_int3, "
+                    "  COUNT(grandChildLevel.internalId) AS c10_big_int1, "
+                    "  " + formatter.FormatNull("BIGINT") + " AS c11_big_int2 "
                     "FROM Lookup "
                     "INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId "
                     "INNER JOIN Resources grandChildLevel ON childLevel.internalId = grandChildLevel.parentId "
@@ -4000,11 +4014,12 @@
              "  parentLevel.publicId AS c3_string1, "
              "  " + formatter.FormatNull("TEXT") + " AS c4_string2, "
              "  " + formatter.FormatNull("TEXT") + " AS c5_string3, "
-             "  " + formatter.FormatNull("INT") + " AS c6_int1, "
-             "  " + formatter.FormatNull("INT") + " AS c7_int2, "
-             "  " + formatter.FormatNull("INT") + " AS c8_int3, "
-             "  " + formatter.FormatNull("BIGINT") + " AS c9_big_int1, "
-             "  " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 "
+             "  " + formatter.FormatNull("BYTEA") + " AS c6_string4, "
+             "  " + formatter.FormatNull("INT") + " AS c7_int1, "
+             "  " + formatter.FormatNull("INT") + " AS c8_int2, "
+             "  " + formatter.FormatNull("INT") + " AS c9_int3, "
+             "  " + formatter.FormatNull("BIGINT") + " AS c10_big_int1, "
+             "  " + formatter.FormatNull("BIGINT") + " AS c11_big_int2 "
              "FROM Lookup "
              "  INNER JOIN Resources currentLevel ON currentLevel.internalId = Lookup.internalId "
              "  INNER JOIN Resources parentLevel ON currentLevel.parentId = parentLevel.internalId ";
@@ -4021,11 +4036,12 @@
              "    instancePublicId AS c3_string1, "
              "    " + formatter.FormatNull("TEXT") + " AS c4_string2, "
              "    " + formatter.FormatNull("TEXT") + " AS c5_string3, "
-             "    " + formatter.FormatNull("INT") + " AS c6_int1, "
-             "    " + formatter.FormatNull("INT") + " AS c7_int2, "
-             "  " + formatter.FormatNull("INT") + " AS c8_int3, "
-             "    instanceInternalId AS c9_big_int1, "
-             "    " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 "
+             "    " + formatter.FormatNull("BYTEA") + " AS c6_string4, "
+             "    " + formatter.FormatNull("INT") + " AS c7_int1, "
+             "    " + formatter.FormatNull("INT") + " AS c8_int2, "
+             "  " + formatter.FormatNull("INT") + " AS c9_int3, "
+             "    instanceInternalId AS c10_big_int1, "
+             "    " + formatter.FormatNull("BIGINT") + " AS c11_big_int2 "
              "   FROM OneInstance ";
 
       sql += "   UNION ALL SELECT"
@@ -4035,11 +4051,12 @@
              "    Metadata.value AS c3_string1, "
              "    " + formatter.FormatNull("TEXT") + " AS c4_string2, "
              "    " + formatter.FormatNull("TEXT") + " AS c5_string3, "
-             "    Metadata.type AS c6_int1, "
+             "    " + formatter.FormatNull("BYTEA") + " AS c6_string4, "
+             "    Metadata.type AS c7_int1, "
              + revisionInC7 +
-             "  " + formatter.FormatNull("INT") + " AS c8_int3, "
-             "    " + formatter.FormatNull("BIGINT") + " AS c9_big_int1, "
-             "    " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 "
+             "  " + formatter.FormatNull("INT") + " AS c9_int3, "
+             "    " + formatter.FormatNull("BIGINT") + " AS c10_big_int1, "
+             "    " + formatter.FormatNull("BIGINT") + " AS c11_big_int2 "
              "   FROM Metadata "
              "   INNER JOIN OneInstance ON Metadata.id = OneInstance.instanceInternalId";
              
@@ -4050,11 +4067,12 @@
              "    uuid AS c3_string1, "
              "    uncompressedHash AS c4_string2, "
              "    compressedHash AS c5_string3, "
-             "    fileType AS c6_int1, "
+             "    customData AS c6_string4, "
+             "    fileType AS c7_int1, "
              + revisionInC7 +
-             "    compressionType AS c8_int3, "
-             "    compressedSize AS c9_big_int1, "
-             "    uncompressedSize AS c10_big_int2 "
+             "    compressionType AS c9_int3, "
+             "    compressedSize AS c10_big_int1, "
+             "    uncompressedSize AS c11_big_int2 "
              "   FROM AttachedFiles "
              "   INNER JOIN OneInstance ON AttachedFiles.id = OneInstance.instanceInternalId";
 
@@ -4078,7 +4096,9 @@
     }
     
     statement->Execute(formatter.GetDictionary());
-    
+
+    statement->SetResultFieldType(C6_STRING_4, ValueType_BinaryString);
+
     // LOG(INFO) << sql;
 
     std::map<int64_t, Orthanc::DatabasePluginMessages::Find_Response*> responses;
@@ -4110,8 +4130,8 @@
           Orthanc::DatabasePluginMessages::Find_Response_Tag* tag = content->add_main_dicom_tags();
 
           tag->set_value(statement->ReadString(C3_STRING_1));
-          tag->set_group(statement->ReadInteger32(C6_INT_1));
-          tag->set_element(statement->ReadInteger32(C7_INT_2));
+          tag->set_group(statement->ReadInteger32(C7_INT_1));
+          tag->set_element(statement->ReadInteger32(C8_INT_2));
           }; break;
 
         case QUERY_PARENT_MAIN_DICOM_TAGS:
@@ -4120,8 +4140,8 @@
           Orthanc::DatabasePluginMessages::Find_Response_Tag* tag = content->add_main_dicom_tags();
 
           tag->set_value(statement->ReadString(C3_STRING_1));
-          tag->set_group(statement->ReadInteger32(C6_INT_1));
-          tag->set_element(statement->ReadInteger32(C7_INT_2));
+          tag->set_group(statement->ReadInteger32(C7_INT_1));
+          tag->set_element(statement->ReadInteger32(C8_INT_2));
         }; break;
 
         case QUERY_GRAND_PARENT_MAIN_DICOM_TAGS:
@@ -4130,8 +4150,8 @@
           Orthanc::DatabasePluginMessages::Find_Response_Tag* tag = content->add_main_dicom_tags();
 
           tag->set_value(statement->ReadString(C3_STRING_1));
-          tag->set_group(statement->ReadInteger32(C6_INT_1));
-          tag->set_element(statement->ReadInteger32(C7_INT_2));
+          tag->set_group(statement->ReadInteger32(C7_INT_1));
+          tag->set_element(statement->ReadInteger32(C8_INT_2));
         }; break;
 
         case QUERY_CHILDREN_IDENTIFIERS:
@@ -4144,7 +4164,7 @@
         case QUERY_CHILDREN_COUNT:
         {
           Orthanc::DatabasePluginMessages::Find_Response_ChildrenContent* content = GetChildrenContent(responses[internalId], static_cast<Orthanc::DatabasePluginMessages::ResourceType>(request.level() + 1));
-          content->set_count(statement->ReadInteger64(C9_BIG_INT_1));
+          content->set_count(statement->ReadInteger64(C10_BIG_INT_1));
         }; break;
 
         case QUERY_CHILDREN_MAIN_DICOM_TAGS:
@@ -4152,8 +4172,8 @@
           Orthanc::DatabasePluginMessages::Find_Response_ChildrenContent* content = GetChildrenContent(responses[internalId], static_cast<Orthanc::DatabasePluginMessages::ResourceType>(request.level() + 1));
           Orthanc::DatabasePluginMessages::Find_Response_Tag* tag = content->add_main_dicom_tags();
           tag->set_value(statement->ReadString(C3_STRING_1)); // TODO: handle sequences ??
-          tag->set_group(statement->ReadInteger32(C6_INT_1));
-          tag->set_element(statement->ReadInteger32(C7_INT_2));
+          tag->set_group(statement->ReadInteger32(C7_INT_1));
+          tag->set_element(statement->ReadInteger32(C8_INT_2));
         }; break;
 
         case QUERY_CHILDREN_METADATA:
@@ -4162,7 +4182,7 @@
           Orthanc::DatabasePluginMessages::Find_Response_Metadata* metadata = content->add_metadata();
 
           metadata->set_value(statement->ReadString(C3_STRING_1));
-          metadata->set_key(statement->ReadInteger32(C6_INT_1));
+          metadata->set_key(statement->ReadInteger32(C7_INT_1));
           metadata->set_revision(0);  // Setting a revision is not required in this case, as of Orthanc 1.12.5
         }; break;
 
@@ -4176,7 +4196,7 @@
         case QUERY_GRAND_CHILDREN_COUNT:
         {
           Orthanc::DatabasePluginMessages::Find_Response_ChildrenContent* content = GetChildrenContent(responses[internalId], static_cast<Orthanc::DatabasePluginMessages::ResourceType>(request.level() + 2));
-          content->set_count(statement->ReadInteger64(C9_BIG_INT_1));
+          content->set_count(statement->ReadInteger64(C10_BIG_INT_1));
         }; break;
 
         case QUERY_GRAND_CHILDREN_MAIN_DICOM_TAGS:
@@ -4185,8 +4205,8 @@
           Orthanc::DatabasePluginMessages::Find_Response_Tag* tag = content->add_main_dicom_tags();
 
           tag->set_value(statement->ReadString(C3_STRING_1)); // TODO: handle sequences ??
-          tag->set_group(statement->ReadInteger32(C6_INT_1));
-          tag->set_element(statement->ReadInteger32(C7_INT_2));
+          tag->set_group(statement->ReadInteger32(C7_INT_1));
+          tag->set_element(statement->ReadInteger32(C8_INT_2));
         }; break;
 
         case QUERY_GRAND_CHILDREN_METADATA:
@@ -4195,7 +4215,7 @@
           Orthanc::DatabasePluginMessages::Find_Response_Metadata* metadata = content->add_metadata();
 
           metadata->set_value(statement->ReadString(C3_STRING_1));
-          metadata->set_key(statement->ReadInteger32(C6_INT_1));
+          metadata->set_key(statement->ReadInteger32(C7_INT_1));
           metadata->set_revision(0);  // Setting a revision is not required in this case, as of Orthanc 1.12.5
         }; break;
 
@@ -4209,7 +4229,7 @@
         case QUERY_GRAND_GRAND_CHILDREN_COUNT:
         {
           Orthanc::DatabasePluginMessages::Find_Response_ChildrenContent* content = GetChildrenContent(responses[internalId], static_cast<Orthanc::DatabasePluginMessages::ResourceType>(request.level() + 3));
-          content->set_count(statement->ReadInteger64(C9_BIG_INT_1));
+          content->set_count(statement->ReadInteger64(C10_BIG_INT_1));
         }; break;
 
         case QUERY_ATTACHMENTS:
@@ -4219,14 +4239,19 @@
           attachment->set_uuid(statement->ReadString(C3_STRING_1));
           attachment->set_uncompressed_hash(statement->ReadString(C4_STRING_2));
           attachment->set_compressed_hash(statement->ReadString(C5_STRING_3));
-          attachment->set_content_type(statement->ReadInteger32(C6_INT_1));
-          attachment->set_compression_type(statement->ReadInteger32(C8_INT_3));
-          attachment->set_compressed_size(statement->ReadInteger64(C9_BIG_INT_1));
-          attachment->set_uncompressed_size(statement->ReadInteger64(C10_BIG_INT_2));
-
-          if (!statement->IsNull(C7_INT_2))  // revision can be null for files that have been atttached by older Orthanc versions
+
+          attachment->set_content_type(statement->ReadInteger32(C7_INT_1));
+          attachment->set_compression_type(statement->ReadInteger32(C9_INT_3));
+          attachment->set_compressed_size(statement->ReadInteger64(C10_BIG_INT_1));
+          attachment->set_uncompressed_size(statement->ReadInteger64(C11_BIG_INT_2));
+
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 8)
+          attachment->set_custom_data(statement->ReadStringOrNull(C6_STRING_4));
+#endif
+
+          if (!statement->IsNull(C8_INT_2))  // revision can be null for files that have been atttached by older Orthanc versions
           {
-            responses[internalId]->add_attachments_revisions(statement->ReadInteger32(C7_INT_2));
+            responses[internalId]->add_attachments_revisions(statement->ReadInteger32(C8_INT_2));
           }
           else
           {
@@ -4240,11 +4265,11 @@
           Orthanc::DatabasePluginMessages::Find_Response_Metadata* metadata = content->add_metadata();
 
           metadata->set_value(statement->ReadString(C3_STRING_1));
-          metadata->set_key(statement->ReadInteger32(C6_INT_1));
+          metadata->set_key(statement->ReadInteger32(C7_INT_1));
           
-          if (!statement->IsNull(C7_INT_2))  // revision can be null for metadata that have been created by older Orthanc versions
+          if (!statement->IsNull(C8_INT_2))  // revision can be null for metadata that have been created by older Orthanc versions
           {
-            metadata->set_revision(statement->ReadInteger32(C7_INT_2));
+            metadata->set_revision(statement->ReadInteger32(C8_INT_2));
           }
           else
           {
@@ -4258,11 +4283,11 @@
           Orthanc::DatabasePluginMessages::Find_Response_Metadata* metadata = content->add_metadata();
 
           metadata->set_value(statement->ReadString(C3_STRING_1));
-          metadata->set_key(statement->ReadInteger32(C6_INT_1));
-
-          if (!statement->IsNull(C7_INT_2))  // revision can be null for metadata that have been created by older Orthanc versions
+          metadata->set_key(statement->ReadInteger32(C7_INT_1));
+
+          if (!statement->IsNull(C8_INT_2))  // revision can be null for metadata that have been created by older Orthanc versions
           {
-            metadata->set_revision(statement->ReadInteger32(C7_INT_2));
+            metadata->set_revision(statement->ReadInteger32(C8_INT_2));
           }
           else
           {
@@ -4276,11 +4301,11 @@
           Orthanc::DatabasePluginMessages::Find_Response_Metadata* metadata = content->add_metadata();
 
           metadata->set_value(statement->ReadString(C3_STRING_1));
-          metadata->set_key(statement->ReadInteger32(C6_INT_1));
-
-          if (!statement->IsNull(C7_INT_2))  // revision can be null for metadata that have been created by older Orthanc versions
+          metadata->set_key(statement->ReadInteger32(C7_INT_1));
+
+          if (!statement->IsNull(C8_INT_2))  // revision can be null for metadata that have been created by older Orthanc versions
           {
-            metadata->set_revision(statement->ReadInteger32(C7_INT_2));
+            metadata->set_revision(statement->ReadInteger32(C8_INT_2));
           }
           else
           {
@@ -4302,11 +4327,11 @@
           Orthanc::DatabasePluginMessages::Find_Response_Metadata* metadata = responses[internalId]->add_one_instance_metadata();
 
           metadata->set_value(statement->ReadString(C3_STRING_1));
-          metadata->set_key(statement->ReadInteger32(C6_INT_1));
-
-          if (!statement->IsNull(C7_INT_2))  // revision can be null for metadata that have been created by older Orthanc versions
+          metadata->set_key(statement->ReadInteger32(C7_INT_1));
+
+          if (!statement->IsNull(C8_INT_2))  // revision can be null for metadata that have been created by older Orthanc versions
           {
-            metadata->set_revision(statement->ReadInteger32(C7_INT_2));
+            metadata->set_revision(statement->ReadInteger32(C8_INT_2));
           }
           else
           {
@@ -4320,10 +4345,14 @@
           attachment->set_uuid(statement->ReadString(C3_STRING_1));
           attachment->set_uncompressed_hash(statement->ReadString(C4_STRING_2));
           attachment->set_compressed_hash(statement->ReadString(C5_STRING_3));
-          attachment->set_content_type(statement->ReadInteger32(C6_INT_1));
-          attachment->set_compression_type(statement->ReadInteger32(C8_INT_3));
-          attachment->set_compressed_size(statement->ReadInteger64(C9_BIG_INT_1));
-          attachment->set_uncompressed_size(statement->ReadInteger64(C10_BIG_INT_2));
+          attachment->set_content_type(statement->ReadInteger32(C7_INT_1));
+          attachment->set_compression_type(statement->ReadInteger32(C9_INT_3));
+          attachment->set_compressed_size(statement->ReadInteger64(C10_BIG_INT_1));
+          attachment->set_uncompressed_size(statement->ReadInteger64(C11_BIG_INT_2));
+
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 8)
+          attachment->set_custom_data(statement->ReadStringOrNull(C6_STRING_4));
+#endif
         }; break;
 
         default:
@@ -4333,4 +4362,325 @@
     }    
   }
 #endif
+
+#if ORTHANC_PLUGINS_HAS_KEY_VALUE_STORES == 1
+    void IndexBackend::StoreKeyValue(DatabaseManager& manager,
+                                     const std::string& storeId,
+                                     const std::string& key,
+                                     const std::string& value)
+    {
+      // TODO: that probably needs to be adapted in ODBC and MySQL
+      DatabaseManager::CachedStatement statement(
+        STATEMENT_FROM_HERE, manager,
+        "INSERT INTO KeyValueStores VALUES(${storeId}, ${key}, ${value}) ON CONFLICT (storeId, key) DO UPDATE SET value = EXCLUDED.value;");
+
+      statement.SetParameterType("storeId", ValueType_Utf8String);
+      statement.SetParameterType("key", ValueType_Utf8String);
+      statement.SetParameterType("value", ValueType_BinaryString);
+
+      Dictionary args;
+      args.SetUtf8Value("storeId", storeId);
+      args.SetUtf8Value("key", key);
+      args.SetBinaryValue("value", value);
+
+      statement.Execute(args);
+    }
+
+    void IndexBackend::DeleteKeyValue(DatabaseManager& manager,
+                                      const std::string& storeId,
+                                      const std::string& key)
+    {
+      DatabaseManager::CachedStatement statement(
+        STATEMENT_FROM_HERE, manager,
+        "DELETE FROM KeyValueStores WHERE storeId = ${storeId} AND key = ${key}");
+
+      statement.SetParameterType("storeId", ValueType_Utf8String);
+      statement.SetParameterType("key", ValueType_Utf8String);
+
+      Dictionary args;
+      args.SetUtf8Value("storeId", storeId);
+      args.SetUtf8Value("key", key);
+
+      statement.Execute(args);
+    }
+
+    bool IndexBackend::GetKeyValue(std::string& value,
+                                   DatabaseManager& manager,
+                                   const std::string& storeId,
+                                   const std::string& key)
+    {
+      DatabaseManager::CachedStatement statement(
+        STATEMENT_FROM_HERE, manager,
+        "SELECT value FROM KeyValueStores WHERE storeId = ${storeId} AND key = ${key}");
+        
+      statement.SetReadOnly(true);
+      statement.SetParameterType("storeId", ValueType_Utf8String);
+      statement.SetParameterType("key", ValueType_Utf8String);
+
+      Dictionary args;
+      args.SetUtf8Value("storeId", storeId);
+      args.SetUtf8Value("key", key);
+
+      statement.Execute(args);
+      statement.SetResultFieldType(0, ValueType_BinaryString);
+
+      if (statement.IsDone())
+      {
+        return false;
+      }
+      else
+      {
+        value = statement.ReadString(0);
+        return true;
+      }
+    }                  
+
+    void IndexBackend::ListKeysValues(Orthanc::DatabasePluginMessages::TransactionResponse& response,
+                                      DatabaseManager& manager,
+                                      const Orthanc::DatabasePluginMessages::ListKeysValues_Request& request)
+    {
+      response.mutable_list_keys_values()->Clear();
+
+      LookupFormatter formatter(manager.GetDialect());
+
+      std::unique_ptr<DatabaseManager::CachedStatement> statement;
+      
+      std::string storeIdParameter = formatter.GenerateParameter(request.store_id());
+
+      if (request.from_first())
+      {
+        statement.reset(new DatabaseManager::CachedStatement(
+                        STATEMENT_FROM_HERE, manager,
+                        "SELECT key, value FROM KeyValueStores WHERE storeId= " + storeIdParameter + " ORDER BY key ASC " + formatter.FormatLimits(0, request.limit())));
+      }
+      else
+      {
+        std::string fromKeyParameter = formatter.GenerateParameter(request.from_key());
+        statement.reset(new DatabaseManager::CachedStatement(
+                        STATEMENT_FROM_HERE, manager,
+                        "SELECT key, value FROM KeyValueStores WHERE storeId= " + storeIdParameter + " AND key > " + fromKeyParameter + " ORDER BY key ASC " + formatter.FormatLimits(0, request.limit())));
+      }
+
+      statement->Execute(formatter.GetDictionary());
+
+      if (!statement->IsDone())
+      {
+        if (statement->GetResultFieldsCount() != 2)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+        }
+        
+        statement->SetResultFieldType(0, ValueType_Utf8String);
+        statement->SetResultFieldType(1, ValueType_BinaryString);
+
+        while (!statement->IsDone())
+        {
+          Orthanc::DatabasePluginMessages::ListKeysValues_Response_KeyValue* kv = response.mutable_list_keys_values()->add_keys_values();
+          kv->set_key(statement->ReadString(0));
+          kv->set_value(statement->ReadString(1));
+
+          statement->Next();
+        }
+      }
+
+    }
+#endif
+
+#if ORTHANC_PLUGINS_HAS_QUEUES == 1
+    void IndexBackend::EnqueueValue(DatabaseManager& manager,
+                                    const std::string& queueId,
+                                    const std::string& value)
+    {
+      DatabaseManager::CachedStatement statement(
+        STATEMENT_FROM_HERE, manager,
+        "INSERT INTO Queues VALUES(${AUTOINCREMENT} ${queueId}, ${value})");
+
+      statement.SetParameterType("queueId", ValueType_Utf8String);
+      statement.SetParameterType("value", ValueType_BinaryString);
+
+      Dictionary args;
+      args.SetUtf8Value("queueId", queueId);
+      args.SetBinaryValue("value", value);
+
+      statement.Execute(args);
+    }
+
+
+    bool IndexBackend::DequeueValueSQLite(std::string& value,
+                                          DatabaseManager& manager,
+                                          const std::string& queueId,
+                                          bool fromFront)
+    {
+      assert(manager.GetDialect() == Dialect_SQLite);
+
+      LookupFormatter formatter(manager.GetDialect());
+
+      std::unique_ptr<DatabaseManager::CachedStatement> statement;
+
+      std::string queueIdParameter = formatter.GenerateParameter(queueId);
+
+      if (fromFront)
+      {
+        statement.reset(new DatabaseManager::CachedStatement(
+                          STATEMENT_FROM_HERE, manager,
+                          "SELECT id, value FROM Queues WHERE queueId=" + queueIdParameter + " ORDER BY id ASC LIMIT 1"));
+      }
+      else
+      {
+        statement.reset(new DatabaseManager::CachedStatement(
+                          STATEMENT_FROM_HERE, manager,
+                          "SELECT id, value FROM Queues WHERE queueId=" + queueIdParameter + " ORDER BY id DESC LIMIT 1"));
+      }
+
+      statement->Execute(formatter.GetDictionary());
+
+      if (statement->IsDone())
+      {
+        return false;
+      }
+      else
+      {
+        statement->SetResultFieldType(0, ValueType_Integer64);
+        statement->SetResultFieldType(1, ValueType_BinaryString);
+
+        value = statement->ReadString(1);
+
+        {
+          DatabaseManager::CachedStatement s2(STATEMENT_FROM_HERE, manager,
+                                              "DELETE FROM Queues WHERE id=${id}");
+
+          s2.SetParameterType("id", ValueType_Integer64);
+
+          Dictionary args;
+          args.SetIntegerValue("id", statement->ReadInteger64(0));
+
+          s2.Execute(args);
+        }
+
+        return true;
+      }
+    }
+
+
+    bool IndexBackend::DequeueValue(std::string& value,
+                                    DatabaseManager& manager,
+                                    const std::string& queueId,
+                                    bool fromFront)
+    {
+      if (manager.GetDialect() == Dialect_SQLite)
+      {
+        return DequeueValueSQLite(value, manager, queueId, fromFront);
+      }
+      else
+      {
+        LookupFormatter formatter(manager.GetDialect());
+
+        std::unique_ptr<DatabaseManager::CachedStatement> statement;
+
+        std::string queueIdParameter = formatter.GenerateParameter(queueId);
+
+        switch (manager.GetDialect())
+        {
+          case Dialect_PostgreSQL:
+            if (fromFront)
+            {
+              statement.reset(new DatabaseManager::CachedStatement(
+                                STATEMENT_FROM_HERE, manager,
+                                "WITH poppedRows AS (DELETE FROM Queues WHERE id = (SELECT MIN(id) FROM Queues WHERE queueId=" + queueIdParameter + ") RETURNING value) "
+                                "SELECT value FROM poppedRows"));
+            }
+            else
+            {
+              statement.reset(new DatabaseManager::CachedStatement(
+                                STATEMENT_FROM_HERE, manager,
+                                "WITH poppedRows AS (DELETE FROM Queues WHERE id = (SELECT MAX(id) FROM Queues WHERE queueId=" + queueIdParameter + ") RETURNING value) "
+                                "SELECT value FROM poppedRows"));
+            }
+            break;
+
+          default:
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+        }
+
+        statement->Execute(formatter.GetDictionary());
+
+        if (statement->IsDone())
+        {
+          return false;
+        }
+        else
+        {
+          statement->SetResultFieldType(0, ValueType_BinaryString);
+          value = statement->ReadString(0);
+          return true;
+        }
+      }
+    }
+
+    uint64_t IndexBackend::GetQueueSize(DatabaseManager& manager,
+                                        const std::string& queueId)
+    {
+      DatabaseManager::CachedStatement statement(
+        STATEMENT_FROM_HERE, manager,
+        "SELECT COUNT(*) FROM Queues WHERE queueId = ${queueId}");
+        
+      statement.SetReadOnly(true);
+      statement.SetParameterType("queueId", ValueType_Utf8String);
+
+      Dictionary args;
+      args.SetUtf8Value("queueId", queueId);
+
+      statement.Execute(args);
+      statement.SetResultFieldType(0, ValueType_Integer64);
+
+      return statement.ReadInteger64(0);
+    }
+#endif
+
+#if ORTHANC_PLUGINS_HAS_ATTACHMENTS_CUSTOM_DATA == 1
+    void IndexBackend::GetAttachmentCustomData(std::string& customData,
+                                               DatabaseManager& manager,
+                                               const std::string& attachmentUuid)
+    {
+      DatabaseManager::CachedStatement statement(
+        STATEMENT_FROM_HERE, manager,
+        "SELECT customData FROM AttachedFiles WHERE uuid = ${uuid}");
+
+      statement.SetReadOnly(true);
+      statement.SetParameterType("uuid", ValueType_Utf8String);
+
+      Dictionary args;
+      args.SetUtf8Value("uuid", attachmentUuid);
+
+      statement.Execute(args);
+      statement.SetResultFieldType(8, ValueType_BinaryString);
+
+      if (statement.IsDone())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource, "Nonexistent attachment: " + attachmentUuid);
+      }
+      else
+      {
+        customData = statement.ReadStringOrNull(0);
+      }
+    }
+
+    void  IndexBackend::SetAttachmentCustomData(DatabaseManager& manager,
+                                                const std::string& attachmentUuid,
+                                                const std::string& customData)
+    {
+      DatabaseManager::CachedStatement statement(
+        STATEMENT_FROM_HERE, manager,
+        "UPDATE AttachedFiles SET customData = ${customData} WHERE uuid = ${uuid}");
+
+      statement.SetParameterType("uuid", ValueType_Utf8String);
+      statement.SetParameterType("customData", ValueType_Utf8String);
+
+      Dictionary args;
+      args.SetUtf8Value("uuid", attachmentUuid);
+      args.SetBinaryValue("customData", customData);
+
+      statement.Execute(args);
+    }
+#endif
 }
--- a/Framework/Plugins/IndexBackend.h	Mon May 19 11:18:43 2025 +0200
+++ b/Framework/Plugins/IndexBackend.h	Fri Jun 13 15:16:52 2025 +0200
@@ -84,6 +84,13 @@
                                        const Dictionary& args,
                                        uint32_t limit);
 
+#if ORTHANC_PLUGINS_HAS_QUEUES == 1
+    bool DequeueValueSQLite(std::string& value,
+                            DatabaseManager& manager,
+                            const std::string& queueId,
+                            bool fromFront);
+#endif
+
   public:
     explicit IndexBackend(OrthancPluginContext* context,
                           bool readOnly,
@@ -102,7 +109,16 @@
                                int64_t id,
                                const OrthancPluginAttachment& attachment,
                                int64_t revision) ORTHANC_OVERRIDE;
-    
+
+#if ORTHANC_PLUGINS_HAS_ATTACHMENTS_CUSTOM_DATA == 1
+    // New in Orthanc 1.12.8
+    virtual void AddAttachment(DatabaseManager& manager,
+                               int64_t id,
+                               const OrthancPluginAttachment& attachment,
+                               int64_t revision,
+                               const std::string& customData) ORTHANC_OVERRIDE;
+#endif
+
     virtual void AttachChild(DatabaseManager& manager,
                              int64_t parent,
                              int64_t child) ORTHANC_OVERRIDE;
@@ -459,6 +475,52 @@
                               const Orthanc::DatabasePluginMessages::Find_Request& request) ORTHANC_OVERRIDE;
 #endif
 
+#if ORTHANC_PLUGINS_HAS_KEY_VALUE_STORES == 1
+    virtual void StoreKeyValue(DatabaseManager& manager,
+                               const std::string& storeId,
+                               const std::string& key,
+                               const std::string& value) ORTHANC_OVERRIDE;
+
+    virtual void DeleteKeyValue(DatabaseManager& manager,
+                                const std::string& storeId,
+                                const std::string& key) ORTHANC_OVERRIDE;
+
+    virtual bool GetKeyValue(std::string& value,
+                             DatabaseManager& manager,
+                             const std::string& storeId,
+                             const std::string& key) ORTHANC_OVERRIDE;
+
+    virtual void ListKeysValues(Orthanc::DatabasePluginMessages::TransactionResponse& response,
+                                DatabaseManager& manager,
+                                const Orthanc::DatabasePluginMessages::ListKeysValues_Request& request) ORTHANC_OVERRIDE;
+#endif
+
+#if ORTHANC_PLUGINS_HAS_QUEUES == 1
+    virtual void EnqueueValue(DatabaseManager& manager,
+                              const std::string& queueId,
+                              const std::string& value) ORTHANC_OVERRIDE;
+
+    virtual bool DequeueValue(std::string& value,
+                              DatabaseManager& manager,
+                              const std::string& queueId,
+                              bool fromFront) ORTHANC_OVERRIDE;
+
+    virtual uint64_t GetQueueSize(DatabaseManager& manager,
+                                  const std::string& queueId) ORTHANC_OVERRIDE;
+
+#endif
+
+#if ORTHANC_PLUGINS_HAS_ATTACHMENTS_CUSTOM_DATA == 1
+    virtual void GetAttachmentCustomData(std::string& customData,
+                                         DatabaseManager& manager,
+                                         const std::string& attachmentUuid) ORTHANC_OVERRIDE;
+
+    virtual void SetAttachmentCustomData(DatabaseManager& manager,
+                                         const std::string& attachmentUuid,
+                                         const std::string& customData) ORTHANC_OVERRIDE;
+
+#endif
+
     virtual bool HasPerformDbHousekeeping() ORTHANC_OVERRIDE
     {
       return false;
--- a/Framework/Plugins/IndexUnitTests.h	Mon May 19 11:18:43 2025 +0200
+++ b/Framework/Plugins/IndexUnitTests.h	Fri Jun 13 15:16:52 2025 +0200
@@ -217,6 +217,102 @@
 }
 
 
+#if ORTHANC_PLUGINS_HAS_KEY_VALUE_STORES == 1
+static void ListKeys(std::set<std::string>& keys,
+                     OrthancDatabases::IndexBackend& db,
+                     OrthancDatabases::DatabaseManager& manager,
+                     const std::string& storeId)
+{
+  {
+    Orthanc::DatabasePluginMessages::ListKeysValues_Request request;
+    request.set_store_id(storeId);
+    request.set_from_first(true);
+    request.set_limit(0);
+
+    Orthanc::DatabasePluginMessages::TransactionResponse response;
+    db.ListKeysValues(response, manager, request);
+
+    keys.clear();
+
+    for (int i = 0; i < response.list_keys_values().keys_values_size(); i++)
+    {
+      const Orthanc::DatabasePluginMessages::ListKeysValues_Response_KeyValue& item = response.list_keys_values().keys_values(i);
+      keys.insert(item.key());
+
+      std::string value;
+      if (!db.GetKeyValue(value, manager, storeId, item.key()) ||
+          value != item.value())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+    }
+  }
+
+  {
+    std::set<std::string> keys2;
+
+    // Alternative implementation using an iterator
+    Orthanc::DatabasePluginMessages::ListKeysValues_Request request;
+    request.set_store_id(storeId);
+    request.set_from_first(true);
+    request.set_limit(1);
+
+    Orthanc::DatabasePluginMessages::TransactionResponse response;
+    db.ListKeysValues(response, manager, request);
+
+    while (response.list_keys_values().keys_values_size() > 0)
+    {
+      int count = response.list_keys_values().keys_values_size();
+
+      for (int i = 0; i < count; i++)
+      {
+        keys2.insert(response.list_keys_values().keys_values(i).key());
+      }
+
+      request.set_from_first(false);
+      request.set_from_key(response.list_keys_values().keys_values(count - 1).key());
+      db.ListKeysValues(response, manager, request);
+    }
+
+    if (keys.size() != keys2.size())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+    else
+    {
+      for (std::set<std::string>::const_iterator it = keys.begin(); it != keys.end(); ++it)
+      {
+        if (keys2.find(*it) == keys2.end())
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+        }
+      }
+    }
+  }
+}
+#endif
+
+
+static void FillBlob(std::string& blob)
+{
+  blob.clear();
+  blob.push_back(0);
+  blob.push_back(1);
+  blob.push_back(0);
+  blob.push_back(2);
+}
+
+
+static void CheckBlob(const std::string& s)
+{
+  ASSERT_EQ(4u, s.size());
+  ASSERT_EQ(0u, static_cast<uint8_t>(s[0]));
+  ASSERT_EQ(1u, static_cast<uint8_t>(s[1]));
+  ASSERT_EQ(0u, static_cast<uint8_t>(s[2]));
+  ASSERT_EQ(2u, static_cast<uint8_t>(s[3]));
+}
+
+
 TEST(IndexBackend, Basic)
 {
   using namespace OrthancDatabases;
@@ -250,6 +346,13 @@
   
   std::unique_ptr<IDatabaseBackendOutput> output(db.CreateOutput());
 
+  {
+    // Sanity check
+    std::string blob;
+    FillBlob(blob);
+    CheckBlob(blob);
+  }
+
   std::string s;
   ASSERT_TRUE(db.LookupGlobalProperty(s, *manager, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabaseSchemaVersion));
   ASSERT_EQ("6", s);
@@ -422,7 +525,12 @@
   a2.compressedSize = 4242;
   a2.compressedHash = "md5_2";
     
+#if ORTHANC_PLUGINS_HAS_ATTACHMENTS_CUSTOM_DATA == 1
+  db.AddAttachment(*manager, a, a1, 42, "my_custom_data");
+#else
   db.AddAttachment(*manager, a, a1, 42);
+#endif
+
   db.ListAvailableAttachments(fc, *manager, a);
   ASSERT_EQ(1u, fc.size());
   ASSERT_EQ(Orthanc::FileContentType_Dicom, fc.front());
@@ -431,6 +539,32 @@
   ASSERT_EQ(2u, fc.size());
   ASSERT_FALSE(db.LookupAttachment(*output, revision, *manager, b, Orthanc::FileContentType_Dicom));
 
+#if ORTHANC_PLUGINS_HAS_ATTACHMENTS_CUSTOM_DATA == 1
+  {
+    std::string s;
+    ASSERT_THROW(db.GetAttachmentCustomData(s, *manager, "nope"), Orthanc::OrthancException);
+
+    db.GetAttachmentCustomData(s, *manager, "uuid1");
+    ASSERT_EQ("my_custom_data", s);
+
+    db.GetAttachmentCustomData(s, *manager, "uuid2");
+    ASSERT_TRUE(s.empty());
+
+    {
+      std::string blob;
+      FillBlob(blob);
+      db.SetAttachmentCustomData(*manager, "uuid1", blob);
+    }
+
+    db.GetAttachmentCustomData(s, *manager, "uuid1");
+    CheckBlob(s);
+
+    db.SetAttachmentCustomData(*manager, "uuid1", "");
+    db.GetAttachmentCustomData(s, *manager, "uuid1");
+    ASSERT_TRUE(s.empty());
+  }
+#endif
+
   ASSERT_EQ(4284u, db.GetTotalCompressedSize(*manager));
   ASSERT_EQ(4284u, db.GetTotalUncompressedSize(*manager));
 
@@ -803,5 +937,125 @@
   }
 #endif
 
+
+#if ORTHANC_PLUGINS_HAS_KEY_VALUE_STORES == 1
+  {
+    manager->StartTransaction(TransactionType_ReadWrite);
+
+    std::set<std::string> keys;
+    ListKeys(keys, db, *manager, "test");
+    ASSERT_EQ(0u, keys.size());
+
+    std::string s;
+    ASSERT_FALSE(db.GetKeyValue(s, *manager, "test", "hello"));
+    db.DeleteKeyValue(*manager, s, "test");
+
+    db.StoreKeyValue(*manager, "test", "hello", "world");
+    db.StoreKeyValue(*manager, "another", "hello", "world");
+    ListKeys(keys, db, *manager, "test");
+    ASSERT_EQ(1u, keys.size());
+    ASSERT_EQ("hello", *keys.begin());
+    ASSERT_TRUE(db.GetKeyValue(s, *manager, "test", "hello"));  ASSERT_EQ("world", s);
+
+    db.StoreKeyValue(*manager, "test", "hello", "overwritten");
+    ListKeys(keys, db, *manager, "test");
+    ASSERT_EQ(1u, keys.size());
+    ASSERT_EQ("hello", *keys.begin());
+    ASSERT_TRUE(db.GetKeyValue(s, *manager, "test", "hello"));  ASSERT_EQ("overwritten", s);
+
+    db.StoreKeyValue(*manager, "test", "hello2", "world2");
+    db.StoreKeyValue(*manager, "test", "hello3", "world3");
+
+    ListKeys(keys, db, *manager, "test");
+    ASSERT_EQ(3u, keys.size());
+    ASSERT_TRUE(keys.find("hello") != keys.end());
+    ASSERT_TRUE(keys.find("hello2") != keys.end());
+    ASSERT_TRUE(keys.find("hello3") != keys.end());
+    ASSERT_TRUE(db.GetKeyValue(s, *manager, "test", "hello"));   ASSERT_EQ("overwritten", s);
+    ASSERT_TRUE(db.GetKeyValue(s, *manager, "test", "hello2"));  ASSERT_EQ("world2", s);
+    ASSERT_TRUE(db.GetKeyValue(s, *manager, "test", "hello3"));  ASSERT_EQ("world3", s);
+
+    db.DeleteKeyValue(*manager, "test", "hello2");
+
+    ListKeys(keys, db, *manager, "test");
+    ASSERT_EQ(2u, keys.size());
+    ASSERT_TRUE(keys.find("hello") != keys.end());
+    ASSERT_TRUE(keys.find("hello3") != keys.end());
+    ASSERT_TRUE(db.GetKeyValue(s, *manager, "test", "hello"));   ASSERT_EQ("overwritten", s);
+    ASSERT_FALSE(db.GetKeyValue(s, *manager, "test", "hello2"));
+    ASSERT_TRUE(db.GetKeyValue(s, *manager, "test", "hello3"));  ASSERT_EQ("world3", s);
+
+    db.DeleteKeyValue(*manager, "test", "nope");
+    db.DeleteKeyValue(*manager, "test", "hello");
+    db.DeleteKeyValue(*manager, "test", "hello3");
+
+    ListKeys(keys, db, *manager, "test");
+    ASSERT_EQ(0u, keys.size());
+
+    {
+      std::string blob;
+      FillBlob(blob);
+      db.StoreKeyValue(*manager, "test", "blob", blob); // Storing binary values
+    }
+
+    ASSERT_TRUE(db.GetKeyValue(s, *manager, "test", "blob"));
+    CheckBlob(s);
+    db.DeleteKeyValue(*manager, "test", "blob");
+    ASSERT_FALSE(db.GetKeyValue(s, *manager, "test", "blob"));
+
+    manager->CommitTransaction();
+  }
+#endif
+
+
+#if ORTHANC_PLUGINS_HAS_QUEUES == 1
+  {
+    manager->StartTransaction(TransactionType_ReadWrite);
+
+    ASSERT_EQ(0u, db.GetQueueSize(*manager, "test"));
+    db.EnqueueValue(*manager, "test", "a");
+    db.EnqueueValue(*manager, "another", "hello");
+    ASSERT_EQ(1u, db.GetQueueSize(*manager, "test"));
+    db.EnqueueValue(*manager, "test", "b");
+    ASSERT_EQ(2u, db.GetQueueSize(*manager, "test"));
+    db.EnqueueValue(*manager, "test", "c");
+    ASSERT_EQ(3u, db.GetQueueSize(*manager, "test"));
+
+    std::string s;
+    ASSERT_FALSE(db.DequeueValue(s, *manager, "nope", false));
+    ASSERT_TRUE(db.DequeueValue(s, *manager, "test", true));  ASSERT_EQ("a", s);
+    ASSERT_EQ(2u, db.GetQueueSize(*manager, "test"));
+    ASSERT_TRUE(db.DequeueValue(s, *manager, "test", true));  ASSERT_EQ("b", s);
+    ASSERT_EQ(1u, db.GetQueueSize(*manager, "test"));
+    ASSERT_TRUE(db.DequeueValue(s, *manager, "test", true));  ASSERT_EQ("c", s);
+    ASSERT_EQ(0u, db.GetQueueSize(*manager, "test"));
+    ASSERT_FALSE(db.DequeueValue(s, *manager, "test", true));
+
+    db.EnqueueValue(*manager, "test", "a");
+    db.EnqueueValue(*manager, "test", "b");
+    db.EnqueueValue(*manager, "test", "c");
+
+    ASSERT_TRUE(db.DequeueValue(s, *manager, "test", false));  ASSERT_EQ("c", s);
+    ASSERT_TRUE(db.DequeueValue(s, *manager, "test", false));  ASSERT_EQ("b", s);
+    ASSERT_TRUE(db.DequeueValue(s, *manager, "test", false));  ASSERT_EQ("a", s);
+    ASSERT_FALSE(db.DequeueValue(s, *manager, "test", false));
+
+    {
+      std::string blob;
+      FillBlob(blob);
+      db.EnqueueValue(*manager, "test", blob); // Storing binary values
+    }
+
+    ASSERT_TRUE(db.DequeueValue(s, *manager, "test", true));
+    CheckBlob(s);
+
+    ASSERT_FALSE(db.DequeueValue(s, *manager, "test", true));
+
+    ASSERT_EQ(1u, db.GetQueueSize(*manager, "another"));
+
+    manager->CommitTransaction();
+  }
+#endif
+
   manager->Close();
 }
--- a/Framework/Plugins/MessagesToolbox.h	Mon May 19 11:18:43 2025 +0200
+++ b/Framework/Plugins/MessagesToolbox.h	Fri Jun 13 15:16:52 2025 +0200
@@ -40,24 +40,31 @@
 
 
 #define ORTHANC_PLUGINS_HAS_INTEGRATED_FIND 0
+#define ORTHANC_PLUGINS_HAS_CHANGES_EXTENDED 0
 
 #if defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE)
 #  if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 5)
 #    undef  ORTHANC_PLUGINS_HAS_INTEGRATED_FIND
 #    define ORTHANC_PLUGINS_HAS_INTEGRATED_FIND 1
-#  endif
-#endif
-
-
-#define ORTHANC_PLUGINS_HAS_CHANGES_EXTENDED 0
-
-#if defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE)
-#  if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 5)
 #    undef  ORTHANC_PLUGINS_HAS_CHANGES_EXTENDED
 #    define ORTHANC_PLUGINS_HAS_CHANGES_EXTENDED 1
 #  endif
 #endif
 
+#define ORTHANC_PLUGINS_HAS_ATTACHMENTS_CUSTOM_DATA 0
+#define ORTHANC_PLUGINS_HAS_KEY_VALUE_STORES 0
+#define ORTHANC_PLUGINS_HAS_QUEUES 0
+
+#if defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE)
+#  if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 8)
+#    undef  ORTHANC_PLUGINS_HAS_ATTACHMENTS_CUSTOM_DATA
+#    define ORTHANC_PLUGINS_HAS_ATTACHMENTS_CUSTOM_DATA 1
+#    undef  ORTHANC_PLUGINS_HAS_KEY_VALUE_STORES
+#    define ORTHANC_PLUGINS_HAS_KEY_VALUE_STORES 1
+#    undef  ORTHANC_PLUGINS_HAS_QUEUES
+#    define ORTHANC_PLUGINS_HAS_QUEUES 1
+#  endif
+#endif
 
 #include <Enumerations.h>
 
--- a/Framework/PostgreSQL/PostgreSQLParameters.cpp	Mon May 19 11:18:43 2025 +0200
+++ b/Framework/PostgreSQL/PostgreSQLParameters.cpp	Fri Jun 13 15:16:52 2025 +0200
@@ -44,6 +44,7 @@
     maxConnectionRetries_ = 10;
     connectionRetryInterval_ = 5;
     isVerboseEnabled_ = false;
+    allowInconsistentChildCounts_ = false;
     isolationMode_ = IsolationMode_Serializable;
   }
 
--- a/Framework/PostgreSQL/PostgreSQLResult.cpp	Mon May 19 11:18:43 2025 +0200
+++ b/Framework/PostgreSQL/PostgreSQLResult.cpp	Fri Jun 13 15:16:52 2025 +0200
@@ -168,7 +168,7 @@
     CheckColumn(column, 0);
 
     Oid oid = PQftype(reinterpret_cast<PGresult*>(result_), column);
-    if (oid != TEXTOID && oid != VARCHAROID && oid != BYTEAOID)
+    if (oid != TEXTOID && oid != VARCHAROID)
     {
       throw Orthanc::OrthancException(Orthanc::ErrorCode_BadParameterType);
     }
@@ -177,6 +177,22 @@
   }
 
 
+  void PostgreSQLResult::GetBinaryString(std::string& target,
+                                         unsigned int column) const
+  {
+    CheckColumn(column, 0);
+
+    Oid oid = PQftype(reinterpret_cast<PGresult*>(result_), column);
+    if (oid != BYTEAOID)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadParameterType);
+    }
+
+    target.assign(PQgetvalue(reinterpret_cast<PGresult*>(result_), position_, column),
+                  PQgetlength(reinterpret_cast<PGresult*>(result_), position_, column));
+  }
+
+
   std::string PostgreSQLResult::GetLargeObjectOid(unsigned int column) const
   {
     CheckColumn(column, OIDOID);
@@ -256,7 +272,14 @@
         return new Utf8StringValue(GetString(column));
 
       case BYTEAOID:
-        return new BinaryStringValue(GetString(column));
+      {
+        std::string s;
+        GetBinaryString(s, column);
+
+        std::unique_ptr<BinaryStringValue> value(new BinaryStringValue);
+        value->Swap(s);
+        return value.release();
+      }
 
       case OIDOID:
         return new LargeObjectResult(database_, GetLargeObjectOid(column));
--- a/Framework/PostgreSQL/PostgreSQLResult.h	Mon May 19 11:18:43 2025 +0200
+++ b/Framework/PostgreSQL/PostgreSQLResult.h	Fri Jun 13 15:16:52 2025 +0200
@@ -74,6 +74,9 @@
 
     std::string GetString(unsigned int column) const;
 
+    void GetBinaryString(std::string& target,
+                         unsigned int column) const;
+
     std::string GetLargeObjectOid(unsigned int column) const;
 
     void GetLargeObjectContent(std::string& content,
--- a/Framework/PostgreSQL/PostgreSQLStatement.cpp	Mon May 19 11:18:43 2025 +0200
+++ b/Framework/PostgreSQL/PostgreSQLStatement.cpp	Fri Jun 13 15:16:52 2025 +0200
@@ -223,7 +223,7 @@
     }
 
     oids_[param] = type;
-    binary_[param] = (type == TEXTOID || type == BYTEAOID || type == OIDOID) ? 0 : 1;
+    binary_[param] = (type == TEXTOID || type == OIDOID) ? 0 : 1;
   }
 
 
@@ -283,16 +283,23 @@
 
     if (PQtransactionStatus(reinterpret_cast<PGconn*>(database_.pg_)) == PQTRANS_INERROR)
     {
+      std::string message;
+
       if (result != NULL)
       {
-        PQclear(result);
+        message.assign(PQresultErrorMessage(result));
+        PQclear(result);  // Frees the memory allocated by "PQresultErrorMessage()"
       }
-      
+
+      if (message.empty())
+      {
+        message = "Collision between multiple writers";
+      }
+
 #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 9, 2)
-      std::string errorString(PQresultErrorMessage(result));
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_DatabaseCannotSerialize, errorString, false); // don't log here, it is handled at higher level
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_DatabaseCannotSerialize, message, false); // don't log here, it is handled #else
 #else
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_Database, "Collision between multiple writers");
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_Database, message);
 #endif
     }
     else if (result == NULL)
@@ -454,19 +461,21 @@
       throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
     }
 
-    if (oids_[param] != TEXTOID && oids_[param] != BYTEAOID)
-    {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadParameterType);
-    }
-
-    if (value.size() == 0)
+    switch (oids_[param])
     {
-      inputs_->SetItem(param, "", 1 /* end-of-string character */);
-    }
-    else
-    {
-      inputs_->SetItem(param, value.c_str(), 
-                       value.size() + 1);  // "+1" for end-of-string character
+      case TEXTOID:
+      {
+        std::string s = value + '\0';  // Make sure that there is an end-of-string character
+        inputs_->SetItem(param, s.c_str(), s.size());
+        break;
+      }
+
+      case BYTEAOID:
+        inputs_->SetItem(param, value.c_str(), value.size());
+        break;
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadParameterType);
     }
   }
 
@@ -528,42 +537,44 @@
     {
       const std::string& name = formatter_.GetParameterName(i);
       
-      switch (formatter_.GetParameterType(i))
-      {
-        case ValueType_Integer64:
-          BindInteger64(i, dynamic_cast<const Integer64Value&>(parameters.GetValue(name)).GetValue());
-          break;
+      const IValue& value = parameters.GetValue(name);
 
-        case ValueType_Integer32:
-          BindInteger(i, dynamic_cast<const Integer32Value&>(parameters.GetValue(name)).GetValue());
-          break;
+      if (value.GetType() == ValueType_Null || value.IsNull())
+      {
+        BindNull(i);
+      }
+      else
+      {
+        switch (formatter_.GetParameterType(i))
+        {
+          case ValueType_Integer64:
+            BindInteger64(i, dynamic_cast<const Integer64Value&>(value).GetValue());
+            break;
 
-        case ValueType_Null:
-          BindNull(i);
-          break;
+          case ValueType_Integer32:
+            BindInteger(i, dynamic_cast<const Integer32Value&>(value).GetValue());
+            break;
 
-        case ValueType_Utf8String:
-          BindString(i, dynamic_cast<const Utf8StringValue&>
-                     (parameters.GetValue(name)).GetContent());
-          break;
+          case ValueType_Utf8String:
+            BindString(i, dynamic_cast<const Utf8StringValue&>(value).GetContent());
+            break;
 
-        case ValueType_BinaryString:
-          BindString(i, dynamic_cast<const BinaryStringValue&>
-                     (parameters.GetValue(name)).GetContent());
-          break;
+          case ValueType_BinaryString:
+            BindString(i, dynamic_cast<const BinaryStringValue&>(value).GetContent());
+            break;
 
-        case ValueType_InputFile:
-        {
-          const InputFileValue& blob =
-            dynamic_cast<const InputFileValue&>(parameters.GetValue(name));
+          case ValueType_InputFile:
+          {
+            const InputFileValue& blob = dynamic_cast<const InputFileValue&>(value);
 
-          PostgreSQLLargeObject largeObject(database_, blob.GetContent());
-          BindLargeObject(i, largeObject);
-          break;
+            PostgreSQLLargeObject largeObject(database_, blob.GetContent());
+            BindLargeObject(i, largeObject);
+            break;
+          }
+
+          default:
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
         }
-
-        default:
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
       }
     }
 
--- a/MySQL/CMakeLists.txt	Mon May 19 11:18:43 2025 +0200
+++ b/MySQL/CMakeLists.txt	Fri Jun 13 15:16:52 2025 +0200
@@ -19,7 +19,7 @@
 # along with this program. If not, see <http://www.gnu.org/licenses/>.
 
 
-cmake_minimum_required(VERSION 2.8)
+cmake_minimum_required(VERSION 2.8...4.0)
 project(OrthancMySQL)
 
 set(ORTHANC_PLUGIN_VERSION "mainline")
@@ -91,6 +91,7 @@
   MYSQL_PREPARE_INDEX          ${CMAKE_SOURCE_DIR}/Plugins/PrepareIndex.sql
   MYSQL_GET_LAST_CHANGE_INDEX  ${CMAKE_SOURCE_DIR}/Plugins/GetLastChangeIndex.sql
   MYSQL_CREATE_INSTANCE        ${CMAKE_SOURCE_DIR}/Plugins/CreateInstance.sql
+  MYSQL_INSTALL_REVISION_AND_CUSTOM_DATA  ${CMAKE_SOURCE_DIR}/Plugins/InstallRevisionAndCustomData.sql
   MYSQL_DELETE_RESOURCES       ${CMAKE_SOURCE_DIR}/Plugins/DeleteResources.sql
   )
 
--- a/MySQL/NEWS	Mon May 19 11:18:43 2025 +0200
+++ b/MySQL/NEWS	Fri Jun 13 15:16:52 2025 +0200
@@ -1,6 +1,8 @@
 Pending changes in the mainline
 ===============================
 
+* Added support for customData in AttachedFiles
+* Added support for revision in AttachedFiles & Metadata
 * Added support for ExtendedChanges:
   - changes?type=...&to=...
 * Added support for ExtendedFind
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MySQL/Plugins/InstallRevisionAndCustomData.sql	Fri Jun 13 15:16:52 2025 +0200
@@ -0,0 +1,28 @@
+ALTER TABLE AttachedFiles ADD COLUMN revision INTEGER;
+ALTER TABLE DeletedFiles ADD COLUMN revision INTEGER;
+ALTER TABLE Metadata ADD COLUMN revision INTEGER;
+
+ALTER TABLE AttachedFiles ADD COLUMN customData LONGTEXT;
+ALTER TABLE DeletedFiles ADD COLUMN customData LONGTEXT;
+
+DROP TRIGGER AttachedFileDeleted;
+
+CREATE TRIGGER AttachedFileDeleted
+AFTER DELETE ON AttachedFiles
+FOR EACH ROW
+  BEGIN
+    INSERT INTO DeletedFiles VALUES(old.uuid, old.filetype, old.compressedSize,
+                                    old.uncompressedSize, old.compressionType,
+                                    old.uncompressedHash, old.compressedHash,
+                                    old.revision, old.customData)@
+  END;
+
+
+DROP TRIGGER ResourceDeleted;
+
+CREATE TRIGGER ResourceDeleted
+BEFORE DELETE ON Resources
+FOR EACH ROW
+  BEGIN
+    INSERT INTO DeletedFiles SELECT uuid, fileType, compressedSize, uncompressedSize, compressionType, uncompressedHash, compressedHash, revision, customData FROM AttachedFiles WHERE id=old.internalId@
+  END;
\ No newline at end of file
--- a/MySQL/Plugins/MySQLIndex.cpp	Mon May 19 11:18:43 2025 +0200
+++ b/MySQL/Plugins/MySQLIndex.cpp	Fri Jun 13 15:16:52 2025 +0200
@@ -338,8 +338,26 @@
         t.Commit();
       }
 
+      if (revision == 8)
+      {
+        DatabaseManager::Transaction t(manager, TransactionType_ReadWrite);
+        
+        // Install revision and customData extension
+        std::string query;
+        
+        Orthanc::EmbeddedResources::GetFileResource
+          (query, Orthanc::EmbeddedResources::MYSQL_INSTALL_REVISION_AND_CUSTOM_DATA);
 
-      if (revision != 8)
+        // Need to escape arobases: Don't use "t.GetDatabaseTransaction().ExecuteMultiLines()" here
+        db.ExecuteMultiLines(query, true);
+        
+        revision = 9;
+        SetGlobalIntegerProperty(manager, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabasePatchLevel, revision);
+
+        t.Commit();
+      }
+
+      if (revision != 9)
       {
         LOG(ERROR) << "MySQL plugin is incompatible with database schema revision: " << revision;
         throw Orthanc::OrthancException(Orthanc::ErrorCode_Database);        
--- a/MySQL/Plugins/MySQLIndex.h	Mon May 19 11:18:43 2025 +0200
+++ b/MySQL/Plugins/MySQLIndex.h	Fri Jun 13 15:16:52 2025 +0200
@@ -58,9 +58,24 @@
  
     virtual bool HasRevisionsSupport() const ORTHANC_OVERRIDE
     {
-      return false;  // TODO - REVISIONS
+      return true;
+    }
+
+    virtual bool HasAttachmentCustomDataSupport() const ORTHANC_OVERRIDE
+    {
+      return true;
     }
     
+    virtual bool HasKeyValueStores() const ORTHANC_OVERRIDE
+    {
+      return false;
+    }
+
+    virtual bool HasQueues() const ORTHANC_OVERRIDE
+    {
+      return false;
+    }
+
     virtual int64_t CreateResource(DatabaseManager& manager,
                                    const char* publicId,
                                    OrthancPluginResourceType type)
--- a/MySQL/Plugins/PrepareIndex.sql	Mon May 19 11:18:43 2025 +0200
+++ b/MySQL/Plugins/PrepareIndex.sql	Fri Jun 13 15:16:52 2025 +0200
@@ -48,6 +48,8 @@
        compressionType INTEGER,
        uncompressedHash VARCHAR(40),
        compressedHash VARCHAR(40),
+       -- revision INTEGER,          -- new in v 4.X, added in MySQLIndex::ConfigureDatabase
+       -- customData LONGTEXT,       -- new in v 4.X, added in MySQLIndex::ConfigureDatabase
        PRIMARY KEY(id, fileType),
        CONSTRAINT AttachedFiles1 FOREIGN KEY (id) REFERENCES Resources(internalId) ON DELETE CASCADE
        );              
@@ -104,6 +106,8 @@
        compressionType INTEGER,        -- 4
        uncompressedHash VARCHAR(40),   -- 5
        compressedHash VARCHAR(40)      -- 6
+       -- revision INTEGER,          -- new in v 4.X, added in MySQLIndex::ConfigureDatabase
+       -- customData LONGTEXT,       -- new in v 4.X, added in MySQLIndex::ConfigureDatabase
        );
 -- End of differences
 
@@ -119,6 +123,8 @@
   INSERT INTO DeletedFiles VALUES(old.uuid, old.filetype, old.compressedSize,
                                   old.uncompressedSize, old.compressionType,
                                   old.uncompressedHash, old.compressedHash)@
+                                  -- old.revision, old.customData    -- new in v 4.X, added in MySQLIndex::ConfigureDatabase
+
 END;
 
 
@@ -127,6 +133,7 @@
 FOR EACH ROW
 BEGIN
    INSERT INTO DeletedFiles SELECT uuid, fileType, compressedSize, uncompressedSize, compressionType, uncompressedHash, compressedHash FROM AttachedFiles WHERE id=old.internalId@
+                                  -- revision, customData    -- new in v 4.X, added in MySQLIndex::ConfigureDatabase
 END;
 
 
--- a/Odbc/CMakeLists.txt	Mon May 19 11:18:43 2025 +0200
+++ b/Odbc/CMakeLists.txt	Fri Jun 13 15:16:52 2025 +0200
@@ -19,7 +19,7 @@
 # along with this program. If not, see <http://www.gnu.org/licenses/>.
 
 
-cmake_minimum_required(VERSION 2.8)
+cmake_minimum_required(VERSION 2.8...4.0)
 project(OrthancOdbc)
 
 set(ORTHANC_PLUGIN_VERSION "mainline")
--- a/Odbc/NEWS	Mon May 19 11:18:43 2025 +0200
+++ b/Odbc/NEWS	Fri Jun 13 15:16:52 2025 +0200
@@ -21,7 +21,6 @@
 * Now detecting communication link failure with the DB and retrying to connect.
 * Fixed "MaximumConnectionRetries" configuration that was not taken into account.
 
-
 Release 1.1 (2021-12-06)
 ========================
 
--- a/Odbc/Plugins/OdbcIndex.cpp	Mon May 19 11:18:43 2025 +0200
+++ b/Odbc/Plugins/OdbcIndex.cpp	Fri Jun 13 15:16:52 2025 +0200
@@ -163,6 +163,49 @@
     return OdbcDatabase::CreateDatabaseFactory(maxConnectionRetries_, connectionRetryInterval_, connectionString_, true);
   }
   
+  static void AdaptTypesToDialect(std::string& sql, Dialect dialect)
+  {
+    switch (dialect)
+    {
+      case Dialect_SQLite:
+        boost::replace_all(sql, "${LONGTEXT}", "TEXT");
+        boost::replace_all(sql, "${AUTOINCREMENT_TYPE}", "INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT");
+        boost::replace_all(sql, "${AUTOINCREMENT_INSERT}", "NULL, ");
+        break;
+      
+      case Dialect_PostgreSQL:
+        boost::replace_all(sql, "${LONGTEXT}", "TEXT");
+        boost::replace_all(sql, "${AUTOINCREMENT_TYPE}", "BIGSERIAL NOT NULL PRIMARY KEY");
+        boost::replace_all(sql, "${AUTOINCREMENT_INSERT}", "DEFAULT, ");
+        break;
+      
+      case Dialect_MySQL:
+        boost::replace_all(sql, "${LONGTEXT}", "LONGTEXT");
+        boost::replace_all(sql, "${AUTOINCREMENT_TYPE}", "BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY");
+        boost::replace_all(sql, "${AUTOINCREMENT_INSERT}", "NULL, ");
+        break;
+
+      case Dialect_MSSQL:
+        /**
+         * cf. OMSSQL-5: Use VARCHAR(MAX) instead of TEXT: (1)
+         * Microsoft issued a warning stating that "ntext, text, and
+         * image data types will be removed in a future version of
+         * SQL Server"
+         * (https://msdn.microsoft.com/en-us/library/ms187993.aspx),
+         * and (2) SQL Server does not support comparison of TEXT
+         * with '=' operator (e.g. in WHERE statements such as
+         * IndexBackend::LookupIdentifier())."
+         **/
+        boost::replace_all(sql, "${LONGTEXT}", "VARCHAR(MAX)");
+        boost::replace_all(sql, "${AUTOINCREMENT_TYPE}", "BIGINT IDENTITY NOT NULL PRIMARY KEY");
+        boost::replace_all(sql, "${AUTOINCREMENT_INSERT}", "");
+        break;
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+    }
+  }
+
   
   void OdbcIndex::ConfigureDatabase(DatabaseManager& manager,
                                     bool hasIdentifierTags,
@@ -190,46 +233,8 @@
     {
       std::string sql;
       Orthanc::EmbeddedResources::GetFileResource(sql, Orthanc::EmbeddedResources::ODBC_PREPARE_INDEX);
-
-      switch (db.GetDialect())
-      {
-        case Dialect_SQLite:
-          boost::replace_all(sql, "${LONGTEXT}", "TEXT");
-          boost::replace_all(sql, "${AUTOINCREMENT_TYPE}", "INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT");
-          boost::replace_all(sql, "${AUTOINCREMENT_INSERT}", "NULL, ");
-          break;
-        
-        case Dialect_PostgreSQL:
-          boost::replace_all(sql, "${LONGTEXT}", "TEXT");
-          boost::replace_all(sql, "${AUTOINCREMENT_TYPE}", "BIGSERIAL NOT NULL PRIMARY KEY");
-          boost::replace_all(sql, "${AUTOINCREMENT_INSERT}", "DEFAULT, ");
-          break;
-        
-        case Dialect_MySQL:
-          boost::replace_all(sql, "${LONGTEXT}", "LONGTEXT");
-          boost::replace_all(sql, "${AUTOINCREMENT_TYPE}", "BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY");
-          boost::replace_all(sql, "${AUTOINCREMENT_INSERT}", "NULL, ");
-          break;
-
-        case Dialect_MSSQL:
-          /**
-           * cf. OMSSQL-5: Use VARCHAR(MAX) instead of TEXT: (1)
-           * Microsoft issued a warning stating that "ntext, text, and
-           * image data types will be removed in a future version of
-           * SQL Server"
-           * (https://msdn.microsoft.com/en-us/library/ms187993.aspx),
-           * and (2) SQL Server does not support comparison of TEXT
-           * with '=' operator (e.g. in WHERE statements such as
-           * IndexBackend::LookupIdentifier())."
-           **/
-          boost::replace_all(sql, "${LONGTEXT}", "VARCHAR(MAX)");
-          boost::replace_all(sql, "${AUTOINCREMENT_TYPE}", "BIGINT IDENTITY NOT NULL PRIMARY KEY");
-          boost::replace_all(sql, "${AUTOINCREMENT_INSERT}", "");
-          break;
-
-        default:
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
-      }
+      
+      AdaptTypesToDialect(sql, db.GetDialect());
 
       {
         DatabaseManager::Transaction t(manager, TransactionType_ReadWrite);
@@ -243,6 +248,22 @@
           db.ExecuteMultiLines("ALTER DATABASE CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
         }
 
+        { // v 4.X: add customData
+          int patchLevel;
+      
+          if (!LookupGlobalIntegerProperty(patchLevel, manager, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabasePatchLevel))
+          {
+            std::string sqlAddCustomData = "ALTER TABLE AttachedFiles ADD customData ${LONGTEXT};"
+                                           "ALTER TABLE DeletedFiles ADD customData ${LONGTEXT}";
+
+            AdaptTypesToDialect(sqlAddCustomData, db.GetDialect());
+
+            db.ExecuteMultiLines(sqlAddCustomData);
+            
+            SetGlobalIntegerProperty(manager, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabasePatchLevel, 1);
+          }
+        }
+
         t.Commit();
       }
     }
--- a/Odbc/Plugins/OdbcIndex.h	Mon May 19 11:18:43 2025 +0200
+++ b/Odbc/Plugins/OdbcIndex.h	Fri Jun 13 15:16:52 2025 +0200
@@ -73,6 +73,21 @@
       return true;
     }
 
+    virtual bool HasAttachmentCustomDataSupport() const ORTHANC_OVERRIDE
+    {
+      return true;
+    }
+
+    virtual bool HasKeyValueStores() const ORTHANC_OVERRIDE
+    {
+      return false;
+    }
+
+    virtual bool HasQueues() const ORTHANC_OVERRIDE
+    {
+      return false;
+    }
+
     virtual int64_t CreateResource(DatabaseManager& manager,
                                    const char* publicId,
                                    OrthancPluginResourceType type) ORTHANC_OVERRIDE;
--- a/PostgreSQL/CMakeLists.txt	Mon May 19 11:18:43 2025 +0200
+++ b/PostgreSQL/CMakeLists.txt	Fri Jun 13 15:16:52 2025 +0200
@@ -19,7 +19,7 @@
 # along with this program. If not, see <http://www.gnu.org/licenses/>.
 
 
-cmake_minimum_required(VERSION 2.8)
+cmake_minimum_required(VERSION 2.8...4.0)
 project(OrthancPostgreSQL)
 
 set(ORTHANC_PLUGIN_VERSION "mainline")
@@ -94,6 +94,7 @@
   POSTGRESQL_UPGRADE_REV1_TO_REV2    ${CMAKE_SOURCE_DIR}/Plugins/SQL/Upgrades/Rev1ToRev2.sql
   POSTGRESQL_UPGRADE_REV2_TO_REV3    ${CMAKE_SOURCE_DIR}/Plugins/SQL/Upgrades/Rev2ToRev3.sql
   POSTGRESQL_UPGRADE_REV3_TO_REV4    ${CMAKE_SOURCE_DIR}/Plugins/SQL/Upgrades/Rev3ToRev4.sql
+  POSTGRESQL_UPGRADE_REV4_TO_REV5    ${CMAKE_SOURCE_DIR}/Plugins/SQL/Upgrades/Rev4ToRev5.sql
   )
 
 
--- a/PostgreSQL/NEWS	Mon May 19 11:18:43 2025 +0200
+++ b/PostgreSQL/NEWS	Fri Jun 13 15:16:52 2025 +0200
@@ -1,6 +1,20 @@
 Pending changes in the mainline
 ===============================
 
+DB schema revision: 4
+
+Minimum plugin SDK (for build): 1.12.6
+Optimal plugin SDK: 1.12.6
+
+Minimum Orthanc runtime: 1.12.6
+Optimal Orthanc runtime: 1.12.6
+
+Minimal Postgresql Server version: 9
+Optimal Postgresql Server version: 11+
+
+
+* Added support for customData in AttachedFiles
+
 
 
 Release 7.2 (2025-02-27)
--- a/PostgreSQL/Plugins/PostgreSQLIndex.cpp	Mon May 19 11:18:43 2025 +0200
+++ b/PostgreSQL/Plugins/PostgreSQLIndex.cpp	Fri Jun 13 15:16:52 2025 +0200
@@ -49,7 +49,7 @@
   static const GlobalProperty GlobalProperty_HasComputeStatisticsReadOnly = GlobalProperty_DatabaseInternal4;
 }
 
-#define CURRENT_DB_REVISION 4
+#define CURRENT_DB_REVISION 5
 
 namespace OrthancDatabases
 {
@@ -235,6 +235,19 @@
             currentRevision = 4;
           }
 
+          if (currentRevision == 4)
+          {
+            LOG(WARNING) << "Upgrading DB schema from revision 4 to revision 5";
+
+            std::string query;
+
+            Orthanc::EmbeddedResources::GetFileResource
+              (query, Orthanc::EmbeddedResources::POSTGRESQL_UPGRADE_REV4_TO_REV5);
+            t.GetDatabaseTransaction().ExecuteMultiLines(query);
+            hasAppliedAnUpgrade = true;
+            currentRevision = 5;
+          }
+
           if (hasAppliedAnUpgrade)
           {
             LOG(WARNING) << "Upgrading DB schema by applying PrepareIndex.sql";
--- a/PostgreSQL/Plugins/PostgreSQLIndex.h	Mon May 19 11:18:43 2025 +0200
+++ b/PostgreSQL/Plugins/PostgreSQLIndex.h	Fri Jun 13 15:16:52 2025 +0200
@@ -72,6 +72,21 @@
       return true;
     }
     
+    virtual bool HasAttachmentCustomDataSupport() const ORTHANC_OVERRIDE
+    {
+      return true;
+    }
+
+    virtual bool HasKeyValueStores() const ORTHANC_OVERRIDE
+    {
+      return true;
+    }
+
+    virtual bool HasQueues() const ORTHANC_OVERRIDE
+    {
+      return true;
+    }
+
     virtual int64_t CreateResource(DatabaseManager& manager,
                                    const char* publicId,
                                    OrthancPluginResourceType type) ORTHANC_OVERRIDE;
--- a/PostgreSQL/Plugins/SQL/Downgrades/Rev3ToRev2.sql	Mon May 19 11:18:43 2025 +0200
+++ b/PostgreSQL/Plugins/SQL/Downgrades/Rev3ToRev2.sql	Fri Jun 13 15:16:52 2025 +0200
@@ -1,4 +1,4 @@
--- This file contains an SQL procedure to downgrade from schema Rev3 to Rev2 (version = 6, revision = 1).
+-- This file contains an SQL procedure to downgrade from schema Rev3 to Rev2 (version = 6).
   -- It actually deletes the ChildCount table and triggers
   -- It actually does not uninstall ChildrenIndex2 because it is anyway more efficient than 
      -- ChildrenIndex and is not incompatible with previous revisions.
--- a/PostgreSQL/Plugins/SQL/Downgrades/Rev4ToRev3.sql	Mon May 19 11:18:43 2025 +0200
+++ b/PostgreSQL/Plugins/SQL/Downgrades/Rev4ToRev3.sql	Fri Jun 13 15:16:52 2025 +0200
@@ -1,5 +1,5 @@
 -- This file contains an SQL procedure to downgrade from schema Rev4 to Rev3 (version = 6).
-  -- It re-installs the old childcount trigger mechanisms
+-- It re-installs the old childcount trigger mechanisms
 
 DO $$
 DECLARE
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/PostgreSQL/Plugins/SQL/Downgrades/Rev5ToRev4.sql	Fri Jun 13 15:16:52 2025 +0200
@@ -0,0 +1,62 @@
+-- This file contains an SQL procedure to downgrade from schema Rev5 to Rev4 (version = 6).
+-- It removes the column that has been added in Rev5
+
+-- these constraints were introduced in Rev5
+ALTER TABLE AttachedFiles DROP COLUMN customData;
+
+-- reinstall previous triggers
+CREATE OR REPLACE FUNCTION CreateDeletedFilesTemporaryTable(
+) RETURNS VOID AS $body$
+
+BEGIN
+
+    SET client_min_messages = warning;   -- suppress NOTICE:  relation "deletedresources" already exists, skipping
+    
+    -- note: temporary tables are created at session (connection) level -> they are likely to exist
+    CREATE TEMPORARY TABLE IF NOT EXISTS DeletedFiles(
+        uuid VARCHAR(64) NOT NULL,
+        fileType INTEGER,
+        compressedSize BIGINT,
+        uncompressedSize BIGINT,
+        compressionType INTEGER,
+        uncompressedHash VARCHAR(40),
+        compressedHash VARCHAR(40)
+        );
+
+    RESET client_min_messages;
+
+    -- clear the temporary table in case it has been created earlier in the session
+    DELETE FROM DeletedFiles;
+END;
+
+$body$ LANGUAGE plpgsql;
+
+
+CREATE OR REPLACE FUNCTION AttachedFileDeletedFunc() 
+RETURNS TRIGGER AS $body$
+BEGIN
+  INSERT INTO DeletedFiles VALUES
+    (old.uuid, old.filetype, old.compressedSize,
+     old.uncompressedSize, old.compressionType,
+     old.uncompressedHash, old.compressedHash);
+  RETURN NULL;
+END;
+$body$ LANGUAGE plpgsql;
+
+DROP TRIGGER IF EXISTS AttachedFileDeleted on AttachedFiles;
+CREATE TRIGGER AttachedFileDeleted
+AFTER DELETE ON AttachedFiles
+FOR EACH ROW
+EXECUTE PROCEDURE AttachedFileDeletedFunc();
+
+
+DROP TABLE IF EXISTS KeyValueStores;
+
+DROP TABLE IF EXISTS Queues;
+
+DROP INDEX IF EXISTS QueuesIndex;
+
+
+DELETE FROM GlobalProperties WHERE property IN (4);
+INSERT INTO GlobalProperties VALUES (4, 4); -- GlobalProperty_DatabasePatchLevel
+
--- a/PostgreSQL/Plugins/SQL/PrepareIndex.sql	Mon May 19 11:18:43 2025 +0200
+++ b/PostgreSQL/Plugins/SQL/PrepareIndex.sql	Fri Jun 13 15:16:52 2025 +0200
@@ -1,4 +1,4 @@
--- This SQL file creates a DB in Rev2 directly
+-- This SQL file creates a DB in Rev5 directly
 -- It is also run after upgrade scripts to create new tables and or create/replace triggers and functions.
 -- This script is self contained, it contains everything that needs to be run to create an Orthanc DB.
 -- Note to developers: 
@@ -53,6 +53,7 @@
        uncompressedHash VARCHAR(40),
        compressedHash VARCHAR(40),
        revision INTEGER,
+       customData BYTEA,           -- new in schema rev 5
        PRIMARY KEY(id, fileType)
        );              
 
@@ -305,7 +306,9 @@
         uncompressedSize BIGINT,
         compressionType INTEGER,
         uncompressedHash VARCHAR(40),
-        compressedHash VARCHAR(40)
+        compressedHash VARCHAR(40),
+        revision INTEGER,
+        customData BYTEA
         );
 
     RESET client_min_messages;
@@ -323,7 +326,8 @@
   INSERT INTO DeletedFiles VALUES
     (old.uuid, old.filetype, old.compressedSize,
      old.uncompressedSize, old.compressionType,
-     old.uncompressedHash, old.compressedHash);
+     old.uncompressedHash, old.compressedHash,
+     old.revision, old.customData);
   RETURN NULL;
 END;
 $body$ LANGUAGE plpgsql;
@@ -717,11 +721,28 @@
 EXECUTE PROCEDURE UpdateChildCount();
 
 
+-- new in 1.12.8 (rev 5 ?)
+
+CREATE TABLE KeyValueStores(
+       storeId TEXT NOT NULL,
+       key TEXT NOT NULL,
+       value BYTEA NOT NULL,
+       PRIMARY KEY(storeId, key)  -- Prevents duplicates
+       );
+
+CREATE TABLE Queues (
+       id BIGSERIAL NOT NULL PRIMARY KEY,
+       queueId TEXT NOT NULL,
+       value BYTEA NOT NULL
+);
+
+CREATE INDEX QueuesIndex ON Queues (queueId, id);
+
 
 -- set the global properties that actually documents the DB version, revision and some of the capabilities
 DELETE FROM GlobalProperties WHERE property IN (1, 4, 6, 10, 11, 12, 13, 14);
 INSERT INTO GlobalProperties VALUES (1, 6); -- GlobalProperty_DatabaseSchemaVersion
-INSERT INTO GlobalProperties VALUES (4, 4); -- GlobalProperty_DatabasePatchLevel
+INSERT INTO GlobalProperties VALUES (4, 5); -- GlobalProperty_DatabasePatchLevel
 INSERT INTO GlobalProperties VALUES (6, 1); -- GlobalProperty_GetTotalSizeIsFast
 INSERT INTO GlobalProperties VALUES (10, 1); -- GlobalProperty_HasTrigramIndex
 INSERT INTO GlobalProperties VALUES (11, 3); -- GlobalProperty_HasCreateInstance  -- this is actually the 3rd version of HasCreateInstance
--- a/PostgreSQL/Plugins/SQL/Upgrades/Rev1ToRev2.sql	Mon May 19 11:18:43 2025 +0200
+++ b/PostgreSQL/Plugins/SQL/Upgrades/Rev1ToRev2.sql	Fri Jun 13 15:16:52 2025 +0200
@@ -1,4 +1,4 @@
--- This file contains part of the changes required to upgrade from Revision 1 to Revision 2 (DB version 6 and revision 1)
+-- This file contains part of the changes required to upgrade from Revision 1 to Revision 2 (DB version 6)
 -- It actually contains only the changes that:
    -- can not be executed with an idempotent statement in SQL
    -- or would polute the PrepareIndex.sql
--- a/PostgreSQL/Plugins/SQL/Upgrades/Rev2ToRev3.sql	Mon May 19 11:18:43 2025 +0200
+++ b/PostgreSQL/Plugins/SQL/Upgrades/Rev2ToRev3.sql	Fri Jun 13 15:16:52 2025 +0200
@@ -41,4 +41,3 @@
 
 -- other changes performed in PrepareIndex.sql:
   -- add ChildCount triggers
-
--- a/PostgreSQL/Plugins/SQL/Upgrades/Rev3ToRev4.sql	Mon May 19 11:18:43 2025 +0200
+++ b/PostgreSQL/Plugins/SQL/Upgrades/Rev3ToRev4.sql	Fri Jun 13 15:16:52 2025 +0200
@@ -1,2 +1,2 @@
 -- everything is performed in PrepareIndex.sql
-SELECT 1;
\ No newline at end of file
+SELECT 1;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/PostgreSQL/Plugins/SQL/Upgrades/Rev4ToRev5.sql	Fri Jun 13 15:16:52 2025 +0200
@@ -0,0 +1,19 @@
+-- This file contains part of the changes required to upgrade from Revision 4 to Revision 5 (DB version 6)
+-- It actually contains only the changes that:
+   -- can not be executed with an idempotent statement in SQL
+   -- or would polute the PrepareIndex.sql
+-- This file is executed only if the current schema is in revision 4 and it is executed 
+-- before PrepareIndex.sql that is idempotent.
+
+
+
+DO $body$
+BEGIN
+
+    BEGIN
+        ALTER TABLE AttachedFiles ADD COLUMN customData BYTEA;
+    EXCEPTION
+        WHEN duplicate_column THEN RAISE NOTICE 'column customData already exists in AttachedFiles.';
+    END;
+
+END $body$ LANGUAGE plpgsql;
--- a/README	Mon May 19 11:18:43 2025 +0200
+++ b/README	Fri Jun 13 15:16:52 2025 +0200
@@ -57,6 +57,69 @@
 https://orthanc.uclouvain.be/book/developers/repositories.html
 
 
+Development
+-----------
+
+PostgreSQL
+==========
+
+To quickly start a test PG server:
+
+  docker run -p 5432:5432 --rm --env POSTGRES_HOST_AUTH_METHOD=trust postgres:13.4
+
+And use this Orthanc configuration:
+  "PostgreSQL": {
+    "EnableIndex": true,
+    "EnableStorage": false, // DICOM files are stored in the Orthanc container in /var/lib/orthanc/db/
+    "Host": "localhost", // the name of the PostgreSQL container
+    "Database": "postgres", // default database name in PostgreSQL container (no need to create it)
+    "Username": "postgres", // default user name in PostgreSQL container (no need to create it)
+    "Password": "postgres"
+  },
+
+MySQL
+=====
+
+To quickly start a test MySQL server:
+
+  docker run -p 3306:3306 --rm --env MYSQL_PASSWORD=orthanc --env MYSQL_USER=orthanc --env MYSQL_DATABASE=orthanc --env MYSQL_ROOT_PASSWORD=pwd-root mysql:8.0 mysqld --default-authentication-plugin=mysql_native_password --log-bin-trust-function-creators=1
+
+And use this Orthanc configuration:
+  "MySQL": {
+    "EnableIndex": true,
+    "EnableStorage": false,
+    "Host": "localhost",
+    "Database": "orthanc",
+    "Username": "orthanc",
+    "Password": "orthanc",
+    "UnixSocket": ""
+  },
+
+
+ODBC (SQL Server)
+=================
+
+To quickly start a test MySQL server:
+
+  docker run -e "ACCEPT_EULA=Y" --rm --env "SA_PASSWORD=yourStrong-Password" --entrypoint=bash -it -p 1433:1433 mcr.microsoft.com/mssql/server:2019-latest
+
+Then:  
+   (sleep 15s && /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P yourStrong-Password -Q 'CREATE DATABASE orthanctest') & /opt/mssql/bin/sqlservr
+
+And use this Orthanc configuration:
+  "Odbc" : {
+    "IndexConnectionString": "Driver={ODBC Driver 17 for SQL Server};Server=tcp:localhost,1433;Database=orthanctest;Uid=sa;Pwd=yourStrong-Password;Encrypt=yes;TrustServerCertificate=yes;Connection Timeout=30;",
+    "EnableIndex": true,
+    "EnableStorage": false
+  }
+
+
+SQLite
+======
+
+To quickly test the SQLite plugin, simply run orthanc and load the plugin (no configuration required).
+  
+
 Licensing
 ---------
 
--- a/Resources/Orthanc/CMake/Compiler.cmake	Mon May 19 11:18:43 2025 +0200
+++ b/Resources/Orthanc/CMake/Compiler.cmake	Fri Jun 13 15:16:52 2025 +0200
@@ -22,6 +22,16 @@
 
 # This file sets all the compiler-related flags
 
+if (${CMAKE_SYSTEM_NAME} STREQUAL "Darwin")
+  # Since Orthanc 1.12.7 that allows CMake 4.0, builds for macOS
+  # require the C++ standard to be explicitly set to C++11. Do *not*
+  # do this on GNU/Linux, as third-party system libraries could have
+  # been compiled with higher versions of the C++ standard.
+  set(CMAKE_CXX_STANDARD 11)
+  set(CMAKE_CXX_STANDARD_REQUIRED ON)
+  set(CMAKE_CXX_EXTENSIONS OFF)
+endif()
+
 
 # Save the current compiler flags to the cache every time cmake configures the project
 set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS}" CACHE STRING "compiler flags" FORCE)
@@ -239,7 +249,9 @@
   add_definitions(
     -D_XOPEN_SOURCE=1
     )
-  link_libraries(iconv)
+  
+  # Linking with iconv breaks the Universal builds on modern compilers
+  # link_libraries(iconv)
 
 elseif (CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
   message("Building using Emscripten (for WebAssembly or asm.js targets)")
--- a/Resources/Orthanc/CMake/DownloadOrthancFramework.cmake	Mon May 19 11:18:43 2025 +0200
+++ b/Resources/Orthanc/CMake/DownloadOrthancFramework.cmake	Fri Jun 13 15:16:52 2025 +0200
@@ -167,6 +167,10 @@
         set(ORTHANC_FRAMEWORK_MD5 "1e61779ea4a7cd705720bdcfed8a6a73")
       elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.5")
         set(ORTHANC_FRAMEWORK_MD5 "5bb69f092981fdcfc11dec0a0f9a7db3")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.6")
+        set(ORTHANC_FRAMEWORK_MD5 "0e971f32f4f3e4951e0f3b5de49a3da6")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.7")
+        set(ORTHANC_FRAMEWORK_MD5 "f27c27d7a7a694dab1fd7f0a99d9715a")
 
       # Below this point are development snapshots that were used to
       # release some plugin, before an official release of the Orthanc
@@ -499,7 +503,15 @@
   
   include(CheckIncludeFile)
   include(CheckIncludeFileCXX)
-  include(FindPythonInterp)
+  
+  if(CMAKE_VERSION VERSION_GREATER "3.11")
+    find_package(Python REQUIRED COMPONENTS Interpreter)
+    set(PYTHON_EXECUTABLE ${Python_EXECUTABLE})
+  else()
+    include(FindPythonInterp)
+    find_package(PythonInterp REQUIRED)
+  endif()
+  
   include(${CMAKE_CURRENT_LIST_DIR}/Compiler.cmake)
   include(${CMAKE_CURRENT_LIST_DIR}/DownloadPackage.cmake)
   include(${CMAKE_CURRENT_LIST_DIR}/AutoGeneratedCode.cmake)
--- a/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp	Mon May 19 11:18:43 2025 +0200
+++ b/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp	Fri Jun 13 15:16:52 2025 +0200
@@ -26,6 +26,7 @@
 #include <boost/algorithm/string/predicate.hpp>
 #include <boost/move/unique_ptr.hpp>
 #include <boost/thread.hpp>
+#include <boost/algorithm/string/join.hpp>
 
 
 #include <json/reader.h>
@@ -2051,6 +2052,26 @@
             DoPost(target, index, uri, body, headers));
   }
 
+  bool OrthancPeers::DoPost(Json::Value& target,
+                            size_t index,
+                            const std::string& uri,
+                            const std::string& body,
+                            const HttpHeaders& headers, 
+                            unsigned int timeout) const
+  {
+    MemoryBuffer buffer;
+
+    if (DoPost(buffer, index, uri, body, headers, timeout))
+    {
+      buffer.ToJson(target);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
 
   bool OrthancPeers::DoPost(Json::Value& target,
                             size_t index,
@@ -2098,6 +2119,17 @@
                             const std::string& body,
                             const HttpHeaders& headers) const
   {
+    return DoPost(target, index, uri, body, headers, timeout_);
+  }
+
+
+  bool OrthancPeers::DoPost(MemoryBuffer& target,
+                            size_t index,
+                            const std::string& uri,
+                            const std::string& body,
+                            const HttpHeaders& headers,
+                            unsigned int timeout) const
+  {
     if (index >= index_.size())
     {
       ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange);
@@ -2116,7 +2148,7 @@
     OrthancPluginErrorCode code = OrthancPluginCallPeerApi
       (GetGlobalContext(), *answer, NULL, &status, peers_,
        static_cast<uint32_t>(index), OrthancPluginHttpMethod_Post, uri.c_str(),
-       pluginHeaders.GetSize(), pluginHeaders.GetKeys(), pluginHeaders.GetValues(), body.empty() ? NULL : body.c_str(), body.size(), timeout_);
+       pluginHeaders.GetSize(), pluginHeaders.GetKeys(), pluginHeaders.GetValues(), body.empty() ? NULL : body.c_str(), body.size(), timeout);
 
     if (code == OrthancPluginErrorCode_Success)
     {
@@ -4077,6 +4109,26 @@
     }    
   }
 
+  void SerializeGetArguments(std::string& output, const OrthancPluginHttpRequest* request)
+  {
+    output.clear();
+    std::vector<std::string> arguments;
+    for (uint32_t i = 0; i < request->getCount; ++i)
+    {
+      if (request->getValues[i] && strlen(request->getValues[i]) > 0)
+      {
+        arguments.push_back(std::string(request->getKeys[i]) + "=" + std::string(request->getValues[i]));
+      }
+      else
+      {
+        arguments.push_back(std::string(request->getKeys[i]));
+      }
+    }
+
+    output = boost::algorithm::join(arguments, "&");
+  }
+
+
 #if !ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 4)
   static void SetPluginProperty(const std::string& pluginIdentifier,
                                 _OrthancPluginProperty property,
@@ -4130,6 +4182,24 @@
     httpStatus_(0)
   {
   }
+
+  RestApiClient::RestApiClient(const char* url,
+                               const OrthancPluginHttpRequest* request) :
+    method_(request->method),
+    path_(url),
+    afterPlugins_(false),
+    httpStatus_(0)
+  {
+    OrthancPlugins::GetHttpHeaders(requestHeaders_, request);
+
+    std::string getArguments;
+    OrthancPlugins::SerializeGetArguments(getArguments, request);
+
+    if (!getArguments.empty())
+    {
+      path_ += "?" + getArguments;
+    }
+  }
 #endif
 
 
@@ -4195,6 +4265,32 @@
       }
     }
   }
+
+  void RestApiClient::Forward(OrthancPluginContext* context, OrthancPluginRestOutput* output)
+  {
+    if (Execute() && httpStatus_ == 200)
+    {
+      const char* mimeType = NULL;
+      for (HttpHeaders::const_iterator h = answerHeaders_.begin(); h != answerHeaders_.end(); ++h)
+      {
+        if (h->first == "content-type")
+        {
+          mimeType = h->second.c_str();
+        }
+      }
+      
+      AnswerString(answerBody_, mimeType, output);
+    }
+    else
+    {
+      AnswerHttpError(httpStatus_, output);
+    }
+  }
+
+  bool RestApiClient::GetAnswerJson(Json::Value& output) const
+  {
+    return ReadJson(output, answerBody_);
+  }
 #endif
 
 
--- a/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h	Mon May 19 11:18:43 2025 +0200
+++ b/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h	Fri Jun 13 15:16:52 2025 +0200
@@ -855,6 +855,13 @@
                 const HttpHeaders& headers) const;
 
     bool DoPost(MemoryBuffer& target,
+                size_t index,
+                const std::string& uri,
+                const std::string& body,
+                const HttpHeaders& headers,
+                unsigned int timeout) const;
+
+    bool DoPost(MemoryBuffer& target,
                 const std::string& name,
                 const std::string& uri,
                 const std::string& body,
@@ -867,6 +874,13 @@
                 const HttpHeaders& headers) const;
 
     bool DoPost(Json::Value& target,
+                size_t index,
+                const std::string& uri,
+                const std::string& body,
+                const HttpHeaders& headers,
+                unsigned int timeout) const;
+
+    bool DoPost(Json::Value& target,
                 const std::string& name,
                 const std::string& uri,
                 const std::string& body,
@@ -1399,6 +1413,9 @@
 // helper method to convert Http headers from the plugin SDK to a std::map
 void GetHttpHeaders(HttpHeaders& result, const OrthancPluginHttpRequest* request);
 
+// helper method to re-serialize the get arguments from the SDK into a string
+void SerializeGetArguments(std::string& output, const OrthancPluginHttpRequest* request);
+
 #if HAS_ORTHANC_PLUGIN_WEBDAV == 1
   class IWebDavCollection : public boost::noncopyable
   {
@@ -1528,6 +1545,10 @@
 
   public:
     RestApiClient();
+    
+    // used to forward a call from the plugin to the core
+    RestApiClient(const char* url,
+                  const OrthancPluginHttpRequest* request);
 
     void SetMethod(OrthancPluginHttpMethod method)
     {
@@ -1584,12 +1605,17 @@
 
     bool Execute();
 
+    // Execute and forward the response as is
+    void Forward(OrthancPluginContext* context, OrthancPluginRestOutput* output);
+
     uint16_t GetHttpStatus() const;
 
     bool LookupAnswerHeader(std::string& value,
                             const std::string& key) const;
 
     const std::string& GetAnswerBody() const;
+
+    bool GetAnswerJson(Json::Value& output) const;
   };
 #endif
 }
--- a/SQLite/CMakeLists.txt	Mon May 19 11:18:43 2025 +0200
+++ b/SQLite/CMakeLists.txt	Fri Jun 13 15:16:52 2025 +0200
@@ -19,7 +19,7 @@
 # along with this program. If not, see <http://www.gnu.org/licenses/>.
 
 
-cmake_minimum_required(VERSION 2.8)
+cmake_minimum_required(VERSION 2.8...4.0)
 project(OrthancSQLite)
 
 set(ORTHANC_PLUGIN_VERSION "mainline")
@@ -55,6 +55,7 @@
 
 EmbedResources(
   SQLITE_PREPARE_INDEX ${CMAKE_SOURCE_DIR}/Plugins/PrepareIndex.sql
+  SQLITE_INSTALL_CUSTOM_DATA ${CMAKE_SOURCE_DIR}/Plugins/InstallCustomData.sql
   )
 
 if (EXISTS ${ORTHANC_SDK_ROOT}/orthanc/OrthancDatabasePlugin.proto)
--- a/SQLite/NEWS	Mon May 19 11:18:43 2025 +0200
+++ b/SQLite/NEWS	Fri Jun 13 15:16:52 2025 +0200
@@ -1,3 +1,9 @@
+Pending changes in the mainline
+===============================
+
+* Added support for customData in AttachedFiles
+
+
 Pending changes in the mainline
 ===============================
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/SQLite/Plugins/InstallCustomData.sql	Fri Jun 13 15:16:52 2025 +0200
@@ -0,0 +1,17 @@
+
+-- Add new column for customData
+ALTER TABLE AttachedFiles ADD COLUMN customData TEXT;
+ALTER TABLE DeletedFiles ADD COLUMN revision INTEGER;
+ALTER TABLE DeletedFiles ADD COLUMN customData TEXT;
+
+
+DROP TRIGGER AttachedFileDeleted;
+
+CREATE TRIGGER AttachedFileDeleted
+AFTER DELETE ON AttachedFiles
+BEGIN
+   INSERT INTO DeletedFiles VALUES(old.uuid, old.filetype, old.compressedSize,
+                                   old.uncompressedSize, old.compressionType,
+                                   old.uncompressedHash, old.compressedHash,
+                                   old.revision, old.customData);
+END;
--- a/SQLite/Plugins/PrepareIndex.sql	Mon May 19 11:18:43 2025 +0200
+++ b/SQLite/Plugins/PrepareIndex.sql	Fri Jun 13 15:16:52 2025 +0200
@@ -156,3 +156,20 @@
 BEGIN
   INSERT INTO PatientRecyclingOrder VALUES (NULL, new.internalId);
 END;
+
+
+-- New in Orthanc 1.12.8
+CREATE TABLE KeyValueStores(
+       storeId TEXT NOT NULL,
+       key TEXT NOT NULL,
+       value BLOB NOT NULL,
+       PRIMARY KEY(storeId, key)  -- Prevents duplicates
+       );
+
+CREATE TABLE Queues (
+       id INTEGER PRIMARY KEY AUTOINCREMENT,
+       queueId TEXT NOT NULL,
+       value BLOB NOT NULL
+);
+
+CREATE INDEX QueuesIndex ON Queues (queueId, id);
--- a/SQLite/Plugins/SQLiteIndex.cpp	Mon May 19 11:18:43 2025 +0200
+++ b/SQLite/Plugins/SQLiteIndex.cpp	Fri Jun 13 15:16:52 2025 +0200
@@ -149,7 +149,22 @@
         SetGlobalIntegerProperty(manager, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabasePatchLevel, revision);
       }
 
-      if (revision != 1)
+      // install customData
+      if (!LookupGlobalIntegerProperty(revision, manager, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabasePatchLevel)
+          || revision == 1)
+      {
+        std::string query;
+
+        Orthanc::EmbeddedResources::GetFileResource
+          (query, Orthanc::EmbeddedResources::SQLITE_INSTALL_CUSTOM_DATA);
+
+        t.GetDatabaseTransaction().ExecuteMultiLines(query);
+
+        revision = 2;
+        SetGlobalIntegerProperty(manager, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabasePatchLevel, revision);
+      }
+
+      if (revision != 2)
       {
         LOG(ERROR) << "SQLite plugin is incompatible with database schema revision: " << revision;
         throw Orthanc::OrthancException(Orthanc::ErrorCode_Database);        
--- a/SQLite/Plugins/SQLiteIndex.h	Mon May 19 11:18:43 2025 +0200
+++ b/SQLite/Plugins/SQLiteIndex.h	Fri Jun 13 15:16:52 2025 +0200
@@ -55,6 +55,21 @@
       return true;
     }
     
+    virtual bool HasAttachmentCustomDataSupport() const ORTHANC_OVERRIDE
+    {
+      return true;
+    }
+
+    virtual bool HasKeyValueStores() const ORTHANC_OVERRIDE
+    {
+      return true;
+    }
+
+    virtual bool HasQueues() const ORTHANC_OVERRIDE
+    {
+      return true;
+    }
+
     virtual int64_t CreateResource(DatabaseManager& manager,
                                    const char* publicId,
                                    OrthancPluginResourceType type) ORTHANC_OVERRIDE;