changeset 652:e5051580aeac attach-custom-data

merged default -> attach-custom-data
author Alain Mazy <am@orthanc.team>
date Mon, 10 Mar 2025 18:53:00 +0100 (2 months ago)
parents 354d4c617387 (diff) f9e43680c480 (current diff)
children d5c889dea585
files Framework/Common/DatabaseManager.cpp Framework/Common/DatabaseManager.h Framework/Plugins/IndexBackend.cpp Framework/Plugins/IndexBackend.h MySQL/Plugins/MySQLIndex.cpp MySQL/Plugins/MySQLIndex.h Odbc/Plugins/OdbcIndex.cpp Odbc/Plugins/OdbcIndex.h PostgreSQL/CMakeLists.txt PostgreSQL/NEWS PostgreSQL/Plugins/PostgreSQLIndex.cpp PostgreSQL/Plugins/PostgreSQLIndex.h PostgreSQL/Plugins/SQL/Downgrades/Rev4ToRev3.sql PostgreSQL/Plugins/SQL/PrepareIndex.sql PostgreSQL/Plugins/SQL/Upgrades/Rev2ToRev3.sql PostgreSQL/Plugins/SQL/Upgrades/Rev3ToRev4.sql SQLite/Plugins/SQLiteIndex.cpp SQLite/Plugins/SQLiteIndex.h
diffstat 37 files changed, 723 insertions(+), 398 deletions(-) [+]
line wrap: on
line diff
--- a/Framework/Common/DatabaseManager.cpp	Thu Feb 27 09:14:30 2025 +0100
+++ b/Framework/Common/DatabaseManager.cpp	Mon Mar 10 18:53:00 2025 +0100
@@ -550,6 +550,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) :
--- a/Framework/Common/DatabaseManager.h	Thu Feb 27 09:14:30 2025 +0100
+++ b/Framework/Common/DatabaseManager.h	Mon Mar 10 18:53:00 2025 +0100
@@ -188,6 +188,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/MySQL/MySQLStatement.cpp	Thu Feb 27 09:14:30 2025 +0100
+++ b/Framework/MySQL/MySQLStatement.cpp	Mon Mar 10 18:53:00 2025 +0100
@@ -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	Thu Feb 27 09:14:30 2025 +0100
+++ b/Framework/Plugins/DatabaseBackendAdapterV2.cpp	Mon Mar 10 18:53:00 2025 +0100
@@ -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	Thu Feb 27 09:14:30 2025 +0100
+++ b/Framework/Plugins/DatabaseBackendAdapterV3.cpp	Mon Mar 10 18:53:00 2025 +0100
@@ -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	Thu Feb 27 09:14:30 2025 +0100
+++ b/Framework/Plugins/DatabaseBackendAdapterV4.cpp	Mon Mar 10 18:53:00 2025 +0100
@@ -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,9 @@
       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 +273,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 +291,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
       {
@@ -682,6 +690,9 @@
       
       case Orthanc::DatabasePluginMessages::OPERATION_ADD_ATTACHMENT:
       {
+#if ORTHANC_PLUGINS_HAS_ATTACHMENTS_CUSTOM_DATA
+        backend.AddAttachment(response, manager, request.add_attachment());
+#else
         OrthancPluginAttachment attachment;
         attachment.uuid = request.add_attachment().attachment().uuid().c_str();
         attachment.contentType = request.add_attachment().attachment().content_type();
@@ -692,9 +703,9 @@
         attachment.compressedHash = request.add_attachment().attachment().compressed_hash().c_str();
         
         backend.AddAttachment(manager, request.add_attachment().id(), attachment, request.add_attachment().revision());
+#endif
         break;
       }
-      
       case Orthanc::DatabasePluginMessages::OPERATION_CLEAR_CHANGES:
       {
         backend.ClearChanges(manager);
--- a/Framework/Plugins/IDatabaseBackend.h	Thu Feb 27 09:14:30 2025 +0100
+++ b/Framework/Plugins/IDatabaseBackend.h	Mon Mar 10 18:53:00 2025 +0100
@@ -59,11 +59,20 @@
 
     virtual bool HasRevisionsSupport() const = 0;
 
+    virtual bool HasAttachmentCustomDataSupport() const = 0;
+
     virtual void AddAttachment(DatabaseManager& manager,
                                int64_t id,
                                const OrthancPluginAttachment& attachment,
                                int64_t revision) = 0;
 
+#if ORTHANC_PLUGINS_HAS_ATTACHMENTS_CUSTOM_DATA
+    // New in Orthanc 1.12.7
+    virtual void AddAttachment(Orthanc::DatabasePluginMessages::TransactionResponse& response,
+                               DatabaseManager& manager,
+                               const Orthanc::DatabasePluginMessages::AddAttachment_Request& request) = 0;
+#endif
+
     virtual void AttachChild(DatabaseManager& manager,
                              int64_t parent,
                              int64_t child) = 0;
--- a/Framework/Plugins/IDatabaseBackendOutput.h	Thu Feb 27 09:14:30 2025 +0100
+++ b/Framework/Plugins/IDatabaseBackendOutput.h	Mon Mar 10 18:53:00 2025 +0100
@@ -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	Thu Feb 27 09:14:30 2025 +0100
+++ b/Framework/Plugins/IndexBackend.cpp	Mon Mar 10 18:53:00 2025 +0100
@@ -270,7 +270,7 @@
     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();
@@ -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,25 @@
     }
   }
 
