changeset 366:cd9521e04249 attach-custom-data

DatabaseBackendAdapterV4: added support for customData + revision when not already done
author Alain Mazy <am@osimis.io>
date Thu, 15 Sep 2022 18:12:34 +0200
parents 7671fa7f099e
children 82f73188b58d
files .hgignore Framework/Plugins/DatabaseBackendAdapterV2.cpp Framework/Plugins/DatabaseBackendAdapterV3.cpp Framework/Plugins/DatabaseBackendAdapterV4.cpp Framework/Plugins/DatabaseBackendAdapterV4.h Framework/Plugins/IDatabaseBackend.h Framework/Plugins/IDatabaseBackendOutput.h Framework/Plugins/IndexBackend.cpp Framework/Plugins/IndexBackend.h MySQL/CMakeLists.txt MySQL/NEWS MySQL/Plugins/InstallRevisionAndCustomData.sql MySQL/Plugins/MySQLIndex.cpp MySQL/Plugins/MySQLIndex.h MySQL/Plugins/PrepareIndex.sql Odbc/NEWS Odbc/Plugins/OdbcIndex.cpp Odbc/Plugins/OdbcIndex.h PostgreSQL/NEWS PostgreSQL/Plugins/PostgreSQLIndex.cpp PostgreSQL/Plugins/PostgreSQLIndex.h PostgreSQL/Plugins/PrepareIndex.sql README Resources/CMake/DatabasesPluginConfiguration.cmake SQLite/CMakeLists.txt SQLite/NEWS SQLite/Plugins/InstallCustomData.sql SQLite/Plugins/SQLiteIndex.cpp SQLite/Plugins/SQLiteIndex.h
diffstat 29 files changed, 2615 insertions(+), 146 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Tue Jul 05 08:44:26 2022 +0200
+++ b/.hgignore	Thu Sep 15 18:12:34 2022 +0200
@@ -3,5 +3,6 @@
 PostgreSQL/ThirdPartyDownloads/
 Odbc/ThirdPartyDownloads/
 MySQL/ThirdPartyDownloads/
+SQLite/ThirdPartyDownloads/
 .vscode/
 