-
-  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 char* customData,
+                                   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 +382,62 @@
     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);
+    if (customData != NULL && strlen(customData) > 0)
+    {
+      args.SetUtf8Value("custom-data", customData);
+    }
+    else
+    {
+      args.SetNullValue("custom-data");
+    }
 
     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() && HasAttachmentCustomDataSupport()); // all plugins support these features now
+    ExecuteAddAttachment(manager, id, attachment.uuid, attachment.contentType, attachment.uncompressedSize, attachment.uncompressedHash,
+                         attachment.compressionType, attachment.compressedSize, attachment.compressedHash, "", revision);
   }
 
-    
+#if ORTHANC_PLUGINS_HAS_ATTACHMENTS_CUSTOM_DATA
+  void IndexBackend::AddAttachment(Orthanc::DatabasePluginMessages::TransactionResponse& response,
+                                   DatabaseManager& manager,
+                                   const Orthanc::DatabasePluginMessages::AddAttachment_Request& request)
+  {
+    assert(HasRevisionsSupport() && HasAttachmentCustomDataSupport()); // all plugins support these features now
+    ExecuteAddAttachment(manager, 
+                         request.id(), 
+                         request.attachment().uuid().c_str(),
+                         request.attachment().content_type(),
+                         request.attachment().uncompressed_size(),
+                         request.attachment().uncompressed_hash().c_str(),
+                         request.attachment().compression_type(),
+                         request.attachment().compressed_size(),
+                         request.attachment().compressed_hash().c_str(),
+                         request.attachment().custom_data().c_str(),
+                         request.revision());
+  }
+#endif
+
+
   void IndexBackend::AttachChild(DatabaseManager& manager,
                                  int64_t parent,
                                  int64_t child)
@@ -1178,12 +1202,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);
@@ -1200,64 +1232,34 @@
     }
     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;