--- a/Framework/Plugins/DatabaseBackendAdapterV2.cpp	Tue Jul 05 08:44:26 2022 +0200
+++ b/Framework/Plugins/DatabaseBackendAdapterV2.cpp	Thu Sep 15 18:12:34 2022 +0200
@@ -184,7 +184,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();
@@ -216,7 +217,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	Tue Jul 05 08:44:26 2022 +0200
+++ b/Framework/Plugins/DatabaseBackendAdapterV3.cpp	Thu Sep 15 18:12:34 2022 +0200
@@ -584,7 +584,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;
@@ -630,7 +631,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);
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Plugins/DatabaseBackendAdapterV4.cpp	Thu Sep 15 18:12:34 2022 +0200
@@ -0,0 +1,2117 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2021 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "DatabaseBackendAdapterV4.h"
+
+#if defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE)         // Macro introduced in Orthanc 1.3.1
+#  if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 0)
+
+#include <Logging.h>
+#include <MultiThreading/SharedMessageQueue.h>
+#include <OrthancException.h>
+
+#include <stdexcept>
+#include <list>
+#include <string>
+#include <cassert>
+
+
+#define ORTHANC_PLUGINS_DATABASE_CATCH(context)                         \
+  catch (::Orthanc::OrthancException& e)                                \
+  {                                                                     \
+    return static_cast<OrthancPluginErrorCode>(e.GetErrorCode());       \
+  }                                                                     \
+  catch (::std::runtime_error& e)                                       \
+  {                                                                     \
+    const std::string message = "Exception in database back-end: " + std::string(e.what()); \
+    OrthancPluginLogError(context, message.c_str());                    \
+    return OrthancPluginErrorCode_DatabasePlugin;                       \
+  }                                                                     \
+  catch (...)                                                           \
+  {                                                                     \
+    OrthancPluginLogError(context, "Native exception");                 \
+    return OrthancPluginErrorCode_DatabasePlugin;                       \
+  }
+
+
+namespace OrthancDatabases
+{
+  static bool isBackendInUse_ = false;  // Only for sanity checks
+
+  
+  template <typename T>
+  static void CopyListToVector(std::vector<T>& target,
+                               const std::list<T>& source)
+  {
+    /**
+     * This has the the same effect as:
+     *
+     *   target.reserve(source.size());
+     *   std::copy(std::begin(source), std::end(source), std::back_inserter(target));
+     *
+     * However, this implementation is compatible with C++03 (Linux
+     * Standard Base), whereas "std::back_inserter" requires C++11.
+     **/
+
+    target.clear();
+    target.reserve(source.size());
+
+    for (typename std::list<T>::const_iterator it = source.begin(); it != source.end(); ++it)
+    {
+      target.push_back(*it);
+    }
+  }
+    
+    
+  class DatabaseBackendAdapterV4::Adapter : public boost::noncopyable
+  {
+  private:
+    class ManagerReference : public Orthanc::IDynamicObject
+    {
+    private:
+      DatabaseManager*  manager_;
+
+    public:
+      ManagerReference(DatabaseManager& manager) :
+        manager_(&manager)
+      {
+      }
+
+      DatabaseManager& GetManager()
+      {
+        assert(manager_ != NULL);
+        return *manager_;
+      }
+    };
+    
+    std::unique_ptr<IndexBackend>  backend_;
+    OrthancPluginContext*          context_;
+    boost::shared_mutex            connectionsMutex_;
+    size_t                         countConnections_;
+    std::list<DatabaseManager*>    connections_;
+    Orthanc::SharedMessageQueue    availableConnections_;
+
+  public:
+    Adapter(IndexBackend* backend,
+            size_t countConnections) :
+      backend_(backend),
+      countConnections_(countConnections)
+    {
+      if (countConnections == 0)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange,
+                                        "There must be a non-zero number of connections to the database");
+      }
+      else if (backend == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+      }
+      else
+      {
+        context_ = backend_->GetContext();
+      }
+    }
+
+    ~Adapter()
+    {
+      for (std::list<DatabaseManager*>::iterator
+             it = connections_.begin(); it != connections_.end(); ++it)
+      {
+        assert(*it != NULL);
+        delete *it;
+      }
+    }
+
+    OrthancPluginContext* GetContext() const
+    {
+      return context_;
+    }
+
+    void OpenConnections()
+    {
+      boost::unique_lock<boost::shared_mutex>  lock(connectionsMutex_);
+
+      if (connections_.size() == 0)
+      {
+        assert(backend_.get() != NULL);
+
+        {
+          std::unique_ptr<DatabaseManager> manager(new DatabaseManager(backend_->CreateDatabaseFactory()));
+          manager->GetDatabase();  // Make sure to open the database connection
+          
+          backend_->ConfigureDatabase(*manager);
+          connections_.push_back(manager.release());
+        }
+
+        for (size_t i = 1; i < countConnections_; i++)
+        {
+          connections_.push_back(new DatabaseManager(backend_->CreateDatabaseFactory()));
+          connections_.back()->GetDatabase();  // Make sure to open the database connection
+        }
+
+        for (std::list<DatabaseManager*>::iterator
+               it = connections_.begin(); it != connections_.end(); ++it)
+        {
+          assert(*it != NULL);
+          availableConnections_.Enqueue(new ManagerReference(**it));
+        }        
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+    }
+
+    void CloseConnections()
+    {
+      boost::unique_lock<boost::shared_mutex>  lock(connectionsMutex_);
+
+      if (connections_.size() != countConnections_)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+      else if (availableConnections_.GetSize() != countConnections_)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_Database, "Some connections are still in use, bug in the Orthanc core");
+      }
+      else
+      {
+        for (std::list<DatabaseManager*>::iterator
+               it = connections_.begin(); it != connections_.end(); ++it)
+        {
+          assert(*it != NULL);
+          (*it)->Close();
+        }
+      }
+    }
+
+    class DatabaseAccessor : public boost::noncopyable
+    {
+    private:
+      boost::shared_lock<boost::shared_mutex>  lock_;
+      Adapter&                                 adapter_;
+      DatabaseManager*                         manager_;
+      
+    public:
+      DatabaseAccessor(Adapter& adapter) :
+        lock_(adapter.connectionsMutex_),
+        adapter_(adapter),
+        manager_(NULL)
+      {
+        for (;;)
+        {
+          std::unique_ptr<Orthanc::IDynamicObject> manager(adapter.availableConnections_.Dequeue(100));
+          if (manager.get() != NULL)
+          {
+            manager_ = &dynamic_cast<ManagerReference&>(*manager).GetManager();
+            return;
+          }
+        }
+      }
+
+      ~DatabaseAccessor()
+      {
+        assert(manager_ != NULL);
+        adapter_.availableConnections_.Enqueue(new ManagerReference(*manager_));
+      }
+
+      IndexBackend& GetBackend() const
+      {
+        return *adapter_.backend_;
+      }
+
+      DatabaseManager& GetManager() const
+      {
+        assert(manager_ != NULL);
+        return *manager_;
+      }
+    };
+  };
+
+
+  class DatabaseBackendAdapterV4::Output : public IDatabaseBackendOutput
+  {
+  private:
+    struct Metadata
+    {
+      int32_t      metadata;
+      const char*  value;
+    };
+    
+    _OrthancPluginDatabaseAnswerType            answerType_;
+    std::list<std::string>                      stringsStore_;
+    
+    std::vector<OrthancPluginAttachment2>       attachments_;
+    std::vector<OrthancPluginChange>            changes_;
+    std::vector<OrthancPluginDicomTag>          tags_;
+    std::vector<OrthancPluginExportedResource>  exported_;
+    std::vector<OrthancPluginDatabaseEvent2>    events_;
+    std::vector<int32_t>                        integers32_;
+    std::vector<int64_t>                        integers64_;
+    std::vector<OrthancPluginMatchingResource>  matches_;
+    std::vector<Metadata>                       metadata_;
+    std::vector<std::string>                    stringAnswers_;
+    
+    const char* StoreString(const std::string& s)
+    {
+      stringsStore_.push_back(s);
+      return stringsStore_.back().c_str();
+    }
+
+    void SetupAnswerType(_OrthancPluginDatabaseAnswerType type)
+    {
+      if (answerType_ == _OrthancPluginDatabaseAnswerType_None)
+      {
+        answerType_ = type;
+      }
+      else if (answerType_ != type)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+    }
+    
+  public:
+    Output() :
+      answerType_(_OrthancPluginDatabaseAnswerType_None)
+    {
+    }
+
+    void Clear()
+    {
+      // We don't systematically clear all the vectors, in order to
+      // avoid spending unnecessary time
+      
+      switch (answerType_)
+      {
+        case _OrthancPluginDatabaseAnswerType_None:
+          break;
+        
+        case _OrthancPluginDatabaseAnswerType_Attachment:
+          attachments_.clear();
+          break;
+        
+        case _OrthancPluginDatabaseAnswerType_Change:
+          changes_.clear();
+          break;
+        
+        case _OrthancPluginDatabaseAnswerType_DicomTag:
+          tags_.clear();
+          break;
+        
+        case _OrthancPluginDatabaseAnswerType_ExportedResource:
+          exported_.clear();
+          break;
+        
+        case _OrthancPluginDatabaseAnswerType_Int32:
+          integers32_.clear();
+          break;
+        
+        case _OrthancPluginDatabaseAnswerType_Int64:
+          integers64_.clear();
+          break;
+        
+        case _OrthancPluginDatabaseAnswerType_MatchingResource:
+          matches_.clear();
+          break;
+        
+        case _OrthancPluginDatabaseAnswerType_Metadata:
+          metadata_.clear();
+          break;
+        
+        case _OrthancPluginDatabaseAnswerType_String:
+          stringAnswers_.clear();
+          break;
+        
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+      
+      answerType_ = _OrthancPluginDatabaseAnswerType_None;
+      stringsStore_.clear();
+      events_.clear();
+      
+      assert(attachments_.empty());
+      assert(changes_.empty());
+      assert(tags_.empty());
+      assert(exported_.empty());
+      assert(events_.empty());
+      assert(integers32_.empty());
+      assert(integers64_.empty());
+      assert(matches_.empty());
+      assert(metadata_.empty());
+      assert(stringAnswers_.empty());
+    }
+
+
+    OrthancPluginErrorCode ReadAnswersCount(uint32_t& target) const
+    {
+      switch (answerType_)
+      {
+        case _OrthancPluginDatabaseAnswerType_None:
+          target = static_cast<uint32_t>(0);
+          break;
+          
+        case _OrthancPluginDatabaseAnswerType_Attachment:
+          target = static_cast<uint32_t>(attachments_.size());
+          break;
+        
+        case _OrthancPluginDatabaseAnswerType_Change:
+          target = static_cast<uint32_t>(changes_.size());
+          break;
+        
+        case _OrthancPluginDatabaseAnswerType_DicomTag:
+          target = static_cast<uint32_t>(tags_.size());
+          break;
+        
+        case _OrthancPluginDatabaseAnswerType_ExportedResource:
+          target = static_cast<uint32_t>(exported_.size());
+          break;
+        
+        case _OrthancPluginDatabaseAnswerType_Int32:
+          target = static_cast<uint32_t>(integers32_.size());
+          break;
+        
+        case _OrthancPluginDatabaseAnswerType_Int64:
+          target = static_cast<uint32_t>(integers64_.size());
+          break;
+        
+        case _OrthancPluginDatabaseAnswerType_MatchingResource:
+          target = static_cast<uint32_t>(matches_.size());
+          break;
+        
+        case _OrthancPluginDatabaseAnswerType_Metadata:
+          target = static_cast<uint32_t>(metadata_.size());
+          break;
+        
+        case _OrthancPluginDatabaseAnswerType_String:
+          target = static_cast<uint32_t>(stringAnswers_.size());
+          break;
+        
+        default:
+          return OrthancPluginErrorCode_InternalError;
+      }
+
+      return OrthancPluginErrorCode_Success;
+    }
+
+
+    OrthancPluginErrorCode ReadAnswerAttachment2(OrthancPluginAttachment2& target /* out */,
+                                                 uint32_t index) const
+    {
+      if (index < attachments_.size())
+      {
+        target = attachments_[index];
+        return OrthancPluginErrorCode_Success;
+      }
+      else
+      {
+        return OrthancPluginErrorCode_ParameterOutOfRange;
+      }
+    }
+
+
+    OrthancPluginErrorCode ReadAnswerChange(OrthancPluginChange& target /* out */,
+                                            uint32_t index) const
+    {
+      if (index < changes_.size())
+      {
+        target = changes_[index];
+        return OrthancPluginErrorCode_Success;        
+      }
+      else
+      {
+        return OrthancPluginErrorCode_ParameterOutOfRange;        
+      }
+    }
+
+
+    OrthancPluginErrorCode ReadAnswerDicomTag(uint16_t& group,
+                                              uint16_t& element,
+                                              const char*& value,
+                                              uint32_t index) const
+    {
+      if (index < tags_.size())
+      {
+        const OrthancPluginDicomTag& tag = tags_[index];
+        group = tag.group;
+        element = tag.element;
+        value = tag.value;
+        return OrthancPluginErrorCode_Success;        
+      }
+      else
+      {
+        return OrthancPluginErrorCode_ParameterOutOfRange;        
+      }
+    }
+
+
+    OrthancPluginErrorCode ReadAnswerExportedResource(OrthancPluginExportedResource& target /* out */,
+                                                      uint32_t index) const
+    {
+      if (index < exported_.size())
+      {
+        target = exported_[index];
+        return OrthancPluginErrorCode_Success;        
+      }
+      else
+      {
+        return OrthancPluginErrorCode_ParameterOutOfRange;        
+      }
+    }
+
+
+    OrthancPluginErrorCode ReadAnswerInt32(int32_t& target,
+                                           uint32_t index) const
+    {
+      if (index < integers32_.size())
+      {
+        target = integers32_[index];
+        return OrthancPluginErrorCode_Success;        
+      }
+      else
+      {
+        return OrthancPluginErrorCode_ParameterOutOfRange;        
+      }
+    }
+
+
+    OrthancPluginErrorCode ReadAnswerInt64(int64_t& target,
+                                           uint32_t index) const
+    {
+      if (index < integers64_.size())
+      {
+        target = integers64_[index];
+        return OrthancPluginErrorCode_Success;        
+      }
+      else
+      {
+        return OrthancPluginErrorCode_ParameterOutOfRange;        
+      }
+    }
+
+
+    OrthancPluginErrorCode ReadAnswerMatchingResource(OrthancPluginMatchingResource& target,
+                                                      uint32_t index) const
+    {
+      if (index < matches_.size())
+      {
+        target = matches_[index];
+        return OrthancPluginErrorCode_Success;        
+      }
+      else
+      {
+        return OrthancPluginErrorCode_ParameterOutOfRange;        
+      }
+    }
+
+
+    OrthancPluginErrorCode ReadAnswerMetadata(int32_t& metadata,
+                                              const char*& value,
+                                              uint32_t index) const
+    {
+      if (index < metadata_.size())
+      {
+        const Metadata& tmp = metadata_[index];
+        metadata = tmp.metadata;
+        value = tmp.value;
+        return OrthancPluginErrorCode_Success;        
+      }
+      else
+      {
+        return OrthancPluginErrorCode_ParameterOutOfRange;        
+      }
+    }
+
+
+    OrthancPluginErrorCode ReadAnswerString(const char*& target,
+                                            uint32_t index) const
+    {
+      if (index < stringAnswers_.size())
+      {
+        target = stringAnswers_[index].c_str();
+        return OrthancPluginErrorCode_Success;        
+      }
+      else
+      {
+        return OrthancPluginErrorCode_ParameterOutOfRange;        
+      }
+    }
+
+
+    OrthancPluginErrorCode ReadEventsCount(uint32_t& target /* out */) const
+    {
+      target = static_cast<uint32_t>(events_.size());
+      return OrthancPluginErrorCode_Success;
+    }
+
+    
+    OrthancPluginErrorCode ReadEvent2(OrthancPluginDatabaseEvent2& event /* out */,
+                                      uint32_t index) const
+    {
+      if (index < events_.size())
+      {
+        event = events_[index];
+        return OrthancPluginErrorCode_Success;
+      }
+      else
+      {
+        return OrthancPluginErrorCode_ParameterOutOfRange;
+      }
+    }
+
+
+    virtual void SignalDeletedAttachment(const std::string& uuid,
+                                         int32_t            contentType,
+                                         uint64_t           uncompressedSize,
+                                         const std::string& uncompressedHash,
+                                         int32_t            compressionType,
+                                         uint64_t           compressedSize,
+                                         const std::string& compressedHash,
+                                         const std::string& customData) ORTHANC_OVERRIDE
+    {
+      OrthancPluginDatabaseEvent2 event;
+      event.type = OrthancPluginDatabaseEventType_DeletedAttachment;
+      event.content.attachment.uuid = StoreString(uuid);
+      event.content.attachment.contentType = contentType;
+      event.content.attachment.uncompressedSize = uncompressedSize;
+      event.content.attachment.uncompressedHash = StoreString(uncompressedHash);
+      event.content.attachment.compressionType = compressionType;
+      event.content.attachment.compressedSize = compressedSize;
+      event.content.attachment.compressedHash = StoreString(compressedHash);
+      event.content.attachment.customData = StoreString(customData);
+        
+      events_.push_back(event);
+    }
+    
+    
+    virtual void SignalDeletedResource(const std::string& publicId,
+                                       OrthancPluginResourceType resourceType) ORTHANC_OVERRIDE
+    {
+      OrthancPluginDatabaseEvent2 event;
+      event.type = OrthancPluginDatabaseEventType_DeletedResource;
+      event.content.resource.level = resourceType;
+      event.content.resource.publicId = StoreString(publicId);
+        
+      events_.push_back(event);
+    }
+    
+
+    virtual void SignalRemainingAncestor(const std::string& ancestorId,
+                                         OrthancPluginResourceType ancestorType) ORTHANC_OVERRIDE
+    {
+      OrthancPluginDatabaseEvent2 event;
+      event.type = OrthancPluginDatabaseEventType_RemainingAncestor;
+      event.content.resource.level = ancestorType;
+      event.content.resource.publicId = StoreString(ancestorId);
+        
+      events_.push_back(event);
+    }
+    
+    
+    virtual void AnswerAttachment(const std::string& uuid,
+                                  int32_t            contentType,
+                                  uint64_t           uncompressedSize,
+                                  const std::string& uncompressedHash,
+                                  int32_t            compressionType,
+                                  uint64_t           compressedSize,
+                                  const std::string& compressedHash,
+                                  const std::string& customData) ORTHANC_OVERRIDE
+    {
+      SetupAnswerType(_OrthancPluginDatabaseAnswerType_Attachment);
+
+      OrthancPluginAttachment2 attachment;
+      attachment.uuid = StoreString(uuid);
+      attachment.contentType = contentType;
+      attachment.uncompressedSize = uncompressedSize;
+      attachment.uncompressedHash = StoreString(uncompressedHash);
+      attachment.compressionType = compressionType;
+      attachment.compressedSize = compressedSize;
+      attachment.compressedHash = StoreString(compressedHash);
+      attachment.customData = StoreString(customData);
+
+      attachments_.push_back(attachment);
+    }
+    
+
+    virtual void AnswerChange(int64_t                    seq,
+                              int32_t                    changeType,
+                              OrthancPluginResourceType  resourceType,
+                              const std::string&         publicId,
+                              const std::string&         date) ORTHANC_OVERRIDE
+    {
+      SetupAnswerType(_OrthancPluginDatabaseAnswerType_Change);
+
+      OrthancPluginChange change;
+      change.seq = seq;
+      change.changeType = changeType;
+      change.resourceType = resourceType;
+      change.publicId = StoreString(publicId);
+      change.date = StoreString(date);
+
+      changes_.push_back(change);
+    }
+    
+
+    virtual void AnswerDicomTag(uint16_t group,
+                                uint16_t element,
+                                const std::string& value) ORTHANC_OVERRIDE
+    {
+      SetupAnswerType(_OrthancPluginDatabaseAnswerType_DicomTag);
+
+      OrthancPluginDicomTag tag;
+      tag.group = group;
+      tag.element = element;
+      tag.value = StoreString(value);
+
+      tags_.push_back(tag);      
+    }
+    
+
+    virtual void AnswerExportedResource(int64_t                    seq,
+                                        OrthancPluginResourceType  resourceType,
+                                        const std::string&         publicId,
+                                        const std::string&         modality,
+                                        const std::string&         date,
+                                        const std::string&         patientId,
+                                        const std::string&         studyInstanceUid,
+                                        const std::string&         seriesInstanceUid,
+                                        const std::string&         sopInstanceUid) ORTHANC_OVERRIDE
+    {
+      SetupAnswerType(_OrthancPluginDatabaseAnswerType_ExportedResource);
+
+      OrthancPluginExportedResource exported;
+      exported.seq = seq;
+      exported.resourceType = resourceType;
+      exported.publicId = StoreString(publicId);
+      exported.modality = StoreString(modality);
+      exported.date = StoreString(date);
+      exported.patientId = StoreString(patientId);
+      exported.studyInstanceUid = StoreString(studyInstanceUid);
+      exported.seriesInstanceUid = StoreString(seriesInstanceUid);
+      exported.sopInstanceUid = StoreString(sopInstanceUid);
+  
+      exported_.push_back(exported);
+    }
+
+    
+    virtual void AnswerMatchingResource(const std::string& resourceId) ORTHANC_OVERRIDE
+    {
+      SetupAnswerType(_OrthancPluginDatabaseAnswerType_MatchingResource);
+
+      OrthancPluginMatchingResource match;
+      match.resourceId = StoreString(resourceId);
+      match.someInstanceId = NULL;
+        
+      matches_.push_back(match);
+    }
+    
+    
+    virtual void AnswerMatchingResource(const std::string& resourceId,
+                                        const std::string& someInstanceId) ORTHANC_OVERRIDE
+    {
+      SetupAnswerType(_OrthancPluginDatabaseAnswerType_MatchingResource);
+
+      OrthancPluginMatchingResource match;
+      match.resourceId = StoreString(resourceId);
+      match.someInstanceId = StoreString(someInstanceId);
+        
+      matches_.push_back(match);
+    }
+
+
+    void AnswerIntegers32(const std::list<int32_t>& values)
+    {
+      SetupAnswerType(_OrthancPluginDatabaseAnswerType_Int32);
+      CopyListToVector(integers32_, values);
+    }
+
+    
+    void AnswerIntegers64(const std::list<int64_t>& values)
+    {
+      SetupAnswerType(_OrthancPluginDatabaseAnswerType_Int64);
+      CopyListToVector(integers64_, values);
+    }
+
+
+    void AnswerInteger64(int64_t value)
+    {
+      SetupAnswerType(_OrthancPluginDatabaseAnswerType_Int64);
+
+      integers64_.resize(1);
+      integers64_[0] = value;
+    }
+
+
+    void AnswerMetadata(int32_t metadata,
+                        const std::string& value)
+    {
+      SetupAnswerType(_OrthancPluginDatabaseAnswerType_Metadata);
+
+      Metadata tmp;
+      tmp.metadata = metadata;
+      tmp.value = StoreString(value);
+
+      metadata_.push_back(tmp);
+    }
+
+
+    void AnswerStrings(const std::list<std::string>& values)
+    {
+      SetupAnswerType(_OrthancPluginDatabaseAnswerType_String);
+      CopyListToVector(stringAnswers_, values);
+    }
+
+
+    void AnswerString(const std::string& value)
+    {
+      SetupAnswerType(_OrthancPluginDatabaseAnswerType_String);
+
+      if (stringAnswers_.empty())
+      {
+        stringAnswers_.push_back(value);
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+    }
+  };
+
+
+  IDatabaseBackendOutput* DatabaseBackendAdapterV4::Factory::CreateOutput()
+  {
+    return new DatabaseBackendAdapterV4::Output;
+  }
+
+
+  class DatabaseBackendAdapterV4::Transaction : public boost::noncopyable
+  {
+  private:
+    Adapter&   adapter_;
+    std::unique_ptr<Adapter::DatabaseAccessor>  accessor_;
+    std::unique_ptr<Output>    output_;
+    
+  public:
+    Transaction(Adapter& adapter) :
+      adapter_(adapter),
+      accessor_(new Adapter::DatabaseAccessor(adapter)),
+      output_(new Output)
+    {
+    }
+
+    ~Transaction()
+    {
+    }
+
+    IndexBackend& GetBackend() const
+    {
+      return accessor_->GetBackend();
+    }
+
+    Output& GetOutput() const
+    {
+      return *output_;
+    }
+
+    DatabaseManager& GetManager() const
+    {
+      return accessor_->GetManager();
+    }
+  };
+
+  
+  static OrthancPluginErrorCode ReadAnswersCount(OrthancPluginDatabaseTransaction* transaction,
+                                                 uint32_t* target /* out */)
+  {
+    assert(target != NULL);
+    const DatabaseBackendAdapterV4::Transaction& that = *reinterpret_cast<const DatabaseBackendAdapterV4::Transaction*>(transaction);
+    return that.GetOutput().ReadAnswersCount(*target);
+  }
+
+
+  static OrthancPluginErrorCode ReadAnswerAttachment2(OrthancPluginDatabaseTransaction* transaction,
+                                                      OrthancPluginAttachment2* target /* out */,
+                                                      uint32_t index)
+  {
+    assert(target != NULL);
+    const DatabaseBackendAdapterV4::Transaction& that = *reinterpret_cast<const DatabaseBackendAdapterV4::Transaction*>(transaction);
+    return that.GetOutput().ReadAnswerAttachment2(*target, index);
+  }
+
+
+  static OrthancPluginErrorCode ReadAnswerChange(OrthancPluginDatabaseTransaction* transaction,
+                                                 OrthancPluginChange* target /* out */,
+                                                 uint32_t index)
+  {
+    assert(target != NULL);
+    const DatabaseBackendAdapterV4::Transaction& that = *reinterpret_cast<const DatabaseBackendAdapterV4::Transaction*>(transaction);
+    return that.GetOutput().ReadAnswerChange(*target, index);
+  }
+
+
+  static OrthancPluginErrorCode ReadAnswerDicomTag(OrthancPluginDatabaseTransaction* transaction,
+                                                   uint16_t* group,
+                                                   uint16_t* element,
+                                                   const char** value,
+                                                   uint32_t index)
+  {
+    assert(group != NULL);
+    assert(element != NULL);
+    assert(value != NULL);
+    const DatabaseBackendAdapterV4::Transaction& that = *reinterpret_cast<const DatabaseBackendAdapterV4::Transaction*>(transaction);
+    return that.GetOutput().ReadAnswerDicomTag(*group, *element, *value, index);
+  }
+
+
+  static OrthancPluginErrorCode ReadAnswerExportedResource(OrthancPluginDatabaseTransaction* transaction,
+                                                           OrthancPluginExportedResource* target /* out */,
+                                                           uint32_t index)
+  {
+    assert(target != NULL);
+    const DatabaseBackendAdapterV4::Transaction& that = *reinterpret_cast<const DatabaseBackendAdapterV4::Transaction*>(transaction);
+    return that.GetOutput().ReadAnswerExportedResource(*target, index);
+  }
+
+
+  static OrthancPluginErrorCode ReadAnswerInt32(OrthancPluginDatabaseTransaction* transaction,
+                                                int32_t* target,
+                                                uint32_t index)
+  {
+    assert(target != NULL);
+    const DatabaseBackendAdapterV4::Transaction& that = *reinterpret_cast<const DatabaseBackendAdapterV4::Transaction*>(transaction);
+    return that.GetOutput().ReadAnswerInt32(*target, index);
+  }
+
+
+  static OrthancPluginErrorCode ReadAnswerInt64(OrthancPluginDatabaseTransaction* transaction,
+                                                int64_t* target,
+                                                uint32_t index)
+  {
+    assert(target != NULL);
+    const DatabaseBackendAdapterV4::Transaction& that = *reinterpret_cast<const DatabaseBackendAdapterV4::Transaction*>(transaction);
+    return that.GetOutput().ReadAnswerInt64(*target, index);
+  }
+
+
+  static OrthancPluginErrorCode ReadAnswerMatchingResource(OrthancPluginDatabaseTransaction* transaction,
+                                                           OrthancPluginMatchingResource* target,
+                                                           uint32_t index)
+  {
+    assert(target != NULL);
+    const DatabaseBackendAdapterV4::Transaction& that = *reinterpret_cast<const DatabaseBackendAdapterV4::Transaction*>(transaction);
+    return that.GetOutput().ReadAnswerMatchingResource(*target, index);
+  }
+
+
+  static OrthancPluginErrorCode ReadAnswerMetadata(OrthancPluginDatabaseTransaction* transaction,
+                                                   int32_t* metadata,
+                                                   const char** value,
+                                                   uint32_t index)
+  {
+    assert(metadata != NULL);
+    assert(value != NULL);
+    const DatabaseBackendAdapterV4::Transaction& that = *reinterpret_cast<const DatabaseBackendAdapterV4::Transaction*>(transaction);
+    return that.GetOutput().ReadAnswerMetadata(*metadata, *value, index);
+  }
+
+
+  static OrthancPluginErrorCode ReadAnswerString(OrthancPluginDatabaseTransaction* transaction,
+                                                 const char** target,
+                                                 uint32_t index)
+  {
+    assert(target != NULL);
+    const DatabaseBackendAdapterV4::Transaction& that = *reinterpret_cast<const DatabaseBackendAdapterV4::Transaction*>(transaction);
+    return that.GetOutput().ReadAnswerString(*target, index);
+  }
+
+
+  static OrthancPluginErrorCode ReadEventsCount(OrthancPluginDatabaseTransaction* transaction,
+                                                uint32_t* target /* out */)
+  {
+    assert(target != NULL);
+    const DatabaseBackendAdapterV4::Transaction& that = *reinterpret_cast<const DatabaseBackendAdapterV4::Transaction*>(transaction);
+    return that.GetOutput().ReadEventsCount(*target);
+  }
+
+    
+  static OrthancPluginErrorCode ReadEvent2(OrthancPluginDatabaseTransaction* transaction,
+                                           OrthancPluginDatabaseEvent2* event /* out */,
+                                           uint32_t index)
+  {
+    assert(event != NULL);
+    const DatabaseBackendAdapterV4::Transaction& that = *reinterpret_cast<const DatabaseBackendAdapterV4::Transaction*>(transaction);
+    return that.GetOutput().ReadEvent2(*event, index);
+  }
+
+    
+  static OrthancPluginErrorCode Open(void* database)
+  {
+    DatabaseBackendAdapterV4::Adapter* adapter = reinterpret_cast<DatabaseBackendAdapterV4::Adapter*>(database);
+
+    try
+    {
+      adapter->OpenConnections();
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(adapter->GetContext());
+  }
+
+  
+  static OrthancPluginErrorCode Close(void* database)
+  {
+    DatabaseBackendAdapterV4::Adapter* adapter = reinterpret_cast<DatabaseBackendAdapterV4::Adapter*>(database);
+
+    try
+    {
+      adapter->CloseConnections();
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(adapter->GetContext());
+  }
+
+  
+  static OrthancPluginErrorCode DestructDatabase(void* database)
+  {
+    DatabaseBackendAdapterV4::Adapter* adapter = reinterpret_cast<DatabaseBackendAdapterV4::Adapter*>(database);
+
+    if (adapter == NULL)
+    {
+      return OrthancPluginErrorCode_InternalError;
+    }
+    else
+    {
+      if (isBackendInUse_)
+      {
+        isBackendInUse_ = false;
+      }
+      else
+      {
+        OrthancPluginLogError(adapter->GetContext(), "More than one index backend was registered, internal error");
+      }
+      
+      delete adapter;
+
+      return OrthancPluginErrorCode_Success;
+    }
+  }
+
+  
+  static OrthancPluginErrorCode GetDatabaseVersion(void* database,
+                                                   uint32_t* version)
+  {
+    DatabaseBackendAdapterV4::Adapter* adapter = reinterpret_cast<DatabaseBackendAdapterV4::Adapter*>(database);
+      
+    try
+    {
+      DatabaseBackendAdapterV4::Adapter::DatabaseAccessor accessor(*adapter);
+      *version = accessor.GetBackend().GetDatabaseVersion(accessor.GetManager());
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(adapter->GetContext());
+  }
+
+
+  static OrthancPluginErrorCode UpgradeDatabase(void* database,
+                                                OrthancPluginStorageArea* storageArea,
+                                                uint32_t  targetVersion)
+  {
+    DatabaseBackendAdapterV4::Adapter* adapter = reinterpret_cast<DatabaseBackendAdapterV4::Adapter*>(database);
+      
+    try
+    {
+      DatabaseBackendAdapterV4::Adapter::DatabaseAccessor accessor(*adapter);
+      accessor.GetBackend().UpgradeDatabase(accessor.GetManager(), targetVersion, storageArea);
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(adapter->GetContext());
+  }
+
+
+  static OrthancPluginErrorCode HasRevisionsSupport(void* database,
+                                                    uint8_t* target)
+  {
+    DatabaseBackendAdapterV4::Adapter* adapter = reinterpret_cast<DatabaseBackendAdapterV4::Adapter*>(database);
+      
+    try
+    {
+      DatabaseBackendAdapterV4::Adapter::DatabaseAccessor accessor(*adapter);
+      *target = (accessor.GetBackend().HasRevisionsSupport() ? 1 : 0);
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(adapter->GetContext());
+  }
+
+
+  static OrthancPluginErrorCode HasAttachmentCustomDataSupport(void* database,
+                                                               uint8_t* target)
+  {
+    DatabaseBackendAdapterV4::Adapter* adapter = reinterpret_cast<DatabaseBackendAdapterV4::Adapter*>(database);
+      
+    try
+    {
+      DatabaseBackendAdapterV4::Adapter::DatabaseAccessor accessor(*adapter);
+      *target = (accessor.GetBackend().HasAttachmentCustomDataSupport() ? 1 : 0);
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(adapter->GetContext());
+  }
+
+  static OrthancPluginErrorCode StartTransaction(void* database,
+                                                 OrthancPluginDatabaseTransaction** target /* out */,
+                                                 OrthancPluginDatabaseTransactionType type)
+  {
+    DatabaseBackendAdapterV4::Adapter* adapter = reinterpret_cast<DatabaseBackendAdapterV4::Adapter*>(database);
+      
+    try
+    {
+      std::unique_ptr<DatabaseBackendAdapterV4::Transaction> transaction(new DatabaseBackendAdapterV4::Transaction(*adapter));
+      
+      switch (type)
+      {
+        case OrthancPluginDatabaseTransactionType_ReadOnly:
+          transaction->GetManager().StartTransaction(TransactionType_ReadOnly);
+          break;
+
+        case OrthancPluginDatabaseTransactionType_ReadWrite:
+          transaction->GetManager().StartTransaction(TransactionType_ReadWrite);
+          break;
+
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      }
+      
+      *target = reinterpret_cast<OrthancPluginDatabaseTransaction*>(transaction.release());
+      
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(adapter->GetContext());
+  }
+
+  
+  static OrthancPluginErrorCode DestructTransaction(OrthancPluginDatabaseTransaction* transaction)
+  {
+    if (transaction == NULL)
+    {
+      return OrthancPluginErrorCode_NullPointer;
+    }
+    else
+    {
+      delete reinterpret_cast<DatabaseBackendAdapterV4::Transaction*>(transaction);
+      return OrthancPluginErrorCode_Success;
+    }
+  }
+
+  
+  static OrthancPluginErrorCode Rollback(OrthancPluginDatabaseTransaction* transaction)
+  {
+    DatabaseBackendAdapterV4::Transaction* t = reinterpret_cast<DatabaseBackendAdapterV4::Transaction*>(transaction);
+
+    try
+    {
+      t->GetOutput().Clear();
+      t->GetManager().RollbackTransaction();
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(t->GetBackend().GetContext());
+  }
+
+  
+  static OrthancPluginErrorCode Commit(OrthancPluginDatabaseTransaction* transaction,
+                                       int64_t fileSizeDelta /* TODO - not used? */)
+  {
+    DatabaseBackendAdapterV4::Transaction* t = reinterpret_cast<DatabaseBackendAdapterV4::Transaction*>(transaction);
+
+    try
+    {
+      t->GetOutput().Clear();
+      t->GetManager().CommitTransaction();
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(t->GetBackend().GetContext());
+  }
+  
+
+  static OrthancPluginErrorCode AddAttachment2(OrthancPluginDatabaseTransaction* transaction,
+                                               int64_t id,
+                                               const OrthancPluginAttachment2* attachment,
+                                               int64_t revision)
+  {
+    DatabaseBackendAdapterV4::Transaction* t = reinterpret_cast<DatabaseBackendAdapterV4::Transaction*>(transaction);
+
+    try
+    {
+      t->GetOutput().Clear();
+      t->GetBackend().AddAttachment2(t->GetManager(), id, *attachment, revision);
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(t->GetBackend().GetContext());
+  }
+
+  
+  static OrthancPluginErrorCode ClearChanges(OrthancPluginDatabaseTransaction* transaction)
+  {
+    DatabaseBackendAdapterV4::Transaction* t = reinterpret_cast<DatabaseBackendAdapterV4::Transaction*>(transaction);
+
+    try
+    {
+      t->GetOutput().Clear();
+      t->GetBackend().ClearChanges(t->GetManager());
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(t->GetBackend().GetContext());
+  }
+
+  
+  static OrthancPluginErrorCode ClearExportedResources(OrthancPluginDatabaseTransaction* transaction)
+  {
+    DatabaseBackendAdapterV4::Transaction* t = reinterpret_cast<DatabaseBackendAdapterV4::Transaction*>(transaction);
+
+    try
+    {
+      t->GetOutput().Clear();
+      t->GetBackend().ClearExportedResources(t->GetManager());
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(t->GetBackend().GetContext());
+  }
+
+  
+  static OrthancPluginErrorCode ClearMainDicomTags(OrthancPluginDatabaseTransaction* transaction,
+                                                   int64_t resourceId)
+  {
+    DatabaseBackendAdapterV4::Transaction* t = reinterpret_cast<DatabaseBackendAdapterV4::Transaction*>(transaction);
+
+    try
+    {
+      t->GetOutput().Clear();
+      t->GetBackend().ClearMainDicomTags(t->GetManager(), resourceId);
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(t->GetBackend().GetContext());
+  }
+
+  
+  static OrthancPluginErrorCode CreateInstance(OrthancPluginDatabaseTransaction* transaction,
+                                               OrthancPluginCreateInstanceResult* target /* out */,
+                                               const char* hashPatient,
+                                               const char* hashStudy,
+                                               const char* hashSeries,
+                                               const char* hashInstance)
+  {
+    DatabaseBackendAdapterV4::Transaction* t = reinterpret_cast<DatabaseBackendAdapterV4::Transaction*>(transaction);
+
+    try
+    {
+      t->GetOutput().Clear();
+
+      if (t->GetBackend().HasCreateInstance())
+      {
+        t->GetBackend().CreateInstance(*target, t->GetManager(), hashPatient, hashStudy, hashSeries, hashInstance);
+      }
+      else
+      {
+        t->GetBackend().CreateInstanceGeneric(*target, t->GetManager(), hashPatient, hashStudy, hashSeries, hashInstance);
+      }
+      
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(t->GetBackend().GetContext());
+  }
+
+  
+  static OrthancPluginErrorCode DeleteAttachment(OrthancPluginDatabaseTransaction* transaction,
+                                                 int64_t id,
+                                                 int32_t contentType)
+  {
+    DatabaseBackendAdapterV4::Transaction* t = reinterpret_cast<DatabaseBackendAdapterV4::Transaction*>(transaction);
+
+    try
+    {
+      t->GetOutput().Clear();
+      t->GetBackend().DeleteAttachment(t->GetOutput(), t->GetManager(), id, contentType);
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(t->GetBackend().GetContext());
+  }
+
+  
+  static OrthancPluginErrorCode DeleteMetadata(OrthancPluginDatabaseTransaction* transaction,
+                                               int64_t id,
+                                               int32_t metadataType)
+  {
+    DatabaseBackendAdapterV4::Transaction* t = reinterpret_cast<DatabaseBackendAdapterV4::Transaction*>(transaction);
+
+    try
+    {
+      t->GetOutput().Clear();
+      t->GetBackend().DeleteMetadata(t->GetManager(), id, metadataType);
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(t->GetBackend().GetContext());
+  }
+
+  
+  static OrthancPluginErrorCode DeleteResource(OrthancPluginDatabaseTransaction* transaction,
+                                               int64_t id)
+  {
+    DatabaseBackendAdapterV4::Transaction* t = reinterpret_cast<DatabaseBackendAdapterV4::Transaction*>(transaction);
+
+    try
+    {
+      t->GetOutput().Clear();
+      t->GetBackend().DeleteResource(t->GetOutput(), t->GetManager(), id);
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(t->GetBackend().GetContext());
+  }
+
+  
+  static OrthancPluginErrorCode GetAllMetadata(OrthancPluginDatabaseTransaction* transaction,
+                                               int64_t id)
+  {
+    DatabaseBackendAdapterV4::Transaction* t = reinterpret_cast<DatabaseBackendAdapterV4::Transaction*>(transaction);
+
+    try
+    {
+      t->GetOutput().Clear();
+
+      std::map<int32_t, std::string> values;
+      t->GetBackend().GetAllMetadata(values, t->GetManager(), id);
+
+      for (std::map<int32_t, std::string>::const_iterator it = values.begin(); it != values.end(); ++it)
+      {
+        t->GetOutput().AnswerMetadata(it->first, it->second);
+      }
+      
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(t->GetBackend().GetContext());
+  }
+
+  
+  static OrthancPluginErrorCode GetAllPublicIds(OrthancPluginDatabaseTransaction* transaction,
+                                                OrthancPluginResourceType resourceType)
+  {
+    DatabaseBackendAdapterV4::Transaction* t = reinterpret_cast<DatabaseBackendAdapterV4::Transaction*>(transaction);
+
+    try
+    {
+      t->GetOutput().Clear();
+
+      std::list<std::string> values;
+      t->GetBackend().GetAllPublicIds(values, t->GetManager(), resourceType);
+      t->GetOutput().AnswerStrings(values);
+      
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(t->GetBackend().GetContext());
+  }
+
+  
+  static OrthancPluginErrorCode GetAllPublicIdsWithLimit(OrthancPluginDatabaseTransaction* transaction,
+                                                         OrthancPluginResourceType resourceType,
+                                                         uint64_t since,
+                                                         uint64_t limit)
+  {
+    DatabaseBackendAdapterV4::Transaction* t = reinterpret_cast<DatabaseBackendAdapterV4::Transaction*>(transaction);
+
+    try
+    {
+      t->GetOutput().Clear();
+
+      std::list<std::string> values;
+      t->GetBackend().GetAllPublicIds(values, t->GetManager(), resourceType, since, limit);
+      t->GetOutput().AnswerStrings(values);
+      
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(t->GetBackend().GetContext());
+  }
+
+  
+  static OrthancPluginErrorCode GetChanges(OrthancPluginDatabaseTransaction* transaction,
+                                           uint8_t* targetDone /* out */,
+                                           int64_t since,
+                                           uint32_t maxResults)
+  {
+    DatabaseBackendAdapterV4::Transaction* t = reinterpret_cast<DatabaseBackendAdapterV4::Transaction*>(transaction);
+
+    try
+    {
+      t->GetOutput().Clear();
+
+      bool done;
+      t->GetBackend().GetChanges(t->GetOutput(), done, t->GetManager(), since, maxResults);
+      *targetDone = (done ? 1 : 0);
+      
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(t->GetBackend().GetContext());
+  }
+
+  
+  static OrthancPluginErrorCode GetChildrenInternalId(OrthancPluginDatabaseTransaction* transaction,
+                                                      int64_t id)
+  {
+    DatabaseBackendAdapterV4::Transaction* t = reinterpret_cast<DatabaseBackendAdapterV4::Transaction*>(transaction);
+
+    try
+    {
+      t->GetOutput().Clear();
+
+      std::list<int64_t> values;
+      t->GetBackend().GetChildrenInternalId(values, t->GetManager(), id);
+      t->GetOutput().AnswerIntegers64(values);
+        
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(t->GetBackend().GetContext());
+  }
+
+  
+  static OrthancPluginErrorCode GetChildrenMetadata(OrthancPluginDatabaseTransaction* transaction,
+                                                    int64_t resourceId,
+                                                    int32_t metadata)
+  {
+    DatabaseBackendAdapterV4::Transaction* t = reinterpret_cast<DatabaseBackendAdapterV4::Transaction*>(transaction);
+
+    try
+    {
+      t->GetOutput().Clear();
+
+      std::list<std::string> values;
+      t->GetBackend().GetChildrenMetadata(values, t->GetManager(), resourceId, metadata);
+      t->GetOutput().AnswerStrings(values);
+        
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(t->GetBackend().GetContext());
+  }
+
+  
+  static OrthancPluginErrorCode GetChildrenPublicId(OrthancPluginDatabaseTransaction* transaction,
+                                                    int64_t id)
+  {
+    DatabaseBackendAdapterV4::Transaction* t = reinterpret_cast<DatabaseBackendAdapterV4::Transaction*>(transaction);
+
+    try
+    {
+      t->GetOutput().Clear();
+
+      std::list<std::string> values;
+      t->GetBackend().GetChildrenPublicId(values, t->GetManager(), id);
+      t->GetOutput().AnswerStrings(values);
+        
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(t->GetBackend().GetContext());
+  }
+
+  
+  static OrthancPluginErrorCode GetExportedResources(OrthancPluginDatabaseTransaction* transaction,
+                                                     uint8_t* targetDone /* out */,
+                                                     int64_t since,
+                                                     uint32_t maxResults)
+  {
+    DatabaseBackendAdapterV4::Transaction* t = reinterpret_cast<DatabaseBackendAdapterV4::Transaction*>(transaction);
+
+    try
+    {
+      t->GetOutput().Clear();
+
+      bool done;
+      t->GetBackend().GetExportedResources(t->GetOutput(), done, t->GetManager(), since, maxResults);
+      *targetDone = (done ? 1 : 0);
+        
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(t->GetBackend().GetContext());
+  }
+
+  
+  static OrthancPluginErrorCode GetLastChange(OrthancPluginDatabaseTransaction* transaction)
+  {
+    DatabaseBackendAdapterV4::Transaction* t = reinterpret_cast<DatabaseBackendAdapterV4::Transaction*>(transaction);
+
+    try
+    {
+      t->GetOutput().Clear();
+      t->GetBackend().GetLastChange(t->GetOutput(), t->GetManager());
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(t->GetBackend().GetContext());
+  }
+
+  
+  static OrthancPluginErrorCode GetLastChangeIndex(OrthancPluginDatabaseTransaction* transaction,
+                                                   int64_t* target)
+  {
+    DatabaseBackendAdapterV4::Transaction* t = reinterpret_cast<DatabaseBackendAdapterV4::Transaction*>(transaction);
+
+    try
+    {
+      t->GetOutput().Clear();
+      *target = t->GetBackend().GetLastChangeIndex(t->GetManager());
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(t->GetBackend().GetContext());
+  }
+
+  
+  static OrthancPluginErrorCode GetLastExportedResource(OrthancPluginDatabaseTransaction* transaction)
+  {
+    DatabaseBackendAdapterV4::Transaction* t = reinterpret_cast<DatabaseBackendAdapterV4::Transaction*>(transaction);
+
+    try
+    {
+      t->GetOutput().Clear();
+      t->GetBackend().GetLastExportedResource(t->GetOutput(), t->GetManager());
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(t->GetBackend().GetContext());
+  }
+
+  
+  static OrthancPluginErrorCode GetMainDicomTags(OrthancPluginDatabaseTransaction* transaction,
+                                                 int64_t id)
+  {
+    DatabaseBackendAdapterV4::Transaction* t = reinterpret_cast<DatabaseBackendAdapterV4::Transaction*>(transaction);
+
+    try
+    {
+      t->GetOutput().Clear();
+      t->GetBackend().GetMainDicomTags(t->GetOutput(), t->GetManager(), id);
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(t->GetBackend().GetContext());
+  }
+
+  
+  static OrthancPluginErrorCode GetPublicId(OrthancPluginDatabaseTransaction* transaction,
+                                            int64_t id)
+  {
+    DatabaseBackendAdapterV4::Transaction* t = reinterpret_cast<DatabaseBackendAdapterV4::Transaction*>(transaction);
+
+    try
+    {
+      t->GetOutput().Clear();
+      t->GetOutput().AnswerString(t->GetBackend().GetPublicId(t->GetManager(), id));
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(t->GetBackend().GetContext());
+  }
+
+
+  static OrthancPluginErrorCode GetResourcesCount(OrthancPluginDatabaseTransaction* transaction,
+                                                  uint64_t* target /* out */,
+                                                  OrthancPluginResourceType resourceType)
+  {
+    DatabaseBackendAdapterV4::Transaction* t = reinterpret_cast<DatabaseBackendAdapterV4::Transaction*>(transaction);
+
+    try
+    {
+      t->GetOutput().Clear();
+      *target = t->GetBackend().GetResourcesCount(t->GetManager(), resourceType);
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(t->GetBackend().GetContext());
+  }
+  
+
+  static OrthancPluginErrorCode GetResourceType(OrthancPluginDatabaseTransaction* transaction,
+                                                OrthancPluginResourceType* target /* out */,
+                                                uint64_t resourceId)
+  {
+    DatabaseBackendAdapterV4::Transaction* t = reinterpret_cast<DatabaseBackendAdapterV4::Transaction*>(transaction);
+
+    try
+    {
+      t->GetOutput().Clear();
+      *target = t->GetBackend().GetResourceType(t->GetManager(), resourceId);
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(t->GetBackend().GetContext());
+  }
+  
+
+  static OrthancPluginErrorCode GetTotalCompressedSize(OrthancPluginDatabaseTransaction* transaction,
+                                                       uint64_t* target /* out */)
+  {
+    DatabaseBackendAdapterV4::Transaction* t = reinterpret_cast<DatabaseBackendAdapterV4::Transaction*>(transaction);
+
+    try
+    {
+      t->GetOutput().Clear();
+      *target = t->GetBackend().GetTotalCompressedSize(t->GetManager());
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(t->GetBackend().GetContext());
+  }
+  
+
+  static OrthancPluginErrorCode GetTotalUncompressedSize(OrthancPluginDatabaseTransaction* transaction,
+                                                         uint64_t* target /* out */)
+  {
+    DatabaseBackendAdapterV4::Transaction* t = reinterpret_cast<DatabaseBackendAdapterV4::Transaction*>(transaction);
+
+    try
+    {
+      t->GetOutput().Clear();
+      *target = t->GetBackend().GetTotalUncompressedSize(t->GetManager());
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(t->GetBackend().GetContext());
+  }
+  
+
+  static OrthancPluginErrorCode IsDiskSizeAbove(OrthancPluginDatabaseTransaction* transaction,
+                                                uint8_t* target,
+                                                uint64_t threshold)
+  {
+    DatabaseBackendAdapterV4::Transaction* t = reinterpret_cast<DatabaseBackendAdapterV4::Transaction*>(transaction);
+
+    try
+    {
+      t->GetOutput().Clear();
+      bool above = (t->GetBackend().GetTotalCompressedSize(t->GetManager()) >= threshold);
+      *target = (above ? 1 : 0);
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(t->GetBackend().GetContext());
+  }
+  
+
+  static OrthancPluginErrorCode IsExistingResource(OrthancPluginDatabaseTransaction* transaction,
+                                                   uint8_t* target,
+                                                   int64_t resourceId)
+  {
+    DatabaseBackendAdapterV4::Transaction* t = reinterpret_cast<DatabaseBackendAdapterV4::Transaction*>(transaction);
+
+    try
+    {
+      t->GetOutput().Clear();
+      bool exists = t->GetBackend().IsExistingResource(t->GetManager(), resourceId);
+      *target = (exists ? 1 : 0);
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(t->GetBackend().GetContext());
+  }
+  
+
+  static OrthancPluginErrorCode IsProtectedPatient(OrthancPluginDatabaseTransaction* transaction,
+                                                   uint8_t* target,
+                                                   int64_t resourceId)
+  {
+    DatabaseBackendAdapterV4::Transaction* t = reinterpret_cast<DatabaseBackendAdapterV4::Transaction*>(transaction);
+
+    try
+    {
+      t->GetOutput().Clear();
+      bool isProtected = t->GetBackend().IsProtectedPatient(t->GetManager(), resourceId);
+      *target = (isProtected ? 1 : 0);
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(t->GetBackend().GetContext());
+  }
+  
+
+  static OrthancPluginErrorCode ListAvailableAttachments(OrthancPluginDatabaseTransaction* transaction,
+                                                         int64_t resourceId)
+  {
+    DatabaseBackendAdapterV4::Transaction* t = reinterpret_cast<DatabaseBackendAdapterV4::Transaction*>(transaction);
+
+    try
+    {
+      t->GetOutput().Clear();
+
+      std::list<int32_t> values;
+      t->GetBackend().ListAvailableAttachments(values, t->GetManager(), resourceId);
+      t->GetOutput().AnswerIntegers32(values);
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(t->GetBackend().GetContext());
+  }
+  
+
+  static OrthancPluginErrorCode LogChange(OrthancPluginDatabaseTransaction* transaction,
+                                          int32_t changeType,
+                                          int64_t resourceId,
+                                          OrthancPluginResourceType resourceType,
+                                          const char* date)
+  {
+    DatabaseBackendAdapterV4::Transaction* t = reinterpret_cast<DatabaseBackendAdapterV4::Transaction*>(transaction);
+
+    try
+    {
+      t->GetOutput().Clear();
+      t->GetBackend().LogChange(t->GetManager(), changeType, resourceId, resourceType, date);
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(t->GetBackend().GetContext());
+  }
+  
+
+  static OrthancPluginErrorCode LogExportedResource(OrthancPluginDatabaseTransaction* transaction,
+                                                    OrthancPluginResourceType resourceType,
+                                                    const char* publicId,
+                                                    const char* modality,
+                                                    const char* date,
+                                                    const char* patientId,
+                                                    const char* studyInstanceUid,
+                                                    const char* seriesInstanceUid,
+                                                    const char* sopInstanceUid)
+  {
+    DatabaseBackendAdapterV4::Transaction* t = reinterpret_cast<DatabaseBackendAdapterV4::Transaction*>(transaction);
+
+    try
+    {
+      OrthancPluginExportedResource exported;
+      exported.seq = 0;
+      exported.resourceType = resourceType;
+      exported.publicId = publicId;
+      exported.modality = modality;
+      exported.date = date;
+      exported.patientId = patientId;
+      exported.studyInstanceUid = studyInstanceUid;
+      exported.seriesInstanceUid = seriesInstanceUid;
+      exported.sopInstanceUid = sopInstanceUid;
+        
+      t->GetOutput().Clear();
+      t->GetBackend().LogExportedResource(t->GetManager(), exported);
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(t->GetBackend().GetContext());
+  }
+
+
+  static OrthancPluginErrorCode LookupAttachment(OrthancPluginDatabaseTransaction* transaction,
+                                                 int64_t* revision /* out */,
+                                                 int64_t resourceId,
+                                                 int32_t contentType)
+  {
+    DatabaseBackendAdapterV4::Transaction* t = reinterpret_cast<DatabaseBackendAdapterV4::Transaction*>(transaction);
+
+    try
+    {
+      t->GetOutput().Clear();
+      t->GetBackend().LookupAttachment(t->GetOutput(), *revision, t->GetManager(), resourceId, contentType);
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(t->GetBackend().GetContext());
+  }
+
+
+  static OrthancPluginErrorCode LookupGlobalProperty(OrthancPluginDatabaseTransaction* transaction,
+                                                     const char* serverIdentifier,
+                                                     int32_t property)
+  {
+    DatabaseBackendAdapterV4::Transaction* t = reinterpret_cast<DatabaseBackendAdapterV4::Transaction*>(transaction);
+
+    try
+    {
+      t->GetOutput().Clear();
+
+      std::string s;
+      if (t->GetBackend().LookupGlobalProperty(s, t->GetManager(), serverIdentifier, property))
+      {
+        t->GetOutput().AnswerString(s);
+      }
+      
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(t->GetBackend().GetContext());
+  }
+
+
+  static OrthancPluginErrorCode LookupMetadata(OrthancPluginDatabaseTransaction* transaction,
+                                               int64_t* revision /* out */,
+                                               int64_t id,
+                                               int32_t metadata)
+  {
+    DatabaseBackendAdapterV4::Transaction* t = reinterpret_cast<DatabaseBackendAdapterV4::Transaction*>(transaction);
+
+    try
+    {
+      t->GetOutput().Clear();
+
+      std::string s;
+      if (t->GetBackend().LookupMetadata(s, *revision, t->GetManager(), id, metadata))
+      {
+        t->GetOutput().AnswerString(s);
+      }
+      
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(t->GetBackend().GetContext());
+  }
+
+
+  static OrthancPluginErrorCode LookupParent(OrthancPluginDatabaseTransaction* transaction,
+                                             uint8_t* existing /* out */,
+                                             int64_t* parentId /* out */,
+                                             int64_t id)
+  {
+    DatabaseBackendAdapterV4::Transaction* t = reinterpret_cast<DatabaseBackendAdapterV4::Transaction*>(transaction);
+
+    try
+    {
+      t->GetOutput().Clear();
+
+      if (t->GetBackend().LookupParent(*parentId, t->GetManager(), id))
+      {
+        *existing = 1;
+      }
+      else
+      {
+        *existing = 0;
+      }
+      
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(t->GetBackend().GetContext());
+  }
+
+
+  static OrthancPluginErrorCode LookupResource(OrthancPluginDatabaseTransaction* transaction,
+                                               uint8_t* isExisting /* out */,
+                                               int64_t* id /* out */,
+                                               OrthancPluginResourceType* type /* out */,
+                                               const char* publicId)
+  {
+    DatabaseBackendAdapterV4::Transaction* t = reinterpret_cast<DatabaseBackendAdapterV4::Transaction*>(transaction);
+
+    try
+    {
+      t->GetOutput().Clear();
+
+      if (t->GetBackend().LookupResource(*id, *type, t->GetManager(), publicId))
+      {
+        *isExisting = 1;
+      }
+      else
+      {
+        *isExisting = 0;
+      }
+        
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(t->GetBackend().GetContext());
+  }
+
+
+  static OrthancPluginErrorCode LookupResources(OrthancPluginDatabaseTransaction* transaction,
+                                                uint32_t constraintsCount,
+                                                const OrthancPluginDatabaseConstraint* constraints,
+                                                OrthancPluginResourceType queryLevel,
+                                                uint32_t limit,
+                                                uint8_t requestSomeInstanceId)
+  {
+    DatabaseBackendAdapterV4::Transaction* t = reinterpret_cast<DatabaseBackendAdapterV4::Transaction*>(transaction);
+
+    try
+    {
+      t->GetOutput().Clear();
+
+      std::vector<Orthanc::DatabaseConstraint> lookup;
+      lookup.reserve(constraintsCount);
+
+      for (uint32_t i = 0; i < constraintsCount; i++)
+      {
+        lookup.push_back(Orthanc::DatabaseConstraint(constraints[i]));
+      }
+        
+      t->GetBackend().LookupResources(t->GetOutput(), t->GetManager(), lookup, queryLevel, limit, (requestSomeInstanceId != 0));
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(t->GetBackend().GetContext());
+  }
+
+
+  static OrthancPluginErrorCode LookupResourceAndParent(OrthancPluginDatabaseTransaction* transaction,
+                                                        uint8_t* isExisting /* out */,
+                                                        int64_t* id /* out */,
+                                                        OrthancPluginResourceType* type /* out */,
+                                                        const char* publicId)
+  {
+    DatabaseBackendAdapterV4::Transaction* t = reinterpret_cast<DatabaseBackendAdapterV4::Transaction*>(transaction);
+
+    try
+    {
+      t->GetOutput().Clear();
+
+      std::string parent;
+      if (t->GetBackend().LookupResourceAndParent(*id, *type, parent, t->GetManager(), publicId))
+      {
+        *isExisting = 1;
+
+        if (!parent.empty())
+        {
+          t->GetOutput().AnswerString(parent);
+        }
+      }
+      else
+      {
+        *isExisting = 0;
+      }
+      
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(t->GetBackend().GetContext());
+  }
+
+  
+  static OrthancPluginErrorCode SelectPatientToRecycle(OrthancPluginDatabaseTransaction* transaction,
+                                                       uint8_t* patientAvailable,
+                                                       int64_t* patientId)
+  {
+    DatabaseBackendAdapterV4::Transaction* t = reinterpret_cast<DatabaseBackendAdapterV4::Transaction*>(transaction);
+
+    try
+    {
+      t->GetOutput().Clear();
+      
+      if (t->GetBackend().SelectPatientToRecycle(*patientId, t->GetManager()))
+      {
+        *patientAvailable = 1;
+      }
+      else
+      {
+        *patientAvailable = 0;
+      }
+      
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(t->GetBackend().GetContext());
+  }
+
+    
+  static OrthancPluginErrorCode SelectPatientToRecycle2(OrthancPluginDatabaseTransaction* transaction,
+                                                        uint8_t* patientAvailable,
+                                                        int64_t* patientId,
+                                                        int64_t patientIdToAvoid)
+  {
+    DatabaseBackendAdapterV4::Transaction* t = reinterpret_cast<DatabaseBackendAdapterV4::Transaction*>(transaction);
+
+    try
+    {
+      t->GetOutput().Clear();
+      
+      if (t->GetBackend().SelectPatientToRecycle(*patientId, t->GetManager(), patientIdToAvoid))
+      {
+        *patientAvailable = 1;
+      }
+      else
+      {
+        *patientAvailable = 0;
+      }
+      
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(t->GetBackend().GetContext());
+  }
+
+    
+  static OrthancPluginErrorCode SetGlobalProperty(OrthancPluginDatabaseTransaction* transaction,
+                                                  const char* serverIdentifier,
+                                                  int32_t property,
+                                                  const char* value)
+  {
+    DatabaseBackendAdapterV4::Transaction* t = reinterpret_cast<DatabaseBackendAdapterV4::Transaction*>(transaction);
+
+    try
+    {
+      t->GetOutput().Clear();
+      t->GetBackend().SetGlobalProperty(t->GetManager(), serverIdentifier, property, value);
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(t->GetBackend().GetContext());
+  }
+
+    
+  static OrthancPluginErrorCode SetMetadata(OrthancPluginDatabaseTransaction* transaction,
+                                            int64_t id,
+                                            int32_t metadata,
+                                            const char* value,
+                                            int64_t revision)
+  {
+    DatabaseBackendAdapterV4::Transaction* t = reinterpret_cast<DatabaseBackendAdapterV4::Transaction*>(transaction);
+
+    try
+    {
+      t->GetOutput().Clear();
+      t->GetBackend().SetMetadata(t->GetManager(), id, metadata, value, revision);
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(t->GetBackend().GetContext());
+  }
+
+    
+  static OrthancPluginErrorCode SetProtectedPatient(OrthancPluginDatabaseTransaction* transaction,
+                                                    int64_t id,
+                                                    uint8_t isProtected)
+  {
+    DatabaseBackendAdapterV4::Transaction* t = reinterpret_cast<DatabaseBackendAdapterV4::Transaction*>(transaction);
+
+    try
+    {
+      t->GetOutput().Clear();
+      t->GetBackend().SetProtectedPatient(t->GetManager(), id, (isProtected != 0));
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(t->GetBackend().GetContext());
+  }
+
+    
+  static OrthancPluginErrorCode SetResourcesContent(OrthancPluginDatabaseTransaction* transaction,
+                                                    uint32_t countIdentifierTags,
+                                                    const OrthancPluginResourcesContentTags* identifierTags,
+                                                    uint32_t countMainDicomTags,
+                                                    const OrthancPluginResourcesContentTags* mainDicomTags,
+                                                    uint32_t countMetadata,
+                                                    const OrthancPluginResourcesContentMetadata* metadata)
+  {
+    DatabaseBackendAdapterV4::Transaction* t = reinterpret_cast<DatabaseBackendAdapterV4::Transaction*>(transaction);
+
+    try
+    {
+      t->GetOutput().Clear();
+      t->GetBackend().SetResourcesContent(t->GetManager(), countIdentifierTags, identifierTags,
+                                          countMainDicomTags, mainDicomTags, countMetadata, metadata);
+      return OrthancPluginErrorCode_Success;
+    }
+    ORTHANC_PLUGINS_DATABASE_CATCH(t->GetBackend().GetContext());
+  }
+
+    
+  void DatabaseBackendAdapterV4::Register(IndexBackend* backend,
+                                          size_t countConnections,
+                                          unsigned int maxDatabaseRetries)
+  {
+    if (isBackendInUse_)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+    
+    if (backend == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+
+    OrthancPluginDatabaseBackendV4 params;
+    memset(&params, 0, sizeof(params));
+
+    params.readAnswersCount = ReadAnswersCount;
+    params.readAnswerAttachment2 = ReadAnswerAttachment2;
+    params.readAnswerChange = ReadAnswerChange;
+    params.readAnswerDicomTag = ReadAnswerDicomTag;
+    params.readAnswerExportedResource = ReadAnswerExportedResource;
+    params.readAnswerInt32 = ReadAnswerInt32;
+    params.readAnswerInt64 = ReadAnswerInt64;
+    params.readAnswerMatchingResource = ReadAnswerMatchingResource;
+    params.readAnswerMetadata = ReadAnswerMetadata;
+    params.readAnswerString = ReadAnswerString;
+    
+    params.readEventsCount = ReadEventsCount;
+    params.readEvent2 = ReadEvent2;
+
+    params.open = Open;
+    params.close = Close;
+    params.destructDatabase = DestructDatabase;
+    params.getDatabaseVersion = GetDatabaseVersion;
+    params.upgradeDatabase = UpgradeDatabase;
+    params.hasRevisionsSupport = HasRevisionsSupport;
+    params.hasAttachmentCustomDataSupport = HasAttachmentCustomDataSupport;
+    params.startTransaction = StartTransaction;
+    params.destructTransaction = DestructTransaction;
+    params.rollback = Rollback;
+    params.commit = Commit;
+
+    params.addAttachment2 = AddAttachment2;
+    params.clearChanges = ClearChanges;
+    params.clearExportedResources = ClearExportedResources;
+    params.clearMainDicomTags = ClearMainDicomTags;
+    params.createInstance = CreateInstance;
+    params.deleteAttachment = DeleteAttachment;
+    params.deleteMetadata = DeleteMetadata;
+    params.deleteResource = DeleteResource;
+    params.getAllMetadata = GetAllMetadata;
+    params.getAllPublicIds = GetAllPublicIds;
+    params.getAllPublicIdsWithLimit = GetAllPublicIdsWithLimit;
+    params.getChanges = GetChanges;
+    params.getChildrenInternalId = GetChildrenInternalId;
+    params.getChildrenMetadata = GetChildrenMetadata;
+    params.getChildrenPublicId = GetChildrenPublicId;
+    params.getExportedResources = GetExportedResources;
+    params.getLastChange = GetLastChange;
+    params.getLastChangeIndex = GetLastChangeIndex;
+    params.getLastExportedResource = GetLastExportedResource;
+    params.getMainDicomTags = GetMainDicomTags;
+    params.getPublicId = GetPublicId;
+    params.getResourceType = GetResourceType;
+    params.getResourcesCount = GetResourcesCount;
+    params.getTotalCompressedSize = GetTotalCompressedSize;
+    params.getTotalUncompressedSize = GetTotalUncompressedSize;
+    params.isDiskSizeAbove = IsDiskSizeAbove;
+    params.isExistingResource = IsExistingResource;
+    params.isProtectedPatient = IsProtectedPatient;
+    params.listAvailableAttachments = ListAvailableAttachments;
+    params.logChange = LogChange;
+    params.logExportedResource = LogExportedResource;
+    params.lookupAttachment = LookupAttachment;
+    params.lookupGlobalProperty = LookupGlobalProperty;
+    params.lookupMetadata = LookupMetadata;
+    params.lookupParent = LookupParent;
+    params.lookupResource = LookupResource;
+    params.lookupResourceAndParent = LookupResourceAndParent;
+    params.lookupResources = LookupResources;
+    params.selectPatientToRecycle = SelectPatientToRecycle;
+    params.selectPatientToRecycle2 = SelectPatientToRecycle2;
+    params.setGlobalProperty = SetGlobalProperty;
+    params.setMetadata = SetMetadata;
+    params.setProtectedPatient = SetProtectedPatient;
+    params.setResourcesContent = SetResourcesContent;
+
+    OrthancPluginContext* context = backend->GetContext();
+ 
+    if (OrthancPluginRegisterDatabaseBackendV4(
+          context, &params, sizeof(params), maxDatabaseRetries,
+          new Adapter(backend, countConnections)) != OrthancPluginErrorCode_Success)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, "Unable to register the database backend");
+    }
+
+    backend->SetOutputFactory(new Factory);
+
+    isBackendInUse_ = true;
+  }
+
+
+  void DatabaseBackendAdapterV4::Finalize()
+  {
+    if (isBackendInUse_)
+    {
+      fprintf(stderr, "The Orthanc core has not destructed the index backend, internal error\n");
+    }
+  }
+}
+
+#  endif
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Plugins/DatabaseBackendAdapterV4.h	Thu Sep 15 18:12:34 2022 +0200
@@ -0,0 +1,69 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2021 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+
+#pragma once
+
+#include "IndexBackend.h"
+
+
+#if defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE)         // Macro introduced in Orthanc 1.3.1
+#  if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 0)
+
+namespace OrthancDatabases
+{  
+  /**
+   * @brief Bridge between C and C++ database engines.
+   * 
+   * Class creating the bridge between the C low-level primitives for
+   * custom database engines, and the high-level IDatabaseBackend C++
+   * interface, for Orthanc >= 1.12.0.
+   **/
+  class DatabaseBackendAdapterV4
+  {
+  private:
+    class Output;
+    
+    // This class cannot be instantiated
+    DatabaseBackendAdapterV4()
+    {
+    }
+
+  public:
+    class Adapter;
+    class Transaction;
+
+    class Factory : public IDatabaseBackendOutput::IFactory
+    {
+    public:
+      virtual IDatabaseBackendOutput* CreateOutput() ORTHANC_OVERRIDE;
+    };
+
+    static void Register(IndexBackend* backend,
+                         size_t countConnections,
+                         unsigned int maxDatabaseRetries);
+
+    static void Finalize();
+  };
+}
+
+#  endif
+#endif
--- a/Framework/Plugins/IDatabaseBackend.h	Tue Jul 05 08:44:26 2022 +0200
+++ b/Framework/Plugins/IDatabaseBackend.h	Thu Sep 15 18:12:34 2022 +0200
@@ -50,11 +50,21 @@
 
     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_VERSION_IS_ABOVE(1, 12, 0)
+
+    virtual void AddAttachment2(DatabaseManager& manager,
+                               int64_t id,
+                               const OrthancPluginAttachment2& attachment,
+                               int64_t revision) = 0;
+#endif
+
     virtual void AttachChild(DatabaseManager& manager,
                              int64_t parent,
                              int64_t child) = 0;
--- a/Framework/Plugins/IDatabaseBackendOutput.h	Tue Jul 05 08:44:26 2022 +0200
+++ b/Framework/Plugins/IDatabaseBackendOutput.h	Thu Sep 15 18:12:34 2022 +0200
@@ -54,7 +54,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;
@@ -68,7 +69,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	Tue Jul 05 08:44:26 2022 +0200
+++ b/Framework/Plugins/IndexBackend.cpp	Thu Sep 15 18:12:34 2022 +0200
@@ -27,6 +27,7 @@
 #include "../Common/Utf8StringValue.h"
 #include "DatabaseBackendAdapterV2.h"
 #include "DatabaseBackendAdapterV3.h"
+#include "DatabaseBackendAdapterV4.h"
 #include "GlobalProperties.h"
 
 #include <Compatibility.h>  // For std::unique_ptr<>
@@ -202,7 +203,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();
@@ -215,7 +216,8 @@
                                      statement.ReadString(3),
                                      statement.ReadInteger32(4),
                                      statement.ReadInteger64(5),
-                                     statement.ReadString(6));
+                                     statement.ReadString(6),
+                                     statement.ReadString(8));
       
       statement.Next();
     }
@@ -282,12 +284,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);
@@ -296,51 +311,45 @@
     statement.SetParameterType("compression", ValueType_Integer64);
     statement.SetParameterType("hash", ValueType_Utf8String);
     statement.SetParameterType("hash-compressed", ValueType_Utf8String);
+    statement.SetParameterType("revision", ValueType_Integer64);
+    statement.SetParameterType("custom-data", ValueType_Utf8String);
 
     args.SetIntegerValue("id", id);
-    args.SetIntegerValue("type", attachment.contentType);
-    args.SetUtf8Value("uuid", attachment.uuid);
-    args.SetIntegerValue("compressed", attachment.compressedSize);
-    args.SetIntegerValue("uncompressed", attachment.uncompressedSize);
-    args.SetIntegerValue("compression", attachment.compressionType);
-    args.SetUtf8Value("hash", attachment.uncompressedHash);
-    args.SetUtf8Value("hash-compressed", attachment.compressedHash);
+    args.SetIntegerValue("type", contentType);
+    args.SetUtf8Value("uuid", uuid);
+    args.SetIntegerValue("compressed", compressedSize);
+    args.SetIntegerValue("uncompressed", uncompressedSize);
+    args.SetIntegerValue("compression", compressionType);
+    args.SetUtf8Value("hash", uncompressedHash);
+    args.SetUtf8Value("hash-compressed", compressedHash);
+    args.SetIntegerValue("revision", revision);
+    args.SetUtf8Value("custom-data", customData);
 
     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 supports these features now
+    ExecuteAddAttachment(manager, id, attachment.uuid, attachment.contentType, attachment.uncompressedSize, attachment.uncompressedHash,
+                         attachment.compressionType, attachment.compressedSize, attachment.compressedHash, "", revision);
   }
 
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 0)
+  void IndexBackend::AddAttachment2(DatabaseManager& manager,
+                                   int64_t id,
+                                   const OrthancPluginAttachment2& attachment,
+                                   int64_t revision)
+  {
+    assert(HasRevisionsSupport() && HasAttachmentCustomDataSupport()); // all plugins supports these features now
+    ExecuteAddAttachment(manager, id, attachment.uuid, attachment.contentType, attachment.uncompressedSize, attachment.uncompressedHash,
+                         attachment.compressionType, attachment.compressedSize, attachment.compressedHash, attachment.customData, revision);
+  }
+#endif
     
   void IndexBackend::AttachChild(DatabaseManager& manager,
                                  int64_t parent,
@@ -1037,12 +1046,21 @@
     statement.Execute(args);
   }
 
-
-  static bool ExecuteLookupAttachment(DatabaseManager::CachedStatement& statement,
-                                      IDatabaseBackendOutput& output,
+    
+  /* Use GetOutput().AnswerAttachment() */
+  bool IndexBackend::LookupAttachment(IDatabaseBackendOutput& output,
+                                      int64_t& revision /*out*/,
+                                      DatabaseManager& manager,
                                       int64_t id,
                                       int32_t contentType)
   {
+    assert(HasRevisionsSupport() && HasAttachmentCustomDataSupport()); // we force 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);
@@ -1059,64 +1077,35 @@
     }
     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);
-    }
+
   }
 
 
@@ -2609,22 +2598,31 @@
       throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
     }
     
-    bool hasLoadedV3 = false;
+    bool hasLoadedV3OrAbove = false;
       
 #if defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE)         // Macro introduced in Orthanc 1.3.1
-#  if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 9, 2)
+#  if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 0)
+    if (OrthancPluginCheckVersionAdvanced(backend->GetContext(), 1, 12, 0) == 1)
+    {
+      LOG(WARNING) << "The index plugin will use " << countConnections << " connection(s) to the database, "
+                   << "and will retry up to " << maxDatabaseRetries << " time(s) in the case of a collision";
+      
+      OrthancDatabases::DatabaseBackendAdapterV4::Register(backend, countConnections, maxDatabaseRetries);
+      hasLoadedV3OrAbove = true;
+    }
+#  elif ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 9, 2)
     if (OrthancPluginCheckVersionAdvanced(backend->GetContext(), 1, 9, 2) == 1)
     {
       LOG(WARNING) << "The index plugin will use " << countConnections << " connection(s) to the database, "
                    << "and will retry up to " << maxDatabaseRetries << " time(s) in the case of a collision";
       
       OrthancDatabases::DatabaseBackendAdapterV3::Register(backend, countConnections, maxDatabaseRetries);
-      hasLoadedV3 = true;
+      hasLoadedV3OrAbove = true;
     }
 #  endif
 #endif
 
-    if (!hasLoadedV3)
+    if (!hasLoadedV3OrAbove)
     {
       LOG(WARNING) << "Performance warning: Your version of the Orthanc core or SDK doesn't support multiple readers/writers";
       OrthancDatabases::DatabaseBackendAdapterV2::Register(backend);
--- a/Framework/Plugins/IndexBackend.h	Tue Jul 05 08:44:26 2022 +0200
+++ b/Framework/Plugins/IndexBackend.h	Thu Sep 15 18:12:34 2022 +0200
@@ -85,7 +85,16 @@
                                int64_t id,
                                const OrthancPluginAttachment& attachment,
                                int64_t revision) ORTHANC_OVERRIDE;
-    
+
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 0)
+
+    virtual void AddAttachment2(DatabaseManager& manager,
+                               int64_t id,
+                               const OrthancPluginAttachment2& attachment,
+                               int64_t revision) ORTHANC_OVERRIDE;
+
+#endif
+
     virtual void AttachChild(DatabaseManager& manager,
                              int64_t parent,
                              int64_t child) ORTHANC_OVERRIDE;
--- a/MySQL/CMakeLists.txt	Tue Jul 05 08:44:26 2022 +0200
+++ b/MySQL/CMakeLists.txt	Thu Sep 15 18:12:34 2022 +0200
@@ -78,6 +78,9 @@
   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
   )
 
 add_custom_target(
--- a/MySQL/NEWS	Tue Jul 05 08:44:26 2022 +0200
+++ b/MySQL/NEWS	Thu Sep 15 18:12:34 2022 +0200
@@ -1,6 +1,9 @@
 Pending changes in the mainline
 ===============================
 
+* Added support for customData in AttachedFiles
+* Added support for revision in AttachedFiles & Metadata
+
 
 Release 4.3 (2021-07-22)
 ========================
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MySQL/Plugins/InstallRevisionAndCustomData.sql	Thu Sep 15 18:12:34 2022 +0200
@@ -0,0 +1,28 @@
+ALTER TABLE AttachedFiles ADD COLUMN revision INTEGER;
+ALTER TABLE DeletedFiles ADD COLUMN revision INTEGER;
+ALTER TABLE Metadata ADD COLUMN revision INTEGER;
+
+ALTER TABLE AttachedFiles ADD COLUMN customData LONGTEXT;
+ALTER TABLE DeletedFiles ADD COLUMN customData LONGTEXT;
+
+DROP TRIGGER AttachedFileDeleted;
+
+CREATE TRIGGER AttachedFileDeleted
+AFTER DELETE ON AttachedFiles
+FOR EACH ROW
+  BEGIN
+    INSERT INTO DeletedFiles VALUES(old.uuid, old.filetype, old.compressedSize,
+                                    old.uncompressedSize, old.compressionType,
+                                    old.uncompressedHash, old.compressedHash,
+                                    old.revision, old.customData)@
+  END;
+
+
+DROP TRIGGER ResourceDeleted;
+
+CREATE TRIGGER ResourceDeleted
+BEFORE DELETE ON Resources
+FOR EACH ROW
+  BEGIN
+    INSERT INTO DeletedFiles SELECT uuid, fileType, compressedSize, uncompressedSize, compressionType, uncompressedHash, compressedHash, revision, customData FROM AttachedFiles WHERE id=old.internalId@
+  END;
\ No newline at end of file
--- a/MySQL/Plugins/MySQLIndex.cpp	Tue Jul 05 08:44:26 2022 +0200
+++ b/MySQL/Plugins/MySQLIndex.cpp	Thu Sep 15 18:12:34 2022 +0200
@@ -294,7 +294,26 @@
         t.Commit();
       }
 
-      if (revision != 6)
+      if (revision == 6)      
+      {
+        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);
+
+        // Need to escape arobases: Don't use "t.GetDatabaseTransaction().ExecuteMultiLines()" here
+        db.ExecuteMultiLines(query, true);
+        
+        revision = 7;
+        SetGlobalIntegerProperty(manager, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabasePatchLevel, revision);
+
+        t.Commit();
+      }
+
+      if (revision != 7)
       {
         LOG(ERROR) << "MySQL plugin is incompatible with database schema revision: " << revision;
         throw Orthanc::OrthancException(Orthanc::ErrorCode_Database);        
--- a/MySQL/Plugins/MySQLIndex.h	Tue Jul 05 08:44:26 2022 +0200
+++ b/MySQL/Plugins/MySQLIndex.h	Thu Sep 15 18:12:34 2022 +0200
@@ -47,7 +47,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	Tue Jul 05 08:44:26 2022 +0200
+++ b/MySQL/Plugins/PrepareIndex.sql	Thu Sep 15 18:12:34 2022 +0200
@@ -48,6 +48,8 @@
        compressionType INTEGER,
        uncompressedHash VARCHAR(40),
        compressedHash VARCHAR(40),
+       -- revision INTEGER,          -- new in v 4.X, added in MySQLIndex::ConfigureDatabase
+       -- customData LONGTEXT,       -- new in v 4.X, added in MySQLIndex::ConfigureDatabase
        PRIMARY KEY(id, fileType),
        CONSTRAINT AttachedFiles1 FOREIGN KEY (id) REFERENCES Resources(internalId) ON DELETE CASCADE
        );              
@@ -104,6 +106,8 @@
        compressionType INTEGER,        -- 4
        uncompressedHash VARCHAR(40),   -- 5
        compressedHash VARCHAR(40)      -- 6
+       -- revision INTEGER,          -- new in v 4.X, added in MySQLIndex::ConfigureDatabase
+       -- customData LONGTEXT,       -- new in v 4.X, added in MySQLIndex::ConfigureDatabase
        );
 -- End of differences
 
@@ -119,6 +123,8 @@
   INSERT INTO DeletedFiles VALUES(old.uuid, old.filetype, old.compressedSize,
                                   old.uncompressedSize, old.compressionType,
                                   old.uncompressedHash, old.compressedHash)@
+                                  -- old.revision, old.customData    -- new in v 4.X, added in MySQLIndex::ConfigureDatabase
+
 END;
 
 
@@ -127,6 +133,7 @@
 FOR EACH ROW
 BEGIN
    INSERT INTO DeletedFiles SELECT uuid, fileType, compressedSize, uncompressedSize, compressionType, uncompressedHash, compressedHash FROM AttachedFiles WHERE id=old.internalId@
+                                  -- revision, customData    -- new in v 4.X, added in MySQLIndex::ConfigureDatabase
 END;
 
 
--- a/Odbc/NEWS	Tue Jul 05 08:44:26 2022 +0200
+++ b/Odbc/NEWS	Thu Sep 15 18:12:34 2022 +0200
@@ -1,3 +1,8 @@
+Pending changes in the mainline
+===============================
+
+* Added support for customData in AttachedFiles
+
 Release 1.1 (2021-12-06)
 ========================
 
--- a/Odbc/Plugins/OdbcIndex.cpp	Tue Jul 05 08:44:26 2022 +0200
+++ b/Odbc/Plugins/OdbcIndex.cpp	Thu Sep 15 18:12:34 2022 +0200
@@ -160,6 +160,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)
   {
@@ -185,46 +228,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);
@@ -238,6 +243,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	Tue Jul 05 08:44:26 2022 +0200
+++ b/Odbc/Plugins/OdbcIndex.h	Thu Sep 15 18:12:34 2022 +0200
@@ -62,6 +62,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/NEWS	Tue Jul 05 08:44:26 2022 +0200
+++ b/PostgreSQL/NEWS	Thu Sep 15 18:12:34 2022 +0200
@@ -1,6 +1,8 @@
 Pending changes in the mainline
 ===============================
 
+* Added support for customData in AttachedFiles
+
 
 Release 4.0 (2021-04-22)
 ========================
--- a/PostgreSQL/Plugins/PostgreSQLIndex.cpp	Tue Jul 05 08:44:26 2022 +0200
+++ b/PostgreSQL/Plugins/PostgreSQLIndex.cpp	Thu Sep 15 18:12:34 2022 +0200
@@ -294,6 +294,49 @@
           t.GetDatabaseTransaction().ExecuteMultiLines("ALTER TABLE AttachedFiles ADD COLUMN revision INTEGER");
         }
 
+        // new in v 4.X
+        if (!db.DoesColumnExist("DeletedFiles", "revision"))
+        {
+          t.GetDatabaseTransaction().ExecuteMultiLines("ALTER TABLE DeletedFiles ADD COLUMN revision INTEGER");
+        }
+
+        if (!db.DoesColumnExist("AttachedFiles", "customData"))
+        {
+          t.GetDatabaseTransaction().ExecuteMultiLines("ALTER TABLE AttachedFiles ADD COLUMN customData TEXT");
+        }
+
+        if (!db.DoesColumnExist("DeletedFiles", "customData"))
+        {
+          // add the column and modify the trigger
+          t.GetDatabaseTransaction().ExecuteMultiLines("ALTER TABLE DeletedFiles ADD COLUMN customData TEXT");
+
+          t.GetDatabaseTransaction().ExecuteMultiLines(
+            "DROP TRIGGER AttachedFileDeleted ON AttachedFiles");
+
+          t.GetDatabaseTransaction().ExecuteMultiLines(
+            "DROP FUNCTION AttachedFileDeletedFunc");
+
+          t.GetDatabaseTransaction().ExecuteMultiLines(
+            "CREATE FUNCTION AttachedFileDeletedFunc() "
+            "RETURNS TRIGGER AS $body$"
+            "BEGIN"
+            "  INSERT INTO DeletedFiles VALUES"
+            "    (old.uuid, old.filetype, old.compressedSize,"
+            "     old.uncompressedSize, old.compressionType,"
+            "     old.uncompressedHash, old.compressedHash,"
+            "     old.revision, old.customData);"
+            "  RETURN NULL;"
+            "END;"
+            "$body$ LANGUAGE plpgsql;");
+
+          t.GetDatabaseTransaction().ExecuteMultiLines(
+            "CREATE TRIGGER AttachedFileDeleted "
+            "AFTER DELETE ON AttachedFiles "
+            "FOR EACH ROW "
+            "EXECUTE PROCEDURE AttachedFileDeletedFunc();"
+          );
+        }
+
         t.Commit();
       }
     }
--- a/PostgreSQL/Plugins/PostgreSQLIndex.h	Tue Jul 05 08:44:26 2022 +0200
+++ b/PostgreSQL/Plugins/PostgreSQLIndex.h	Thu Sep 15 18:12:34 2022 +0200
@@ -50,6 +50,11 @@
       return true;
     }
     
+    virtual bool HasAttachmentCustomDataSupport() const ORTHANC_OVERRIDE
+    {
+      return true;
+    }
+
     virtual int64_t CreateResource(DatabaseManager& manager,
                                    const char* publicId,
                                    OrthancPluginResourceType type)
--- a/PostgreSQL/Plugins/PrepareIndex.sql	Tue Jul 05 08:44:26 2022 +0200
+++ b/PostgreSQL/Plugins/PrepareIndex.sql	Thu Sep 15 18:12:34 2022 +0200
@@ -30,6 +30,7 @@
        id BIGINT REFERENCES Resources(internalId) ON DELETE CASCADE,
        type INTEGER NOT NULL,
        value TEXT,
+       -- revision INTEGER,                   -- new in v4.0 (this column is added in PostgreSQLIndex::ConfigureDatabase)
        PRIMARY KEY(id, type)
        );
 
@@ -42,6 +43,8 @@
        compressionType INTEGER,
        uncompressedHash VARCHAR(40),
        compressedHash VARCHAR(40),
+       -- revision BIGINT,                    -- new in v 4.0 (this column is added in PostgreSQLIndex::ConfigureDatabase)
+       -- customData TEXT,                    -- new in v 4.X (this column is added in PostgreSQLIndex::ConfigureDatabase)
        PRIMARY KEY(id, fileType)
        );              
 
@@ -112,7 +115,9 @@
   INSERT INTO DeletedFiles VALUES
     (old.uuid, old.filetype, old.compressedSize,
      old.uncompressedSize, old.compressionType,
-     old.uncompressedHash, old.compressedHash);
+     old.uncompressedHash, old.compressedHash
+     -- old.customData                        -- new in v 4.X (this column is added in PostgreSQLIndex::ConfigureDatabase)
+     );
   RETURN NULL;
 END;
 $body$ LANGUAGE plpgsql;
--- a/README	Tue Jul 05 08:44:26 2022 +0200
+++ b/README	Thu Sep 15 18:12:34 2022 +0200
@@ -49,6 +49,69 @@
 https://hg.orthanc-server.com/orthanc-postgresql/
 
 
+Development
+-----------
+
+PostgreSQL
+==========
+
+To quickly start a test PG server:
+
+  docker run -p 5432:5432 --rm --env POSTGRES_HOST_AUTH_METHOD=trust postgres:13.4
+
+And use this Orthanc configuration:
+  "PostgreSQL": {
+    "EnableIndex": true,
+    "EnableStorage": false, // DICOM files are stored in the Orthanc container in /var/lib/orthanc/db/
+    "Host": "localhost", // the name of the PostgreSQL container
+    "Database": "postgres", // default database name in PostgreSQL container (no need to create it)
+    "Username": "postgres", // default user name in PostgreSQL container (no need to create it)
+    "Password": "postgres"
+  },
+
+MySQL
+=====
+
+To quickly start a test MySQL server:
+
+  docker run -p 3306:3306 --rm --env MYSQL_PASSWORD=orthanc --env MYSQL_USER=orthanc --env MYSQL_DATABASE=orthanc --env MYSQL_ROOT_PASSWORD=pwd-root mysql:8.0 mysqld --default-authentication-plugin=mysql_native_password --log-bin-trust-function-creators=1
+
+And use this Orthanc configuration:
+  "MySQL": {
+    "EnableIndex": true,
+    "EnableStorage": false,
+    "Host": "localhost",
+    "Database": "orthanc",
+    "Username": "orthanc",
+    "Password": "orthanc",
+    "UnixSocket": ""
+  },
+
+
+ODBC (SQL Server)
+=================
+
+To quickly start a test MySQL server:
+
+  docker run -e "ACCEPT_EULA=Y" --rm --env "SA_PASSWORD=yourStrong-Password" --entrypoint=bash -it -p 1433:1433 mcr.microsoft.com/mssql/server:2019-latest
+
+Then:  
+   (sleep 15s && /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P yourStrong-Password -Q 'CREATE DATABASE orthanctest') & /opt/mssql/bin/sqlservr
+
+And use this Orthanc configuration:
+  "Odbc" : {
+    "IndexConnectionString": "Driver={ODBC Driver 17 for SQL Server};Server=tcp:localhost,1433;Database=orthanctest;Uid=sa;Pwd=yourStrong-Password;Encrypt=yes;TrustServerCertificate=yes;Connection Timeout=30;",
+    "EnableIndex": true,
+    "EnableStorage": false
+  }
+
+
+SQLite
+======
+
+To quickly test the SQLite plugin, simply run orthanc and load the plugin (no configuration required).
+  
+
 Licensing
 ---------
 
--- a/Resources/CMake/DatabasesPluginConfiguration.cmake	Tue Jul 05 08:44:26 2022 +0200
+++ b/Resources/CMake/DatabasesPluginConfiguration.cmake	Thu Sep 15 18:12:34 2022 +0200
@@ -79,6 +79,7 @@
   ${ORTHANC_CORE_SOURCES}
   ${ORTHANC_DATABASES_ROOT}/Framework/Plugins/DatabaseBackendAdapterV2.cpp
   ${ORTHANC_DATABASES_ROOT}/Framework/Plugins/DatabaseBackendAdapterV3.cpp
+  ${ORTHANC_DATABASES_ROOT}/Framework/Plugins/DatabaseBackendAdapterV4.cpp
   ${ORTHANC_DATABASES_ROOT}/Framework/Plugins/IndexBackend.cpp
   ${ORTHANC_DATABASES_ROOT}/Framework/Plugins/StorageBackend.cpp
   ${ORTHANC_DATABASES_ROOT}/Resources/Orthanc/Databases/DatabaseConstraint.cpp
--- a/SQLite/CMakeLists.txt	Tue Jul 05 08:44:26 2022 +0200
+++ b/SQLite/CMakeLists.txt	Thu Sep 15 18:12:34 2022 +0200
@@ -42,6 +42,7 @@
 
 EmbedResources(
   SQLITE_PREPARE_INDEX ${CMAKE_SOURCE_DIR}/Plugins/PrepareIndex.sql
+  SQLITE_INSTALL_CUSTOM_DATA ${CMAKE_SOURCE_DIR}/Plugins/InstallCustomData.sql
   )
 
 add_custom_target(
--- a/SQLite/NEWS	Tue Jul 05 08:44:26 2022 +0200
+++ b/SQLite/NEWS	Thu Sep 15 18:12:34 2022 +0200
@@ -1,3 +1,9 @@
+Pending changes in the mainline
+===============================
+
+* Added support for customData in AttachedFiles
+
+
 Pending changes in the mainline
 ===============================
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/SQLite/Plugins/InstallCustomData.sql	Thu Sep 15 18:12:34 2022 +0200
@@ -0,0 +1,17 @@
+
+-- Add new column for customData
+ALTER TABLE AttachedFiles ADD COLUMN customData TEXT;
+ALTER TABLE DeletedFiles ADD COLUMN revision INTEGER;
+ALTER TABLE DeletedFiles ADD COLUMN customData TEXT;
+
+
+DROP TRIGGER AttachedFileDeleted;
+
+CREATE TRIGGER AttachedFileDeleted
+AFTER DELETE ON AttachedFiles
+BEGIN
+   INSERT INTO DeletedFiles VALUES(old.uuid, old.filetype, old.compressedSize,
+                                   old.uncompressedSize, old.compressionType,
+                                   old.uncompressedHash, old.compressedHash,
+                                   old.revision, old.customData);
+END;
--- a/SQLite/Plugins/SQLiteIndex.cpp	Tue Jul 05 08:44:26 2022 +0200
+++ b/SQLite/Plugins/SQLiteIndex.cpp	Thu Sep 15 18:12:34 2022 +0200
@@ -145,7 +145,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	Tue Jul 05 08:44:26 2022 +0200
+++ b/SQLite/Plugins/SQLiteIndex.h	Thu Sep 15 18:12:34 2022 +0200
@@ -51,6 +51,11 @@
       return true;
     }
     
+    bool HasAttachmentCustomDataSupport() const ORTHANC_OVERRIDE
+    {
+      return true;
+    }
+
     virtual int64_t CreateResource(DatabaseManager& manager,
                                    const char* publicId,
                                    OrthancPluginResourceType type) ORTHANC_OVERRIDE;