+      if (statement.GetResultField(7).GetType() == ValueType_Utf8String) // column has been added in 1.12.0
+      {
+        customData = statement.ReadString(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 +3255,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 +3434,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 +3449,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("TEXT") + " 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 +3467,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("TEXT") + " 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 +3487,12 @@
              "  value AS c3_string1, "
              "  " + formatter.FormatNull("TEXT") + " AS c4_string2, "
              "  " + formatter.FormatNull("TEXT") + " AS c5_string3, "
-             "  type AS c6_int1, "
+             "  " + formatter.FormatNull("TEXT") + " 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 +3507,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 +3527,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("TEXT") + " 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 +3566,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("TEXT") + " 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 +3586,12 @@
                "  value AS c3_string1, "
                "  " + formatter.FormatNull("TEXT") + " AS c4_string2, "
                "  " + formatter.FormatNull("TEXT") + " AS c5_string3, "
-               "  type AS c6_int1, "
+               "  " + formatter.FormatNull("TEXT") + " 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 +3623,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("TEXT") + " 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 +3644,12 @@
                 "  value AS c3_string1, "
                 "  " + formatter.FormatNull("TEXT") + " AS c4_string2, "
                 "  " + formatter.FormatNull("TEXT") + " AS c5_string3, "
-                "  type AS c6_int1, "
+                "  " + formatter.FormatNull("TEXT") + " 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 +3672,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("TEXT") + " 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 +3693,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("TEXT") + " 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 +3729,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("TEXT") + " 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 +3752,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("TEXT") + " 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 +3772,12 @@
                 "  value AS c3_string1, "
                 "  " + formatter.FormatNull("TEXT") + " AS c4_string2, "
                 "  " + formatter.FormatNull("TEXT") + " AS c5_string3, "
-                "  type AS c6_int1, "
+                "  " + formatter.FormatNull("TEXT") + " 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 +3797,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("TEXT") + " 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 +3811,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 +3834,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("TEXT") + " 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 +3864,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("TEXT") + " 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 +3885,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("TEXT") + " 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 +3906,12 @@
                  "  value AS c3_string1, "
                  "  " + formatter.FormatNull("TEXT") + " AS c4_string2, "
                  "  " + formatter.FormatNull("TEXT") + " AS c5_string3, "
-                 "  type AS c6_int1, "
+                 "  " + formatter.FormatNull("TEXT") + " 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 +3932,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("TEXT") + " 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 +3977,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("TEXT") + " 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 +3988,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 +4001,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("TEXT") + " 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 +4027,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("TEXT") + " 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 +4049,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("TEXT") + " 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 +4064,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("TEXT") + " 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 +4080,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";
 
@@ -4110,8 +4141,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 +4151,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 +4161,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 +4175,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 +4183,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 +4193,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 +4207,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 +4216,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 +4226,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 +4240,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 +4250,15 @@
           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_custom_data(statement->ReadStringOrNull(C6_STRING_4));
+          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 (!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 +4272,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 +4290,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 +4308,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 +4334,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 +4352,11 @@
           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_custom_data(statement->ReadStringOrNull(C6_STRING_4));
+          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));
         }; break;
 
         default:
--- a/Framework/Plugins/IndexBackend.h	Thu Feb 27 09:14:30 2025 +0100
+++ b/Framework/Plugins/IndexBackend.h	Mon Mar 10 18:53:00 2025 +0100
@@ -102,7 +102,14 @@
                                int64_t id,
                                const OrthancPluginAttachment& attachment,
                                int64_t revision) ORTHANC_OVERRIDE;
-    
+
+#if ORTHANC_PLUGINS_HAS_ATTACHMENTS_CUSTOM_DATA
+    // New in Orthanc 1.12.7
+    virtual void AddAttachment(Orthanc::DatabasePluginMessages::TransactionResponse& response,
+                               DatabaseManager& manager,
+                               const Orthanc::DatabasePluginMessages::AddAttachment_Request& request) ORTHANC_OVERRIDE;
+#endif
+
     virtual void AttachChild(DatabaseManager& manager,
                              int64_t parent,
                              int64_t child) ORTHANC_OVERRIDE;
--- a/Framework/Plugins/MessagesToolbox.h	Thu Feb 27 09:14:30 2025 +0100
+++ b/Framework/Plugins/MessagesToolbox.h	Mon Mar 10 18:53:00 2025 +0100
@@ -59,6 +59,15 @@
 #endif
 
 
+#define ORTHANC_PLUGINS_HAS_ATTACHMENTS_CUSTOM_DATA 0
+
+#if defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE)
+#  if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 7)
+#    undef  ORTHANC_PLUGINS_HAS_ATTACHMENTS_CUSTOM_DATA
+#    define ORTHANC_PLUGINS_HAS_ATTACHMENTS_CUSTOM_DATA 1
+#  endif
+#endif
+
 #include <Enumerations.h>
 
 
--- a/Framework/PostgreSQL/PostgreSQLStatement.cpp	Thu Feb 27 09:14:30 2025 +0100
+++ b/Framework/PostgreSQL/PostgreSQLStatement.cpp	Mon Mar 10 18:53:00 2025 +0100
@@ -528,42 +528,47 @@
     {
       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)
+      {
+        BindNull(i);
+      }
+      else
+      {
+        switch (formatter_.GetParameterType(i))
+        {
+          case ValueType_Integer64:
+            BindInteger64(i, dynamic_cast<const Integer64Value&>(parameters.GetValue(name)).GetValue());
+            break;
 
-        case ValueType_Null:
-          BindNull(i);
-          break;
+          case ValueType_Integer32:
+            BindInteger(i, dynamic_cast<const Integer32Value&>(parameters.GetValue(name)).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&>
+                      (parameters.GetValue(name)).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&>
+                      (parameters.GetValue(name)).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&>(parameters.GetValue(name));
 
-          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	Thu Feb 27 09:14:30 2025 +0100
+++ b/MySQL/CMakeLists.txt	Mon Mar 10 18:53:00 2025 +0100
@@ -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	Thu Feb 27 09:14:30 2025 +0100
+++ b/MySQL/NEWS	Mon Mar 10 18:53:00 2025 +0100
@@ -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	Mon Mar 10 18:53:00 2025 +0100
@@ -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	Thu Feb 27 09:14:30 2025 +0100
+++ b/MySQL/Plugins/MySQLIndex.cpp	Mon Mar 10 18:53:00 2025 +0100
@@ -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	Thu Feb 27 09:14:30 2025 +0100
+++ b/MySQL/Plugins/MySQLIndex.h	Mon Mar 10 18:53:00 2025 +0100
@@ -58,7 +58,12 @@
  
     virtual bool HasRevisionsSupport() const ORTHANC_OVERRIDE
     {
-      return false;  // TODO - REVISIONS
+      return true;
+    }
+
+    virtual bool HasAttachmentCustomDataSupport() const ORTHANC_OVERRIDE
+    {
+      return true;
     }
     
     virtual int64_t CreateResource(DatabaseManager& manager,
--- a/MySQL/Plugins/PrepareIndex.sql	Thu Feb 27 09:14:30 2025 +0100
+++ b/MySQL/Plugins/PrepareIndex.sql	Mon Mar 10 18:53:00 2025 +0100
@@ -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/NEWS	Thu Feb 27 09:14:30 2025 +0100
+++ b/Odbc/NEWS	Mon Mar 10 18:53:00 2025 +0100
@@ -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	Thu Feb 27 09:14:30 2025 +0100
+++ b/Odbc/Plugins/OdbcIndex.cpp	Mon Mar 10 18:53:00 2025 +0100
@@ -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	Thu Feb 27 09:14:30 2025 +0100
+++ b/Odbc/Plugins/OdbcIndex.h	Mon Mar 10 18:53:00 2025 +0100
@@ -73,6 +73,11 @@
       return true;
     }
 
+    bool HasAttachmentCustomDataSupport() const ORTHANC_OVERRIDE
+    {
+      return true;
+    }
+
     virtual int64_t CreateResource(DatabaseManager& manager,
                                    const char* publicId,
                                    OrthancPluginResourceType type) ORTHANC_OVERRIDE;
--- a/PostgreSQL/CMakeLists.txt	Thu Feb 27 09:14:30 2025 +0100
+++ b/PostgreSQL/CMakeLists.txt	Mon Mar 10 18:53:00 2025 +0100
@@ -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	Thu Feb 27 09:14:30 2025 +0100
+++ b/PostgreSQL/NEWS	Mon Mar 10 18:53:00 2025 +0100
@@ -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	Thu Feb 27 09:14:30 2025 +0100
+++ b/PostgreSQL/Plugins/PostgreSQLIndex.cpp	Mon Mar 10 18:53:00 2025 +0100
@@ -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	Thu Feb 27 09:14:30 2025 +0100
+++ b/PostgreSQL/Plugins/PostgreSQLIndex.h	Mon Mar 10 18:53:00 2025 +0100
@@ -72,6 +72,11 @@
       return true;
     }
     
+    virtual bool HasAttachmentCustomDataSupport() 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	Thu Feb 27 09:14:30 2025 +0100
+++ b/PostgreSQL/Plugins/SQL/Downgrades/Rev3ToRev2.sql	Mon Mar 10 18:53:00 2025 +0100
@@ -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	Thu Feb 27 09:14:30 2025 +0100
+++ b/PostgreSQL/Plugins/SQL/Downgrades/Rev4ToRev3.sql	Mon Mar 10 18:53:00 2025 +0100
@@ -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
--- a/PostgreSQL/Plugins/SQL/PrepareIndex.sql	Thu Feb 27 09:14:30 2025 +0100
+++ b/PostgreSQL/Plugins/SQL/PrepareIndex.sql	Mon Mar 10 18:53:00 2025 +0100
@@ -53,6 +53,7 @@
        uncompressedHash VARCHAR(40),
        compressedHash VARCHAR(40),
        revision INTEGER,
+       customData TEXT,              -- new in schema rev 4
        PRIMARY KEY(id, fileType)
        );              
 
@@ -305,7 +306,9 @@
         uncompressedSize BIGINT,
         compressionType INTEGER,
         uncompressedHash VARCHAR(40),
-        compressedHash VARCHAR(40)
+        compressedHash VARCHAR(40),
+        revision INTEGER,
+        customData TEXT
         );
 
     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;
--- a/PostgreSQL/Plugins/SQL/Upgrades/Rev1ToRev2.sql	Thu Feb 27 09:14:30 2025 +0100
+++ b/PostgreSQL/Plugins/SQL/Upgrades/Rev1ToRev2.sql	Mon Mar 10 18:53:00 2025 +0100
@@ -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	Thu Feb 27 09:14:30 2025 +0100
+++ b/PostgreSQL/Plugins/SQL/Upgrades/Rev2ToRev3.sql	Mon Mar 10 18:53:00 2025 +0100
@@ -41,4 +41,3 @@
 
 -- other changes performed in PrepareIndex.sql:
   -- add ChildCount triggers
-
--- a/PostgreSQL/Plugins/SQL/Upgrades/Rev3ToRev4.sql	Thu Feb 27 09:14:30 2025 +0100
+++ b/PostgreSQL/Plugins/SQL/Upgrades/Rev3ToRev4.sql	Mon Mar 10 18:53:00 2025 +0100
@@ -1,2 +1,2 @@
 -- everything is performed in PrepareIndex.sql
-SELECT 1;
\ No newline at end of file
+SELECT 1;>>>>>>> merge rev
--- a/README	Thu Feb 27 09:14:30 2025 +0100
+++ b/README	Mon Mar 10 18:53:00 2025 +0100
@@ -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/SQLite/CMakeLists.txt	Thu Feb 27 09:14:30 2025 +0100
+++ b/SQLite/CMakeLists.txt	Mon Mar 10 18:53:00 2025 +0100
@@ -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	Thu Feb 27 09:14:30 2025 +0100
+++ b/SQLite/NEWS	Mon Mar 10 18:53:00 2025 +0100
@@ -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	Mon Mar 10 18:53:00 2025 +0100
@@ -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/SQLiteIndex.cpp	Thu Feb 27 09:14:30 2025 +0100
+++ b/SQLite/Plugins/SQLiteIndex.cpp	Mon Mar 10 18:53:00 2025 +0100
@@ -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	Thu Feb 27 09:14:30 2025 +0100
+++ b/SQLite/Plugins/SQLiteIndex.h	Mon Mar 10 18:53:00 2025 +0100
@@ -55,6 +55,11 @@
       return true;
     }
     
+    bool HasAttachmentCustomDataSupport() const ORTHANC_OVERRIDE
+    {
+      return true;
+    }
+
     virtual int64_t CreateResource(DatabaseManager& manager,
                                    const char* publicId,
                                    OrthancPluginResourceType type) ORTHANC_OVERRIDE;