changeset 5083:75e949689c08 attach-custom-data

PluginDatabaseV4 to handle customData
author Alain Mazy <am@osimis.io>
date Wed, 14 Sep 2022 17:11:45 +0200
parents 4af5f496a0dd
children 9770d537880d
files OrthancServer/CMakeLists.txt OrthancServer/Plugins/Engine/OrthancPluginDatabase.h OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.h OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h OrthancServer/Plugins/Engine/OrthancPlugins.cpp OrthancServer/Plugins/Include/orthanc/OrthancCDatabasePlugin.h OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h OrthancServer/Sources/Database/IDatabaseWrapper.h OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h TODO
diffstat 11 files changed, 1820 insertions(+), 7 deletions(-) [+]
line wrap: on
line diff
--- a/OrthancServer/CMakeLists.txt	Wed Sep 14 17:11:05 2022 +0200
+++ b/OrthancServer/CMakeLists.txt	Wed Sep 14 17:11:45 2022 +0200
@@ -181,6 +181,7 @@
   list(APPEND ORTHANC_SERVER_SOURCES
     ${CMAKE_SOURCE_DIR}/Plugins/Engine/OrthancPluginDatabase.cpp
     ${CMAKE_SOURCE_DIR}/Plugins/Engine/OrthancPluginDatabaseV3.cpp
+    ${CMAKE_SOURCE_DIR}/Plugins/Engine/OrthancPluginDatabaseV4.cpp
     ${CMAKE_SOURCE_DIR}/Plugins/Engine/OrthancPlugins.cpp
     ${CMAKE_SOURCE_DIR}/Plugins/Engine/PluginsEnumerations.cpp
     ${CMAKE_SOURCE_DIR}/Plugins/Engine/PluginsErrorDictionary.cpp
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.h	Wed Sep 14 17:11:05 2022 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.h	Wed Sep 14 17:11:45 2022 +0200
@@ -113,6 +113,11 @@
       return false;  // No support for revisions in old API
     }
 
+    virtual bool HasAttachmentCustomDataSupport() const ORTHANC_OVERRIDE
+    {
+      return false;  // No support for custom data in old API
+    }
+
     void AnswerReceived(const _OrthancPluginDatabaseAnswer& answer);
   };
 }
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.h	Wed Sep 14 17:11:05 2022 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.h	Wed Sep 14 17:11:45 2022 +0200
@@ -82,6 +82,12 @@
                          IStorageArea& storageArea) ORTHANC_OVERRIDE;    
 
     virtual bool HasRevisionsSupport() const ORTHANC_OVERRIDE;
+
+    virtual bool HasAttachmentCustomDataSupport() const ORTHANC_OVERRIDE
+    {
+      return false; // introduced in V4
+    }
+
   };
 }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp	Wed Sep 14 17:11:45 2022 +0200
@@ -0,0 +1,1256 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2022 Osimis S.A., Belgium
+ * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../../Sources/PrecompiledHeadersServer.h"
+#include "OrthancPluginDatabaseV4.h"
+
+#if ORTHANC_ENABLE_PLUGINS != 1
+#  error The plugin support is disabled
+#endif
+
+#include "../../../OrthancFramework/Sources/Logging.h"
+#include "../../../OrthancFramework/Sources/OrthancException.h"
+#include "../../Sources/Database/ResourcesContent.h"
+#include "../../Sources/Database/VoidDatabaseListener.h"
+#include "PluginsEnumerations.h"
+
+#include <cassert>
+
+
+#define CHECK_FUNCTION_EXISTS(backend, func)                            \
+  if (backend.func == NULL)                                             \
+  {                                                                     \
+    throw OrthancException(                                             \
+      ErrorCode_DatabasePlugin, "Missing primitive: " #func "()");      \
+  }
+
+namespace Orthanc
+{
+  class OrthancPluginDatabaseV4::Transaction : public IDatabaseWrapper::ITransaction
+  {
+  private:
+    OrthancPluginDatabaseV4&           that_;
+    IDatabaseListener&                 listener_;
+    OrthancPluginDatabaseTransaction*  transaction_;
+
+    
+    void CheckSuccess(OrthancPluginErrorCode code) const
+    {
+      that_.CheckSuccess(code);
+    }
+    
+
+    static FileInfo Convert(const OrthancPluginAttachment& attachment)
+    {
+      std::string customData;
+      return FileInfo(attachment.uuid,
+                      static_cast<FileContentType>(attachment.contentType),
+                      attachment.uncompressedSize,
+                      attachment.uncompressedHash,
+                      static_cast<CompressionType>(attachment.compressionType),
+                      attachment.compressedSize,
+                      attachment.compressedHash,
+                      customData);
+    }
+
+    static FileInfo Convert(const OrthancPluginAttachment2& attachment)
+    {
+      return FileInfo(attachment.uuid,
+                      static_cast<FileContentType>(attachment.contentType),
+                      attachment.uncompressedSize,
+                      attachment.uncompressedHash,
+                      static_cast<CompressionType>(attachment.compressionType),
+                      attachment.compressedSize,
+                      attachment.compressedHash,
+                      attachment.customData);
+    }
+
+    void ReadStringAnswers(std::list<std::string>& target)
+    {
+      uint32_t count;
+      CheckSuccess(that_.backend_.readAnswersCount(transaction_, &count));
+
+      target.clear();
+      for (uint32_t i = 0; i < count; i++)
+      {
+        const char* value = NULL;
+        CheckSuccess(that_.backend_.readAnswerString(transaction_, &value, i));
+        if (value == NULL)
+        {
+          throw OrthancException(ErrorCode_DatabasePlugin);
+        }
+        else
+        {
+          target.push_back(value);
+        }
+      }
+    }
+
+
+    bool ReadSingleStringAnswer(std::string& target)
+    {
+      uint32_t count;
+      CheckSuccess(that_.backend_.readAnswersCount(transaction_, &count));
+
+      if (count == 0)
+      {
+        return false;
+      }
+      else if (count == 1)
+      {
+        const char* value = NULL;
+        CheckSuccess(that_.backend_.readAnswerString(transaction_, &value, 0));
+        if (value == NULL)
+        {
+          throw OrthancException(ErrorCode_DatabasePlugin);
+        }
+        else
+        {
+          target.assign(value);
+          return true;
+        }
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_DatabasePlugin);
+      }
+    }
+
+
+    bool ReadSingleInt64Answer(int64_t& target)
+    {
+      uint32_t count;
+      CheckSuccess(that_.backend_.readAnswersCount(transaction_, &count));
+
+      if (count == 0)
+      {
+        return false;
+      }
+      else if (count == 1)
+      {
+        CheckSuccess(that_.backend_.readAnswerInt64(transaction_, &target, 0));
+        return true;
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_DatabasePlugin);
+      }
+    }
+
+    
+    ExportedResource ReadAnswerExportedResource(uint32_t answerIndex)
+    {
+      OrthancPluginExportedResource exported;
+      CheckSuccess(that_.backend_.readAnswerExportedResource(transaction_, &exported, answerIndex));
+
+      if (exported.publicId == NULL ||
+          exported.modality == NULL ||
+          exported.date == NULL ||
+          exported.patientId == NULL ||
+          exported.studyInstanceUid == NULL ||
+          exported.seriesInstanceUid == NULL ||
+          exported.sopInstanceUid == NULL)
+      {
+        throw OrthancException(ErrorCode_DatabasePlugin);
+      }
+      else
+      {
+        return ExportedResource(exported.seq,
+                                Plugins::Convert(exported.resourceType),
+                                exported.publicId,
+                                exported.modality,
+                                exported.date,
+                                exported.patientId,
+                                exported.studyInstanceUid,
+                                exported.seriesInstanceUid,
+                                exported.sopInstanceUid);
+      }
+    }
+    
+
+    ServerIndexChange ReadAnswerChange(uint32_t answerIndex)
+    {
+      OrthancPluginChange change;
+      CheckSuccess(that_.backend_.readAnswerChange(transaction_, &change, answerIndex));
+
+      if (change.publicId == NULL ||
+          change.date == NULL)
+      {
+        throw OrthancException(ErrorCode_DatabasePlugin);
+      }
+      else
+      {
+        return ServerIndexChange(change.seq,
+                                 static_cast<ChangeType>(change.changeType),
+                                 Plugins::Convert(change.resourceType),
+                                 change.publicId,
+                                 change.date);
+      }
+    }
+
+
+    void CheckNoEvent()
+    {
+      uint32_t count;
+      CheckSuccess(that_.backend_.readEventsCount(transaction_, &count));
+      if (count != 0)
+      {
+        throw OrthancException(ErrorCode_DatabasePlugin);
+      }
+    }
+
+
+    void ProcessEvents(bool isDeletingAttachment)
+    {
+      uint32_t count;
+      CheckSuccess(that_.backend_.readEventsCount(transaction_, &count));
+
+      for (uint32_t i = 0; i < count; i++)
+      {
+        OrthancPluginDatabaseEvent2 event;
+        CheckSuccess(that_.backend_.readEvent2(transaction_, &event, i));
+
+        switch (event.type)
+        {
+          case OrthancPluginDatabaseEventType_DeletedAttachment:
+            listener_.SignalAttachmentDeleted(Convert(event.content.attachment));
+            break;
+            
+          case OrthancPluginDatabaseEventType_DeletedResource:
+            if (isDeletingAttachment)
+            {
+              // This event should only be triggered by "DeleteResource()"
+              throw OrthancException(ErrorCode_DatabasePlugin);
+            }
+            else
+            {
+              listener_.SignalResourceDeleted(Plugins::Convert(event.content.resource.level), event.content.resource.publicId);
+            }            
+            break;
+            
+          case OrthancPluginDatabaseEventType_RemainingAncestor:
+            if (isDeletingAttachment)
+            {
+              // This event should only triggered by "DeleteResource()"
+              throw OrthancException(ErrorCode_DatabasePlugin);
+            }
+            else
+            {
+              listener_.SignalRemainingAncestor(Plugins::Convert(event.content.resource.level), event.content.resource.publicId);
+            }
+            break;
+
+          default:
+            break;  // Unhandled event
+        }
+      }
+    }
+
+
+  public:
+    Transaction(OrthancPluginDatabaseV4& that,
+                IDatabaseListener& listener,
+                OrthancPluginDatabaseTransactionType type) :
+      that_(that),
+      listener_(listener)
+    {
+      CheckSuccess(that.backend_.startTransaction(that.database_, &transaction_, type));
+      if (transaction_ == NULL)
+      {
+        throw OrthancException(ErrorCode_DatabasePlugin);
+      }
+    }
+
+    
+    virtual ~Transaction()
+    {
+      OrthancPluginErrorCode code = that_.backend_.destructTransaction(transaction_);
+      if (code != OrthancPluginErrorCode_Success)
+      {
+        // Don't throw exception in destructors
+        that_.errorDictionary_.LogError(code, true);
+      }
+    }
+    
+
+    virtual void Rollback() ORTHANC_OVERRIDE
+    {
+      CheckSuccess(that_.backend_.rollback(transaction_));
+      CheckNoEvent();
+    }
+    
+
+    virtual void Commit(int64_t fileSizeDelta) ORTHANC_OVERRIDE
+    {
+      CheckSuccess(that_.backend_.commit(transaction_, fileSizeDelta));
+      CheckNoEvent();
+    }
+
+    
+    virtual void AddAttachment(int64_t id,
+                               const FileInfo& attachment,
+                               int64_t revision) ORTHANC_OVERRIDE
+    {
+      OrthancPluginAttachment2 tmp;
+      tmp.uuid = attachment.GetUuid().c_str();
+      tmp.contentType = static_cast<int32_t>(attachment.GetContentType());
+      tmp.uncompressedSize = attachment.GetUncompressedSize();
+      tmp.uncompressedHash = attachment.GetUncompressedMD5().c_str();
+      tmp.compressionType = static_cast<int32_t>(attachment.GetCompressionType());
+      tmp.compressedSize = attachment.GetCompressedSize();
+      tmp.compressedHash = attachment.GetCompressedMD5().c_str();
+      tmp.customData = attachment.GetCustomData().c_str();
+
+      CheckSuccess(that_.backend_.addAttachment2(transaction_, id, &tmp, revision));
+      CheckNoEvent();
+    }
+
+
+    virtual void ClearChanges() ORTHANC_OVERRIDE
+    {
+      CheckSuccess(that_.backend_.clearChanges(transaction_));
+      CheckNoEvent();
+    }
+
+    
+    virtual void ClearExportedResources() ORTHANC_OVERRIDE
+    {
+      CheckSuccess(that_.backend_.clearExportedResources(transaction_));
+      CheckNoEvent();
+    }
+
+    
+    virtual void DeleteAttachment(int64_t id,
+                                  FileContentType attachment) ORTHANC_OVERRIDE
+    {
+      CheckSuccess(that_.backend_.deleteAttachment(transaction_, id, static_cast<int32_t>(attachment)));
+      ProcessEvents(true);
+    }
+
+    
+    virtual void DeleteMetadata(int64_t id,
+                                MetadataType type) ORTHANC_OVERRIDE
+    {
+      CheckSuccess(that_.backend_.deleteMetadata(transaction_, id, static_cast<int32_t>(type)));
+      CheckNoEvent();
+    }
+
+    
+    virtual void DeleteResource(int64_t id) ORTHANC_OVERRIDE
+    {
+      CheckSuccess(that_.backend_.deleteResource(transaction_, id));
+      ProcessEvents(false);
+    }
+
+    
+    virtual void GetAllMetadata(std::map<MetadataType, std::string>& target,
+                                int64_t id) ORTHANC_OVERRIDE
+    {
+      CheckSuccess(that_.backend_.getAllMetadata(transaction_, id));
+      CheckNoEvent();
+
+      uint32_t count;
+      CheckSuccess(that_.backend_.readAnswersCount(transaction_, &count));
+      
+      target.clear();
+      for (uint32_t i = 0; i < count; i++)
+      {
+        int32_t metadata;
+        const char* value = NULL;
+        CheckSuccess(that_.backend_.readAnswerMetadata(transaction_, &metadata, &value, i));
+
+        if (value == NULL)
+        {
+          throw OrthancException(ErrorCode_DatabasePlugin);
+        }
+        else
+        {
+          target[static_cast<MetadataType>(metadata)] = value;
+        }
+      }
+    }
+
+    
+    virtual void GetAllPublicIds(std::list<std::string>& target,
+                                 ResourceType resourceType) ORTHANC_OVERRIDE
+    {
+      CheckSuccess(that_.backend_.getAllPublicIds(transaction_, Plugins::Convert(resourceType)));
+      CheckNoEvent();
+
+      ReadStringAnswers(target);
+    }
+
+    
+    virtual void GetAllPublicIds(std::list<std::string>& target,
+                                 ResourceType resourceType,
+                                 size_t since,
+                                 size_t limit) ORTHANC_OVERRIDE
+    {
+      CheckSuccess(that_.backend_.getAllPublicIdsWithLimit(
+                     transaction_, Plugins::Convert(resourceType),
+                     static_cast<uint64_t>(since), static_cast<uint64_t>(limit)));
+      CheckNoEvent();
+
+      ReadStringAnswers(target);
+    }
+
+    
+    virtual void GetChanges(std::list<ServerIndexChange>& target /*out*/,
+                            bool& done /*out*/,
+                            int64_t since,
+                            uint32_t maxResults) ORTHANC_OVERRIDE
+    {
+      uint8_t tmpDone = true;
+      CheckSuccess(that_.backend_.getChanges(transaction_, &tmpDone, since, maxResults));
+      CheckNoEvent();
+
+      done = (tmpDone != 0);
+      
+      uint32_t count;
+      CheckSuccess(that_.backend_.readAnswersCount(transaction_, &count));
+      
+      target.clear();
+      for (uint32_t i = 0; i < count; i++)
+      {
+        target.push_back(ReadAnswerChange(i));
+      }
+    }
+
+    
+    virtual void GetChildrenInternalId(std::list<int64_t>& target,
+                                       int64_t id) ORTHANC_OVERRIDE
+    {
+      CheckSuccess(that_.backend_.getChildrenInternalId(transaction_, id));
+      CheckNoEvent();
+
+      uint32_t count;
+      CheckSuccess(that_.backend_.readAnswersCount(transaction_, &count));
+      
+      target.clear();
+      for (uint32_t i = 0; i < count; i++)
+      {
+        int64_t value;
+        CheckSuccess(that_.backend_.readAnswerInt64(transaction_, &value, i));
+        target.push_back(value);
+      }
+    }
+
+    
+    virtual void GetChildrenPublicId(std::list<std::string>& target,
+                                     int64_t id) ORTHANC_OVERRIDE
+    {
+      CheckSuccess(that_.backend_.getChildrenPublicId(transaction_, id));
+      CheckNoEvent();
+
+      ReadStringAnswers(target);
+    }
+
+    
+    virtual void GetExportedResources(std::list<ExportedResource>& target /*out*/,
+                                      bool& done /*out*/,
+                                      int64_t since,
+                                      uint32_t maxResults) ORTHANC_OVERRIDE
+    {
+      uint8_t tmpDone = true;
+      CheckSuccess(that_.backend_.getExportedResources(transaction_, &tmpDone, since, maxResults));
+      CheckNoEvent();
+
+      done = (tmpDone != 0);
+      
+      uint32_t count;
+      CheckSuccess(that_.backend_.readAnswersCount(transaction_, &count));
+      
+      target.clear();
+      for (uint32_t i = 0; i < count; i++)
+      {
+        target.push_back(ReadAnswerExportedResource(i));
+      }
+    }
+
+
+    virtual void GetLastChange(std::list<ServerIndexChange>& target /*out*/) ORTHANC_OVERRIDE
+    {
+      CheckSuccess(that_.backend_.getLastChange(transaction_));
+      CheckNoEvent();
+
+      uint32_t count;
+      CheckSuccess(that_.backend_.readAnswersCount(transaction_, &count));
+
+      target.clear();
+      if (count == 1)
+      {
+        target.push_back(ReadAnswerChange(0));
+      }
+      else if (count > 1)
+      {
+        throw OrthancException(ErrorCode_DatabasePlugin);
+      }
+    }
+
+
+    virtual void GetLastExportedResource(std::list<ExportedResource>& target /*out*/) ORTHANC_OVERRIDE
+    {
+      CheckSuccess(that_.backend_.getLastExportedResource(transaction_));
+      CheckNoEvent();
+
+      uint32_t count;
+      CheckSuccess(that_.backend_.readAnswersCount(transaction_, &count));
+
+      target.clear();
+      if (count == 1)
+      {
+        target.push_back(ReadAnswerExportedResource(0));
+      }
+      else if (count > 1)
+      {
+        throw OrthancException(ErrorCode_DatabasePlugin);
+      }
+    }
+
+    
+    virtual void GetMainDicomTags(DicomMap& target,
+                                  int64_t id) ORTHANC_OVERRIDE
+    {
+      CheckSuccess(that_.backend_.getMainDicomTags(transaction_, id));
+      CheckNoEvent();
+
+      uint32_t count;
+      CheckSuccess(that_.backend_.readAnswersCount(transaction_, &count));
+
+      target.Clear();
+      for (uint32_t i = 0; i < count; i++)
+      {
+        uint16_t group, element;
+        const char* value = NULL;
+        CheckSuccess(that_.backend_.readAnswerDicomTag(transaction_, &group, &element, &value, i));
+
+        if (value == NULL)
+        {
+          throw OrthancException(ErrorCode_DatabasePlugin);
+        }
+        else
+        {
+          target.SetValue(group, element, std::string(value), false);
+        }
+      }
+    }
+
+    
+    virtual std::string GetPublicId(int64_t resourceId) ORTHANC_OVERRIDE
+    {
+      CheckSuccess(that_.backend_.getPublicId(transaction_, resourceId));
+      CheckNoEvent();
+
+      std::string s;
+      if (ReadSingleStringAnswer(s))
+      {
+        return s;
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_InexistentItem);
+      }
+    }
+
+    
+    virtual uint64_t GetResourcesCount(ResourceType resourceType) ORTHANC_OVERRIDE
+    {
+      uint64_t value;
+      CheckSuccess(that_.backend_.getResourcesCount(transaction_, &value, Plugins::Convert(resourceType)));
+      CheckNoEvent();
+      return value;
+    }
+
+    
+    virtual ResourceType GetResourceType(int64_t resourceId) ORTHANC_OVERRIDE
+    {
+      OrthancPluginResourceType type;
+      CheckSuccess(that_.backend_.getResourceType(transaction_, &type, resourceId));
+      CheckNoEvent();
+      return Plugins::Convert(type);
+    }
+
+    
+    virtual uint64_t GetTotalCompressedSize() ORTHANC_OVERRIDE
+    {
+      uint64_t s;
+      CheckSuccess(that_.backend_.getTotalCompressedSize(transaction_, &s));
+      CheckNoEvent();
+      return s;
+    }
+
+    
+    virtual uint64_t GetTotalUncompressedSize() ORTHANC_OVERRIDE
+    {
+      uint64_t s;
+      CheckSuccess(that_.backend_.getTotalUncompressedSize(transaction_, &s));
+      CheckNoEvent();
+      return s;
+    }
+
+    
+    virtual bool IsExistingResource(int64_t internalId) ORTHANC_OVERRIDE
+    {
+      uint8_t b;
+      CheckSuccess(that_.backend_.isExistingResource(transaction_, &b, internalId));
+      CheckNoEvent();
+      return (b != 0);
+    }
+
+    
+    virtual bool IsProtectedPatient(int64_t internalId) ORTHANC_OVERRIDE
+    {
+      uint8_t b;
+      CheckSuccess(that_.backend_.isProtectedPatient(transaction_, &b, internalId));
+      CheckNoEvent();
+      return (b != 0);
+    }
+
+    
+    virtual void ListAvailableAttachments(std::set<FileContentType>& target,
+                                          int64_t id) ORTHANC_OVERRIDE
+    {
+      CheckSuccess(that_.backend_.listAvailableAttachments(transaction_, id));
+      CheckNoEvent();
+
+      uint32_t count;
+      CheckSuccess(that_.backend_.readAnswersCount(transaction_, &count));
+      
+      target.clear();
+      for (uint32_t i = 0; i < count; i++)
+      {
+        int32_t value;
+        CheckSuccess(that_.backend_.readAnswerInt32(transaction_, &value, i));
+        target.insert(static_cast<FileContentType>(value));
+      }
+    }
+
+    
+    virtual void LogChange(int64_t internalId,
+                           const ServerIndexChange& change) ORTHANC_OVERRIDE
+    {
+      CheckSuccess(that_.backend_.logChange(transaction_, static_cast<int32_t>(change.GetChangeType()),
+                                            internalId, Plugins::Convert(change.GetResourceType()),
+                                            change.GetDate().c_str()));
+      CheckNoEvent();
+    }
+
+    
+    virtual void LogExportedResource(const ExportedResource& resource) ORTHANC_OVERRIDE
+    {
+      CheckSuccess(that_.backend_.logExportedResource(transaction_, Plugins::Convert(resource.GetResourceType()),
+                                                      resource.GetPublicId().c_str(),
+                                                      resource.GetModality().c_str(),
+                                                      resource.GetDate().c_str(),
+                                                      resource.GetPatientId().c_str(),
+                                                      resource.GetStudyInstanceUid().c_str(),
+                                                      resource.GetSeriesInstanceUid().c_str(),
+                                                      resource.GetSopInstanceUid().c_str()));
+      CheckNoEvent();
+    }
+
+    
+    virtual bool LookupAttachment(FileInfo& attachment,
+                                  int64_t& revision,
+                                  int64_t id,
+                                  FileContentType contentType) ORTHANC_OVERRIDE
+    {
+      CheckSuccess(that_.backend_.lookupAttachment(transaction_, &revision, id, static_cast<int32_t>(contentType)));
+      CheckNoEvent();
+
+      uint32_t count;
+      CheckSuccess(that_.backend_.readAnswersCount(transaction_, &count));
+
+      if (count == 0)
+      {
+        return false;
+      }
+      else if (count == 1)
+      {
+        OrthancPluginAttachment2 tmp;
+        CheckSuccess(that_.backend_.readAnswerAttachment2(transaction_, &tmp, 0));
+        attachment = Convert(tmp);
+        return true;
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_DatabasePlugin);
+      }
+    }
+
+    
+    virtual bool LookupGlobalProperty(std::string& target,
+                                      GlobalProperty property,
+                                      bool shared) ORTHANC_OVERRIDE
+    {
+      const char* id = (shared ? "" : that_.serverIdentifier_.c_str());
+      
+      CheckSuccess(that_.backend_.lookupGlobalProperty(transaction_, id, static_cast<int32_t>(property)));
+      CheckNoEvent();
+      return ReadSingleStringAnswer(target);      
+    }
+
+    
+    virtual bool LookupMetadata(std::string& target,
+                                int64_t& revision,
+                                int64_t id,
+                                MetadataType type) ORTHANC_OVERRIDE
+    {
+      CheckSuccess(that_.backend_.lookupMetadata(transaction_, &revision, id, static_cast<int32_t>(type)));
+      CheckNoEvent();
+      return ReadSingleStringAnswer(target);      
+    }
+
+    
+    virtual bool LookupParent(int64_t& parentId,
+                              int64_t resourceId) ORTHANC_OVERRIDE
+    {
+      uint8_t existing;
+      CheckSuccess(that_.backend_.lookupParent(transaction_, &existing, &parentId, resourceId));
+      CheckNoEvent();
+      return (existing != 0);
+    }
+
+    
+    virtual bool LookupResource(int64_t& id,
+                                ResourceType& type,
+                                const std::string& publicId) ORTHANC_OVERRIDE
+    {
+      uint8_t existing;
+      OrthancPluginResourceType t;
+      CheckSuccess(that_.backend_.lookupResource(transaction_, &existing, &id, &t, publicId.c_str()));
+      CheckNoEvent();
+
+      if (existing == 0)
+      {
+        return false;
+      }
+      else
+      {
+        type = Plugins::Convert(t);
+        return true;
+      }
+    }
+
+    
+    virtual bool SelectPatientToRecycle(int64_t& internalId) ORTHANC_OVERRIDE
+    {
+      uint8_t available;      
+      CheckSuccess(that_.backend_.selectPatientToRecycle(transaction_, &available, &internalId));
+      CheckNoEvent();
+      return (available != 0);
+    }
+
+    
+    virtual bool SelectPatientToRecycle(int64_t& internalId,
+                                        int64_t patientIdToAvoid) ORTHANC_OVERRIDE
+    {
+      uint8_t available;      
+      CheckSuccess(that_.backend_.selectPatientToRecycle2(transaction_, &available, &internalId, patientIdToAvoid));
+      CheckNoEvent();
+      return (available != 0);
+    }
+
+    
+    virtual void SetGlobalProperty(GlobalProperty property,
+                                   bool shared,
+                                   const std::string& value) ORTHANC_OVERRIDE
+    {
+      const char* id = (shared ? "" : that_.serverIdentifier_.c_str());
+      
+      CheckSuccess(that_.backend_.setGlobalProperty(transaction_, id, static_cast<int32_t>(property), value.c_str()));
+      CheckNoEvent();
+    }
+
+    
+    virtual void ClearMainDicomTags(int64_t id) ORTHANC_OVERRIDE
+    {
+      CheckSuccess(that_.backend_.clearMainDicomTags(transaction_, id));
+      CheckNoEvent();
+    }
+
+    
+    virtual void SetMetadata(int64_t id,
+                             MetadataType type,
+                             const std::string& value,
+                             int64_t revision) ORTHANC_OVERRIDE
+    {
+      CheckSuccess(that_.backend_.setMetadata(transaction_, id, static_cast<int32_t>(type), value.c_str(), revision));
+      CheckNoEvent();
+    }
+
+    
+    virtual void SetProtectedPatient(int64_t internalId, 
+                                     bool isProtected) ORTHANC_OVERRIDE
+    {
+      CheckSuccess(that_.backend_.setProtectedPatient(transaction_, internalId, (isProtected ? 1 : 0)));
+      CheckNoEvent();
+    }
+
+
+    virtual bool IsDiskSizeAbove(uint64_t threshold) ORTHANC_OVERRIDE
+    {
+      uint8_t tmp;
+      CheckSuccess(that_.backend_.isDiskSizeAbove(transaction_, &tmp, threshold));
+      CheckNoEvent();
+      return (tmp != 0);
+    }
+
+    
+    virtual void ApplyLookupResources(std::list<std::string>& resourcesId,
+                                      std::list<std::string>* instancesId, // Can be NULL if not needed
+                                      const std::vector<DatabaseConstraint>& lookup,
+                                      ResourceType queryLevel,
+                                      size_t limit) ORTHANC_OVERRIDE
+    {
+      std::vector<OrthancPluginDatabaseConstraint> constraints;
+      std::vector< std::vector<const char*> > constraintsValues;
+
+      constraints.resize(lookup.size());
+      constraintsValues.resize(lookup.size());
+
+      for (size_t i = 0; i < lookup.size(); i++)
+      {
+        lookup[i].EncodeForPlugins(constraints[i], constraintsValues[i]);
+      }
+
+      CheckSuccess(that_.backend_.lookupResources(transaction_, lookup.size(),
+                                                  (lookup.empty() ? NULL : &constraints[0]),
+                                                  Plugins::Convert(queryLevel),
+                                                  limit, (instancesId == NULL ? 0 : 1)));
+      CheckNoEvent();
+
+      uint32_t count;
+      CheckSuccess(that_.backend_.readAnswersCount(transaction_, &count));
+      
+      resourcesId.clear();
+
+      if (instancesId != NULL)
+      {
+        instancesId->clear();
+      }
+      
+      for (uint32_t i = 0; i < count; i++)
+      {
+        OrthancPluginMatchingResource resource;
+        CheckSuccess(that_.backend_.readAnswerMatchingResource(transaction_, &resource, i));
+
+        if (resource.resourceId == NULL)
+        {
+          throw OrthancException(ErrorCode_DatabasePlugin);
+        }
+        
+        resourcesId.push_back(resource.resourceId);
+
+        if (instancesId != NULL)
+        {
+          if (resource.someInstanceId == NULL)
+          {
+            throw OrthancException(ErrorCode_DatabasePlugin);
+          }
+          else
+          {
+            instancesId->push_back(resource.someInstanceId);
+          }
+        }
+      }
+    }
+
+    
+    virtual bool CreateInstance(CreateInstanceResult& result, /* out */
+                                int64_t& instanceId,          /* out */
+                                const std::string& patient,
+                                const std::string& study,
+                                const std::string& series,
+                                const std::string& instance) ORTHANC_OVERRIDE
+    {
+      OrthancPluginCreateInstanceResult output;
+      memset(&output, 0, sizeof(output));
+
+      CheckSuccess(that_.backend_.createInstance(transaction_, &output, patient.c_str(),
+                                                 study.c_str(), series.c_str(), instance.c_str()));
+      CheckNoEvent();
+
+      instanceId = output.instanceId;
+      
+      if (output.isNewInstance)
+      {
+        result.isNewPatient_ = output.isNewPatient;
+        result.isNewStudy_ = output.isNewStudy;
+        result.isNewSeries_ = output.isNewSeries;
+        result.patientId_ = output.patientId;
+        result.studyId_ = output.studyId;
+        result.seriesId_ = output.seriesId;
+        return true;
+      }
+      else
+      {
+        return false;
+      }
+
+    }
+
+    
+    virtual void SetResourcesContent(const ResourcesContent& content) ORTHANC_OVERRIDE
+    {
+      std::vector<OrthancPluginResourcesContentTags> identifierTags;
+      std::vector<OrthancPluginResourcesContentTags> mainDicomTags;
+      std::vector<OrthancPluginResourcesContentMetadata> metadata;
+
+      identifierTags.reserve(content.GetListTags().size());
+      mainDicomTags.reserve(content.GetListTags().size());
+      metadata.reserve(content.GetListMetadata().size());
+
+      for (ResourcesContent::ListTags::const_iterator
+             it = content.GetListTags().begin(); it != content.GetListTags().end(); ++it)
+      {
+        OrthancPluginResourcesContentTags tmp;
+        tmp.resource = it->resourceId_;
+        tmp.group = it->tag_.GetGroup();
+        tmp.element = it->tag_.GetElement();
+        tmp.value = it->value_.c_str();
+
+        if (it->isIdentifier_)
+        {
+          identifierTags.push_back(tmp);
+        }
+        else
+        {
+          mainDicomTags.push_back(tmp);
+        }
+      }
+
+      for (ResourcesContent::ListMetadata::const_iterator
+             it = content.GetListMetadata().begin(); it != content.GetListMetadata().end(); ++it)
+      {
+        OrthancPluginResourcesContentMetadata tmp;
+        tmp.resource = it->resourceId_;
+        tmp.metadata = it->metadata_;
+        tmp.value = it->value_.c_str();
+        metadata.push_back(tmp);
+      }
+
+      assert(identifierTags.size() + mainDicomTags.size() == content.GetListTags().size() &&
+             metadata.size() == content.GetListMetadata().size());
+       
+      CheckSuccess(that_.backend_.setResourcesContent(transaction_,
+                                                      identifierTags.size(),
+                                                      (identifierTags.empty() ? NULL : &identifierTags[0]),
+                                                      mainDicomTags.size(),
+                                                      (mainDicomTags.empty() ? NULL : &mainDicomTags[0]),
+                                                      metadata.size(),
+                                                      (metadata.empty() ? NULL : &metadata[0])));
+      CheckNoEvent();
+    }
+
+    
+    virtual void GetChildrenMetadata(std::list<std::string>& target,
+                                     int64_t resourceId,
+                                     MetadataType metadata) ORTHANC_OVERRIDE
+    {
+      CheckSuccess(that_.backend_.getChildrenMetadata(transaction_, resourceId, static_cast<int32_t>(metadata)));
+      CheckNoEvent();
+      ReadStringAnswers(target);
+    }
+
+    
+    virtual int64_t GetLastChangeIndex() ORTHANC_OVERRIDE
+    {
+      int64_t tmp;
+      CheckSuccess(that_.backend_.getLastChangeIndex(transaction_, &tmp));
+      CheckNoEvent();
+      return tmp;
+    }
+
+    
+    virtual bool LookupResourceAndParent(int64_t& id,
+                                         ResourceType& type,
+                                         std::string& parentPublicId,
+                                         const std::string& publicId) ORTHANC_OVERRIDE
+    {
+      uint8_t isExisting;
+      OrthancPluginResourceType tmpType;
+      CheckSuccess(that_.backend_.lookupResourceAndParent(transaction_, &isExisting, &id, &tmpType, publicId.c_str()));
+      CheckNoEvent();
+
+      if (isExisting)
+      {
+        type = Plugins::Convert(tmpType);
+        
+        uint32_t count;
+        CheckSuccess(that_.backend_.readAnswersCount(transaction_, &count));
+
+        if (count > 1)
+        {
+          throw OrthancException(ErrorCode_DatabasePlugin);
+        }
+
+        switch (type)
+        {
+          case ResourceType_Patient:
+            // A patient has no parent
+            if (count == 1)
+            {
+              throw OrthancException(ErrorCode_DatabasePlugin);
+            }
+            break;
+
+          case ResourceType_Study:
+          case ResourceType_Series:
+          case ResourceType_Instance:
+            if (count == 0)
+            {
+              throw OrthancException(ErrorCode_DatabasePlugin);
+            }
+            else
+            {
+              const char* value = NULL;
+              CheckSuccess(that_.backend_.readAnswerString(transaction_, &value, 0));
+              if (value == NULL)
+              {
+                throw OrthancException(ErrorCode_DatabasePlugin);
+              }
+              else
+              {
+                parentPublicId.assign(value);
+              }              
+            }
+            break;
+
+          default:
+            throw OrthancException(ErrorCode_DatabasePlugin);
+        }
+        
+        return true;
+      }
+      else
+      {
+        return false;
+      }
+    }
+  };
+
+  
+  void OrthancPluginDatabaseV4::CheckSuccess(OrthancPluginErrorCode code) const
+  {
+    if (code != OrthancPluginErrorCode_Success)
+    {
+      errorDictionary_.LogError(code, true);
+      throw OrthancException(static_cast<ErrorCode>(code));
+    }
+  }
+
+
+  OrthancPluginDatabaseV4::OrthancPluginDatabaseV4(SharedLibrary& library,
+                                                   PluginsErrorDictionary&  errorDictionary,
+                                                   const OrthancPluginDatabaseBackendV4* backend,
+                                                   size_t backendSize,
+                                                   void* database,
+                                                   const std::string& serverIdentifier) :
+    library_(library),
+    errorDictionary_(errorDictionary),
+    database_(database),
+    serverIdentifier_(serverIdentifier)
+  {
+    CLOG(INFO, PLUGINS) << "Identifier of this Orthanc server for the global properties "
+                        << "of the custom database: \"" << serverIdentifier << "\"";
+    
+    if (backendSize >= sizeof(backend_))
+    {
+      memcpy(&backend_, backend, sizeof(backend_));
+    }
+    else
+    {
+      // Not all the primitives are implemented by the plugin
+      memset(&backend_, 0, sizeof(backend_));
+      memcpy(&backend_, backend, backendSize);
+    }
+
+    // Sanity checks
+    CHECK_FUNCTION_EXISTS(backend_, readAnswersCount);
+    CHECK_FUNCTION_EXISTS(backend_, readAnswerAttachment2);
+    CHECK_FUNCTION_EXISTS(backend_, readAnswerChange);
+    CHECK_FUNCTION_EXISTS(backend_, readAnswerDicomTag);
+    CHECK_FUNCTION_EXISTS(backend_, readAnswerExportedResource);
+    CHECK_FUNCTION_EXISTS(backend_, readAnswerInt32);
+    CHECK_FUNCTION_EXISTS(backend_, readAnswerInt64);
+    CHECK_FUNCTION_EXISTS(backend_, readAnswerMatchingResource);
+    CHECK_FUNCTION_EXISTS(backend_, readAnswerMetadata);
+    CHECK_FUNCTION_EXISTS(backend_, readAnswerString);
+    
+    CHECK_FUNCTION_EXISTS(backend_, readEventsCount);
+    CHECK_FUNCTION_EXISTS(backend_, readEvent2);
+
+    CHECK_FUNCTION_EXISTS(backend_, open);
+    CHECK_FUNCTION_EXISTS(backend_, close);
+    CHECK_FUNCTION_EXISTS(backend_, destructDatabase);
+    CHECK_FUNCTION_EXISTS(backend_, getDatabaseVersion);
+    CHECK_FUNCTION_EXISTS(backend_, upgradeDatabase);
+    CHECK_FUNCTION_EXISTS(backend_, startTransaction);
+    CHECK_FUNCTION_EXISTS(backend_, destructTransaction);
+    CHECK_FUNCTION_EXISTS(backend_, hasRevisionsSupport);
+    CHECK_FUNCTION_EXISTS(backend_, hasAttachmentCustomDataSupport);   // new in v4
+
+    CHECK_FUNCTION_EXISTS(backend_, rollback);
+    CHECK_FUNCTION_EXISTS(backend_, commit);
+    
+    CHECK_FUNCTION_EXISTS(backend_, addAttachment2);
+    CHECK_FUNCTION_EXISTS(backend_, clearChanges);
+    CHECK_FUNCTION_EXISTS(backend_, clearExportedResources);
+    CHECK_FUNCTION_EXISTS(backend_, clearMainDicomTags);
+    CHECK_FUNCTION_EXISTS(backend_, createInstance);
+    CHECK_FUNCTION_EXISTS(backend_, deleteAttachment);
+    CHECK_FUNCTION_EXISTS(backend_, deleteMetadata);
+    CHECK_FUNCTION_EXISTS(backend_, deleteResource);
+    CHECK_FUNCTION_EXISTS(backend_, getAllMetadata);
+    CHECK_FUNCTION_EXISTS(backend_, getAllPublicIds);
+    CHECK_FUNCTION_EXISTS(backend_, getAllPublicIdsWithLimit);
+    CHECK_FUNCTION_EXISTS(backend_, getChanges);
+    CHECK_FUNCTION_EXISTS(backend_, getChildrenInternalId);
+    CHECK_FUNCTION_EXISTS(backend_, getChildrenMetadata);
+    CHECK_FUNCTION_EXISTS(backend_, getChildrenPublicId);
+    CHECK_FUNCTION_EXISTS(backend_, getExportedResources);
+    CHECK_FUNCTION_EXISTS(backend_, getLastChange);
+    CHECK_FUNCTION_EXISTS(backend_, getLastChangeIndex);
+    CHECK_FUNCTION_EXISTS(backend_, getLastExportedResource);
+    CHECK_FUNCTION_EXISTS(backend_, getMainDicomTags);
+    CHECK_FUNCTION_EXISTS(backend_, getPublicId);
+    CHECK_FUNCTION_EXISTS(backend_, getResourceType);
+    CHECK_FUNCTION_EXISTS(backend_, getResourcesCount);
+    CHECK_FUNCTION_EXISTS(backend_, getTotalCompressedSize);
+    CHECK_FUNCTION_EXISTS(backend_, getTotalUncompressedSize);
+    CHECK_FUNCTION_EXISTS(backend_, isDiskSizeAbove);
+    CHECK_FUNCTION_EXISTS(backend_, isExistingResource);
+    CHECK_FUNCTION_EXISTS(backend_, isProtectedPatient);
+    CHECK_FUNCTION_EXISTS(backend_, listAvailableAttachments);
+    CHECK_FUNCTION_EXISTS(backend_, logChange);
+    CHECK_FUNCTION_EXISTS(backend_, logExportedResource);
+    CHECK_FUNCTION_EXISTS(backend_, lookupAttachment);
+    CHECK_FUNCTION_EXISTS(backend_, lookupGlobalProperty);
+    CHECK_FUNCTION_EXISTS(backend_, lookupMetadata);
+    CHECK_FUNCTION_EXISTS(backend_, lookupParent);
+    CHECK_FUNCTION_EXISTS(backend_, lookupResource);
+    CHECK_FUNCTION_EXISTS(backend_, lookupResourceAndParent);
+    CHECK_FUNCTION_EXISTS(backend_, lookupResources);
+    CHECK_FUNCTION_EXISTS(backend_, selectPatientToRecycle);
+    CHECK_FUNCTION_EXISTS(backend_, selectPatientToRecycle2);
+    CHECK_FUNCTION_EXISTS(backend_, setGlobalProperty);
+    CHECK_FUNCTION_EXISTS(backend_, setMetadata);
+    CHECK_FUNCTION_EXISTS(backend_, setProtectedPatient);
+    CHECK_FUNCTION_EXISTS(backend_, setResourcesContent);
+  }
+
+  
+  OrthancPluginDatabaseV4::~OrthancPluginDatabaseV4()
+  {
+    if (database_ != NULL)
+    {
+      OrthancPluginErrorCode code = backend_.destructDatabase(database_);
+      if (code != OrthancPluginErrorCode_Success)
+      {
+        // Don't throw exception in destructors
+        errorDictionary_.LogError(code, true);
+      }
+    }
+  }
+
+  
+  void OrthancPluginDatabaseV4::Open()
+  {
+    CheckSuccess(backend_.open(database_));
+  }
+
+
+  void OrthancPluginDatabaseV4::Close()
+  {
+    CheckSuccess(backend_.close(database_));
+  }
+  
+
+  IDatabaseWrapper::ITransaction* OrthancPluginDatabaseV4::StartTransaction(TransactionType type,
+                                                                            IDatabaseListener& listener)
+  {
+    switch (type)
+    {
+      case TransactionType_ReadOnly:
+        return new Transaction(*this, listener, OrthancPluginDatabaseTransactionType_ReadOnly);
+
+      case TransactionType_ReadWrite:
+        return new Transaction(*this, listener, OrthancPluginDatabaseTransactionType_ReadWrite);
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+  }
+
+  
+  unsigned int OrthancPluginDatabaseV4::GetDatabaseVersion()
+  {
+    uint32_t version = 0;
+    CheckSuccess(backend_.getDatabaseVersion(database_, &version));
+    return version;
+  }
+
+  
+  void OrthancPluginDatabaseV4::Upgrade(unsigned int targetVersion,
+                                        IStorageArea& storageArea)
+  {
+    VoidDatabaseListener listener;
+    
+    if (backend_.upgradeDatabase != NULL)
+    {
+      Transaction transaction(*this, listener, OrthancPluginDatabaseTransactionType_ReadWrite);
+
+      OrthancPluginErrorCode code = backend_.upgradeDatabase(
+        database_, reinterpret_cast<OrthancPluginStorageArea*>(&storageArea),
+        static_cast<uint32_t>(targetVersion));
+
+      if (code == OrthancPluginErrorCode_Success)
+      {
+        transaction.Commit(0);
+      }
+      else
+      {
+        transaction.Rollback();
+        errorDictionary_.LogError(code, true);
+        throw OrthancException(static_cast<ErrorCode>(code));
+      }
+    }
+  }
+
+  
+  bool OrthancPluginDatabaseV4::HasRevisionsSupport() const
+  {
+    // WARNING: This method requires "Open()" to have been called
+    uint8_t hasRevisions;
+    CheckSuccess(backend_.hasRevisionsSupport(database_, &hasRevisions));
+    return (hasRevisions != 0);
+  }
+
+  bool OrthancPluginDatabaseV4::HasAttachmentCustomDataSupport() const
+  {
+    // WARNING: This method requires "Open()" to have been called
+    uint8_t hasAttachmentCustomDataSupport;
+    CheckSuccess(backend_.hasAttachmentCustomDataSupport(database_, &hasAttachmentCustomDataSupport));
+    return (hasAttachmentCustomDataSupport != 0);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h	Wed Sep 14 17:11:45 2022 +0200
@@ -0,0 +1,91 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2022 Osimis S.A., Belgium
+ * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#if ORTHANC_ENABLE_PLUGINS == 1
+
+#include "../../../OrthancFramework/Sources/SharedLibrary.h"
+#include "../../Sources/Database/IDatabaseWrapper.h"
+#include "../Include/orthanc/OrthancCDatabasePlugin.h"
+#include "PluginsErrorDictionary.h"
+
+namespace Orthanc
+{
+  class OrthancPluginDatabaseV4 : public IDatabaseWrapper
+  {
+  private:
+    class Transaction;
+
+    SharedLibrary&                  library_;
+    PluginsErrorDictionary&         errorDictionary_;
+    OrthancPluginDatabaseBackendV4  backend_;
+    void*                           database_;
+    std::string                     serverIdentifier_;
+
+    void CheckSuccess(OrthancPluginErrorCode code) const;
+
+  public:
+    OrthancPluginDatabaseV4(SharedLibrary& library,
+                            PluginsErrorDictionary&  errorDictionary,
+                            const OrthancPluginDatabaseBackendV4* backend,
+                            size_t backendSize,
+                            void* database,
+                            const std::string& serverIdentifier);
+
+    virtual ~OrthancPluginDatabaseV4();
+
+    virtual void Open() ORTHANC_OVERRIDE;
+
+    virtual void Close() ORTHANC_OVERRIDE;
+
+    const SharedLibrary& GetSharedLibrary() const
+    {
+      return library_;
+    }
+
+    virtual void FlushToDisk() ORTHANC_OVERRIDE
+    {
+    }
+
+    virtual bool HasFlushToDisk() const ORTHANC_OVERRIDE
+    {
+      return false;
+    }
+
+    virtual IDatabaseWrapper::ITransaction* StartTransaction(TransactionType type,
+                                                             IDatabaseListener& listener)
+      ORTHANC_OVERRIDE;
+
+    virtual unsigned int GetDatabaseVersion() ORTHANC_OVERRIDE;
+
+    virtual void Upgrade(unsigned int targetVersion,
+                         IStorageArea& storageArea) ORTHANC_OVERRIDE;    
+
+    virtual bool HasRevisionsSupport() const ORTHANC_OVERRIDE;
+
+    virtual bool HasAttachmentCustomDataSupport() const ORTHANC_OVERRIDE;
+
+  };
+}
+
+#endif
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Wed Sep 14 17:11:05 2022 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Wed Sep 14 17:11:45 2022 +0200
@@ -64,6 +64,7 @@
 #include "../../Sources/ServerToolbox.h"
 #include "OrthancPluginDatabase.h"
 #include "OrthancPluginDatabaseV3.h"
+#include "OrthancPluginDatabaseV4.h"
 #include "PluginsEnumerations.h"
 #include "PluginsJob.h"
 
@@ -1890,6 +1891,7 @@
     char** argv_;
     std::unique_ptr<OrthancPluginDatabase>  database_;
     std::unique_ptr<OrthancPluginDatabaseV3>  databaseV3_;  // New in Orthanc 1.9.2
+    std::unique_ptr<OrthancPluginDatabaseV4>  databaseV4_;  // New in Orthanc 1.12.0
     PluginsErrorDictionary  dictionary_;
     std::string databaseServerIdentifier_;   // New in Orthanc 1.9.2
     unsigned int maxDatabaseRetries_;   // New in Orthanc 1.9.2
@@ -5746,7 +5748,8 @@
           *reinterpret_cast<const _OrthancPluginRegisterDatabaseBackend*>(parameters);
 
         if (pimpl_->database_.get() == NULL &&
-            pimpl_->databaseV3_.get() == NULL)
+            pimpl_->databaseV3_.get() == NULL &&
+            pimpl_->databaseV4_.get() == NULL)
         {
           pimpl_->database_.reset(new OrthancPluginDatabase(plugin, GetErrorDictionary(), 
                                                             *p.backend, NULL, 0, p.payload));
@@ -5770,7 +5773,8 @@
           *reinterpret_cast<const _OrthancPluginRegisterDatabaseBackendV2*>(parameters);
 
         if (pimpl_->database_.get() == NULL &&
-            pimpl_->databaseV3_.get() == NULL)
+            pimpl_->databaseV3_.get() == NULL &&
+            pimpl_->databaseV4_.get() == NULL)
         {
           pimpl_->database_.reset(new OrthancPluginDatabase(plugin, GetErrorDictionary(),
                                                             *p.backend, p.extensions,
@@ -5788,13 +5792,14 @@
 
       case _OrthancPluginService_RegisterDatabaseBackendV3:
       {
-        CLOG(INFO, PLUGINS) << "Plugin has registered a custom database back-end";
+        CLOG(INFO, PLUGINS) << "Plugin has registered a custom database back-end (v3)";
 
         const _OrthancPluginRegisterDatabaseBackendV3& p =
           *reinterpret_cast<const _OrthancPluginRegisterDatabaseBackendV3*>(parameters);
 
         if (pimpl_->database_.get() == NULL &&
-            pimpl_->databaseV3_.get() == NULL)
+            pimpl_->databaseV3_.get() == NULL &&
+            pimpl_->databaseV4_.get() == NULL)
         {
           pimpl_->databaseV3_.reset(new OrthancPluginDatabaseV3(plugin, GetErrorDictionary(), p.backend,
                                                                 p.backendSize, p.database, pimpl_->databaseServerIdentifier_));
@@ -5808,6 +5813,30 @@
         return true;
       }
 
+
+      case _OrthancPluginService_RegisterDatabaseBackendV4:
+      {
+        CLOG(INFO, PLUGINS) << "Plugin has registered a custom database back-end (v4)";
+
+        const _OrthancPluginRegisterDatabaseBackendV4& p =
+          *reinterpret_cast<const _OrthancPluginRegisterDatabaseBackendV4*>(parameters);
+
+        if (pimpl_->database_.get() == NULL &&
+            pimpl_->databaseV3_.get() == NULL &&
+            pimpl_->databaseV4_.get() == NULL)
+        {
+          pimpl_->databaseV4_.reset(new OrthancPluginDatabaseV4(plugin, GetErrorDictionary(), p.backend,
+                                                                p.backendSize, p.database, pimpl_->databaseServerIdentifier_));
+          pimpl_->maxDatabaseRetries_ = p.maxDatabaseRetries;
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_DatabaseBackendAlreadyRegistered);
+        }
+
+        return true;
+      }
+
       case _OrthancPluginService_DatabaseAnswer:
         throw OrthancException(ErrorCode_InternalError);   // Implemented before locking (*)
 
@@ -5947,7 +5976,8 @@
   {
     boost::recursive_mutex::scoped_lock lock(pimpl_->invokeServiceMutex_);
     return (pimpl_->database_.get() != NULL ||
-            pimpl_->databaseV3_.get() != NULL);
+            pimpl_->databaseV3_.get() != NULL ||
+            pimpl_->databaseV4_.get() != NULL);
   }
 
 
@@ -5987,6 +6017,10 @@
     {
       return *pimpl_->databaseV3_;
     }
+    else if (pimpl_->databaseV4_.get() != NULL)
+    {
+      return *pimpl_->databaseV4_;
+    }
     else
     {
       throw OrthancException(ErrorCode_BadSequenceOfCalls);
@@ -6004,6 +6038,10 @@
     {
       return pimpl_->databaseV3_->GetSharedLibrary();
     }
+    else if (pimpl_->databaseV4_.get() != NULL)
+    {
+      return pimpl_->databaseV4_->GetSharedLibrary();
+    }
     else
     {
       throw OrthancException(ErrorCode_BadSequenceOfCalls);
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCDatabasePlugin.h	Wed Sep 14 17:11:05 2022 +0200
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancCDatabasePlugin.h	Wed Sep 14 17:11:45 2022 +0200
@@ -62,6 +62,7 @@
     _OrthancPluginDatabaseAnswerType_DeletedAttachment = 1,
     _OrthancPluginDatabaseAnswerType_DeletedResource = 2,
     _OrthancPluginDatabaseAnswerType_RemainingAncestor = 3,
+    _OrthancPluginDatabaseAnswerType_DeletedAttachment2 = 4,
 
     /* Return value */
     _OrthancPluginDatabaseAnswerType_Attachment = 10,
@@ -74,6 +75,7 @@
     _OrthancPluginDatabaseAnswerType_String = 17,
     _OrthancPluginDatabaseAnswerType_MatchingResource = 18,  /* New in Orthanc 1.5.2 */
     _OrthancPluginDatabaseAnswerType_Metadata = 19,          /* New in Orthanc 1.5.4 */
+    _OrthancPluginDatabaseAnswerType_Attachment2 = 20,       /* New in Orthanc 1.12.0 */
 
     _OrthancPluginDatabaseAnswerType_INTERNAL = 0x7fffffff
   } _OrthancPluginDatabaseAnswerType;
@@ -92,6 +94,18 @@
 
   typedef struct
   {
+    const char* uuid;
+    int32_t     contentType;
+    uint64_t    uncompressedSize;
+    const char* uncompressedHash;
+    int32_t     compressionType;
+    uint64_t    compressedSize;
+    const char* compressedHash;
+    const char* customData;
+  } OrthancPluginAttachment2;
+
+  typedef struct
+  {
     uint16_t     group;
     uint16_t     element;
     const char*  value;
@@ -305,6 +319,19 @@
     context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
   }
 
+  ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerAttachment2(
+    OrthancPluginContext*          context,
+    OrthancPluginDatabaseContext*  database,
+    const OrthancPluginAttachment2* attachment)
+  {
+    _OrthancPluginDatabaseAnswer params;
+    memset(&params, 0, sizeof(params));
+    params.database = database;
+    params.type = _OrthancPluginDatabaseAnswerType_Attachment2;
+    params.valueGeneric = attachment;
+    context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
+  }
+
   ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerResource(
     OrthancPluginContext*          context,
     OrthancPluginDatabaseContext*  database,
@@ -365,6 +392,19 @@
     context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
   }
 
+  ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseSignalDeletedAttachment2(
+    OrthancPluginContext*          context,
+    OrthancPluginDatabaseContext*  database,
+    const OrthancPluginAttachment2* attachment)
+  {
+    _OrthancPluginDatabaseAnswer params;
+    memset(&params, 0, sizeof(params));
+    params.database = database;
+    params.type = _OrthancPluginDatabaseAnswerType_DeletedAttachment2;
+    params.valueGeneric = attachment;
+    context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
+  }
+
   ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseSignalDeletedResource(
     OrthancPluginContext*          context,
     OrthancPluginDatabaseContext*  database,
@@ -1360,7 +1400,374 @@
 
     return context->InvokeService(context, _OrthancPluginService_RegisterDatabaseBackendV3, &params);
   }
+
+
+  typedef struct
+  {
+    OrthancPluginDatabaseEventType type;
+
+    union
+    {
+      struct
+      {
+        /* For ""DeletedResource" and "RemainingAncestor" */
+        OrthancPluginResourceType  level;
+        const char*                publicId;
+      } resource;
+
+      /* For "DeletedAttachment" */
+      OrthancPluginAttachment2  attachment;
+      
+    } content;
+    
+  } OrthancPluginDatabaseEvent2;
+
+
+  typedef struct
+  {
+    /**
+     * Functions to read the answers inside a transaction
+     **/
+    
+    OrthancPluginErrorCode (*readAnswersCount) (OrthancPluginDatabaseTransaction* transaction,
+                                                uint32_t* target /* out */);
+
+    OrthancPluginErrorCode (*readAnswerAttachment2) (OrthancPluginDatabaseTransaction* transaction,
+                                                     OrthancPluginAttachment2* target /* out */,          // new in v4
+                                                     uint32_t index);
+
+    OrthancPluginErrorCode (*readAnswerChange) (OrthancPluginDatabaseTransaction* transaction,
+                                                OrthancPluginChange* target /* out */,
+                                                uint32_t index);
+
+    OrthancPluginErrorCode (*readAnswerDicomTag) (OrthancPluginDatabaseTransaction* transaction,
+                                                  uint16_t* group,
+                                                  uint16_t* element,
+                                                  const char** value,
+                                                  uint32_t index);
+
+    OrthancPluginErrorCode (*readAnswerExportedResource) (OrthancPluginDatabaseTransaction* transaction,
+                                                          OrthancPluginExportedResource* target /* out */,
+                                                          uint32_t index);
+
+    OrthancPluginErrorCode (*readAnswerInt32) (OrthancPluginDatabaseTransaction* transaction,
+                                               int32_t* target /* out */,
+                                               uint32_t index);
+
+    OrthancPluginErrorCode (*readAnswerInt64) (OrthancPluginDatabaseTransaction* transaction,
+                                               int64_t* target /* out */,
+                                               uint32_t index);
+
+    OrthancPluginErrorCode (*readAnswerMatchingResource) (OrthancPluginDatabaseTransaction* transaction,
+                                                          OrthancPluginMatchingResource* target /* out */,
+                                                          uint32_t index);
+    
+    OrthancPluginErrorCode (*readAnswerMetadata) (OrthancPluginDatabaseTransaction* transaction,
+                                                  int32_t* metadata /* out */,
+                                                  const char** value /* out */,
+                                                  uint32_t index);
+
+    OrthancPluginErrorCode (*readAnswerString) (OrthancPluginDatabaseTransaction* transaction,
+                                                const char** target /* out */,
+                                                uint32_t index);
+    
+    OrthancPluginErrorCode (*readEventsCount) (OrthancPluginDatabaseTransaction* transaction,
+                                               uint32_t* target /* out */);
+
+    OrthancPluginErrorCode (*readEvent2) (OrthancPluginDatabaseTransaction* transaction,
+                                          OrthancPluginDatabaseEvent2* event /* out */,                // new in v4
+                                          uint32_t index);
+
+    
+    
+    /**
+     * Functions to access the global database object
+     * (cf. "IDatabaseWrapper" class in Orthanc)
+     **/
+
+    OrthancPluginErrorCode (*open) (void* database);
+
+    OrthancPluginErrorCode (*close) (void* database);
+
+    OrthancPluginErrorCode (*destructDatabase) (void* database);
+
+    OrthancPluginErrorCode (*getDatabaseVersion) (void* database,
+                                                  uint32_t* target /* out */);
+
+    OrthancPluginErrorCode (*hasRevisionsSupport) (void* database,
+                                                   uint8_t* target /* out */);
+
+    OrthancPluginErrorCode (*hasAttachmentCustomDataSupport) (void* database,                           // new in v4
+                                                              uint8_t* target /* out */);
+
+    OrthancPluginErrorCode (*upgradeDatabase) (void* database,
+                                               OrthancPluginStorageArea* storageArea,
+                                               uint32_t targetVersion);
+
+    OrthancPluginErrorCode (*startTransaction) (void* database,
+                                                OrthancPluginDatabaseTransaction** target /* out */,
+                                                OrthancPluginDatabaseTransactionType type);
+
+    OrthancPluginErrorCode (*destructTransaction) (OrthancPluginDatabaseTransaction* transaction);
+
+
+    /**
+     * Functions to run operations within a database transaction
+     * (cf. "IDatabaseWrapper::ITransaction" class in Orthanc)
+     **/
+
+    OrthancPluginErrorCode (*rollback) (OrthancPluginDatabaseTransaction* transaction);
+    
+    OrthancPluginErrorCode (*commit) (OrthancPluginDatabaseTransaction* transaction,
+                                      int64_t fileSizeDelta);
+
+    /* A call to "addAttachment()" guarantees that this attachment is not already existing ("INSERT") */
+    OrthancPluginErrorCode (*addAttachment2) (OrthancPluginDatabaseTransaction* transaction,
+                                             int64_t id,
+                                             const OrthancPluginAttachment2* attachment,                      // new in v4
+                                             int64_t revision);
+
+    OrthancPluginErrorCode (*clearChanges) (OrthancPluginDatabaseTransaction* transaction);
+    
+    OrthancPluginErrorCode (*clearExportedResources) (OrthancPluginDatabaseTransaction* transaction);
+    
+    OrthancPluginErrorCode (*clearMainDicomTags) (OrthancPluginDatabaseTransaction* transaction,
+                                                  int64_t resourceId);
+
+    OrthancPluginErrorCode (*createInstance) (OrthancPluginDatabaseTransaction* transaction,
+                                              OrthancPluginCreateInstanceResult* target /* out */,
+                                              const char* hashPatient,
+                                              const char* hashStudy,
+                                              const char* hashSeries,
+                                              const char* hashInstance);
+
+    OrthancPluginErrorCode (*deleteAttachment) (OrthancPluginDatabaseTransaction* transaction,
+                                                int64_t id,
+                                                int32_t contentType);
+    
+    OrthancPluginErrorCode (*deleteMetadata) (OrthancPluginDatabaseTransaction* transaction,
+                                              int64_t id,
+                                              int32_t metadataType);
+
+    OrthancPluginErrorCode (*deleteResource) (OrthancPluginDatabaseTransaction* transaction,
+                                              int64_t id);
+
+    /* Answers are read using "readAnswerMetadata()" */
+    OrthancPluginErrorCode (*getAllMetadata) (OrthancPluginDatabaseTransaction* transaction,
+                                              int64_t id);
+    
+    /* Answers are read using "readAnswerString()" */
+    OrthancPluginErrorCode (*getAllPublicIds) (OrthancPluginDatabaseTransaction* transaction,
+                                               OrthancPluginResourceType resourceType);
+    
+    /* Answers are read using "readAnswerString()" */
+    OrthancPluginErrorCode (*getAllPublicIdsWithLimit) (OrthancPluginDatabaseTransaction* transaction,
+                                                        OrthancPluginResourceType resourceType,
+                                                        uint64_t since,
+                                                        uint64_t limit);
+
+    /* Answers are read using "readAnswerChange()" */
+    OrthancPluginErrorCode (*getChanges) (OrthancPluginDatabaseTransaction* transaction,
+                                          uint8_t* targetDone /* out */,
+                                          int64_t since,
+                                          uint32_t maxResults);
+    
+    /* Answers are read using "readAnswerInt64()" */
+    OrthancPluginErrorCode (*getChildrenInternalId) (OrthancPluginDatabaseTransaction* transaction,
+                                                     int64_t id);
+    
+    /* Answers are read using "readAnswerString()" */
+    OrthancPluginErrorCode  (*getChildrenMetadata) (OrthancPluginDatabaseTransaction* transaction,
+                                                    int64_t resourceId,
+                                                    int32_t metadata);
+
+    /* Answers are read using "readAnswerString()" */
+    OrthancPluginErrorCode (*getChildrenPublicId) (OrthancPluginDatabaseTransaction* transaction,
+                                                   int64_t id);
+
+    /* Answers are read using "readAnswerExportedResource()" */
+    OrthancPluginErrorCode (*getExportedResources) (OrthancPluginDatabaseTransaction* transaction,
+                                                    uint8_t* targetDone /* out */,
+                                                    int64_t since,
+                                                    uint32_t maxResults);
+    
+    /* Answer is read using "readAnswerChange()" */
+    OrthancPluginErrorCode (*getLastChange) (OrthancPluginDatabaseTransaction* transaction);
+    
+    OrthancPluginErrorCode (*getLastChangeIndex) (OrthancPluginDatabaseTransaction* transaction,
+                                                  int64_t* target /* out */);
+    
+    /* Answer is read using "readAnswerExportedResource()" */
+    OrthancPluginErrorCode (*getLastExportedResource) (OrthancPluginDatabaseTransaction* transaction);
+    
+    /* Answers are read using "readAnswerDicomTag()" */
+    OrthancPluginErrorCode (*getMainDicomTags) (OrthancPluginDatabaseTransaction* transaction,
+                                                int64_t id);
+    
+    /* Answer is read using "readAnswerString()" */
+    OrthancPluginErrorCode (*getPublicId) (OrthancPluginDatabaseTransaction* transaction,
+                                           int64_t internalId);
+    
+    OrthancPluginErrorCode (*getResourcesCount) (OrthancPluginDatabaseTransaction* transaction,
+                                                 uint64_t* target /* out */,
+                                                 OrthancPluginResourceType resourceType);
+    
+    OrthancPluginErrorCode (*getResourceType) (OrthancPluginDatabaseTransaction* transaction,
+                                               OrthancPluginResourceType* target /* out */,
+                                               uint64_t resourceId);
+    
+    OrthancPluginErrorCode (*getTotalCompressedSize) (OrthancPluginDatabaseTransaction* transaction,
+                                                      uint64_t* target /* out */);
+    
+    OrthancPluginErrorCode (*getTotalUncompressedSize) (OrthancPluginDatabaseTransaction* transaction,
+                                                        uint64_t* target /* out */);
+    
+    OrthancPluginErrorCode (*isDiskSizeAbove) (OrthancPluginDatabaseTransaction* transaction,
+                                               uint8_t* target /* out */,
+                                               uint64_t threshold);
+    
+    OrthancPluginErrorCode (*isExistingResource) (OrthancPluginDatabaseTransaction* transaction,
+                                                  uint8_t* target /* out */,
+                                                  int64_t resourceId);
+    
+    OrthancPluginErrorCode (*isProtectedPatient) (OrthancPluginDatabaseTransaction* transaction,
+                                                  uint8_t* target /* out */,
+                                                  int64_t resourceId);
+    
+    /* Answers are read using "readAnswerInt32()" */
+    OrthancPluginErrorCode (*listAvailableAttachments) (OrthancPluginDatabaseTransaction* transaction,
+                                                        int64_t internalId);
+
+    OrthancPluginErrorCode (*logChange) (OrthancPluginDatabaseTransaction* transaction,
+                                         int32_t changeType,
+                                         int64_t resourceId,
+                                         OrthancPluginResourceType resourceType,
+                                         const char* date);
+
+    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);
+
+    /* Answer is read using "readAnswerAttachment()" */
+    OrthancPluginErrorCode (*lookupAttachment) (OrthancPluginDatabaseTransaction* transaction,
+                                                int64_t* revision /* out */,
+                                                int64_t resourceId,
+                                                int32_t contentType);
+
+    /* Answer is read using "readAnswerString()" */
+    OrthancPluginErrorCode (*lookupGlobalProperty) (OrthancPluginDatabaseTransaction* transaction,
+                                                    const char* serverIdentifier,
+                                                    int32_t property);
+    
+    /* Answer is read using "readAnswerString()" */
+    OrthancPluginErrorCode (*lookupMetadata) (OrthancPluginDatabaseTransaction* transaction,
+                                              int64_t* revision /* out */,
+                                              int64_t id,
+                                              int32_t metadata);
+    
+    OrthancPluginErrorCode (*lookupParent) (OrthancPluginDatabaseTransaction* transaction,
+                                            uint8_t* isExisting /* out */,
+                                            int64_t* parentId /* out */,
+                                            int64_t id);
+    
+    OrthancPluginErrorCode (*lookupResource) (OrthancPluginDatabaseTransaction* transaction,
+                                              uint8_t* isExisting /* out */,
+                                              int64_t* id /* out */,
+                                              OrthancPluginResourceType* type /* out */,
+                                              const char* publicId);
+    
+    /* Answers are read using "readAnswerMatchingResource()" */
+    OrthancPluginErrorCode  (*lookupResources) (OrthancPluginDatabaseTransaction* transaction,
+                                                uint32_t constraintsCount,
+                                                const OrthancPluginDatabaseConstraint* constraints,
+                                                OrthancPluginResourceType queryLevel,
+                                                uint32_t limit,
+                                                uint8_t requestSomeInstanceId);
+
+    /* The public ID of the parent resource is read using "readAnswerString()" */
+    OrthancPluginErrorCode (*lookupResourceAndParent) (OrthancPluginDatabaseTransaction* transaction,
+                                                       uint8_t* isExisting /* out */,
+                                                       int64_t* id /* out */,
+                                                       OrthancPluginResourceType* type /* out */,
+                                                       const char* publicId);
+
+    OrthancPluginErrorCode (*selectPatientToRecycle) (OrthancPluginDatabaseTransaction* transaction,
+                                                      uint8_t* patientAvailable /* out */,
+                                                      int64_t* patientId /* out */);
+    
+    OrthancPluginErrorCode (*selectPatientToRecycle2) (OrthancPluginDatabaseTransaction* transaction,
+                                                       uint8_t* patientAvailable /* out */,
+                                                       int64_t* patientId /* out */,
+                                                       int64_t patientIdToAvoid);
+
+    OrthancPluginErrorCode (*setGlobalProperty) (OrthancPluginDatabaseTransaction* transaction,
+                                                 const char* serverIdentifier,
+                                                 int32_t property,
+                                                 const char* value);
+
+    /* In "setMetadata()", the metadata might already be existing ("INSERT OR REPLACE")  */
+    OrthancPluginErrorCode (*setMetadata) (OrthancPluginDatabaseTransaction* transaction,
+                                           int64_t id,
+                                           int32_t metadata,
+                                           const char* value,
+                                           int64_t revision);
+    
+    OrthancPluginErrorCode (*setProtectedPatient) (OrthancPluginDatabaseTransaction* transaction,
+                                                   int64_t id,
+                                                   uint8_t isProtected);
+
+    OrthancPluginErrorCode  (*setResourcesContent) (OrthancPluginDatabaseTransaction* transaction,
+                                                    uint32_t countIdentifierTags,
+                                                    const OrthancPluginResourcesContentTags* identifierTags,
+                                                    uint32_t countMainDicomTags,
+                                                    const OrthancPluginResourcesContentTags* mainDicomTags,
+                                                    uint32_t countMetadata,
+                                                    const OrthancPluginResourcesContentMetadata* metadata);
+    
+
+  } OrthancPluginDatabaseBackendV4;
+
+/*<! @endcond */
   
+
+  typedef struct
+  {
+    const OrthancPluginDatabaseBackendV4*  backend;
+    uint32_t                               backendSize;
+    uint32_t                               maxDatabaseRetries;
+    void*                                  database;
+  } _OrthancPluginRegisterDatabaseBackendV4;
+
+
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterDatabaseBackendV4(
+    OrthancPluginContext*                  context,
+    const OrthancPluginDatabaseBackendV4*  backend,
+    uint32_t                               backendSize,
+    uint32_t                               maxDatabaseRetries,  /* To handle "OrthancPluginErrorCode_DatabaseCannotSerialize" */
+    void*                                  database)
+  {
+    _OrthancPluginRegisterDatabaseBackendV4 params;
+
+    if (sizeof(int32_t) != sizeof(_OrthancPluginDatabaseAnswerType))
+    {
+      return OrthancPluginErrorCode_Plugin;
+    }
+
+    memset(&params, 0, sizeof(params));
+    params.backend = backend;
+    params.backendSize = sizeof(OrthancPluginDatabaseBackendV4);
+    params.maxDatabaseRetries = maxDatabaseRetries;
+    params.database = database;
+
+    return context->InvokeService(context, _OrthancPluginService_RegisterDatabaseBackendV4, &params);
+  }
+
 #ifdef  __cplusplus
 }
 #endif
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Wed Sep 14 17:11:05 2022 +0200
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Wed Sep 14 17:11:45 2022 +0200
@@ -119,8 +119,8 @@
 #endif
 
 #define ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER     1
-#define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER     11
-#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER  2
+#define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER     12
+#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER  0
 
 
 #if !defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE)
@@ -536,6 +536,7 @@
     _OrthancPluginService_StorageAreaRead = 5004,
     _OrthancPluginService_StorageAreaRemove = 5005,
     _OrthancPluginService_RegisterDatabaseBackendV3 = 5006,  /* New in Orthanc 1.9.2 */
+    _OrthancPluginService_RegisterDatabaseBackendV4 = 5007,  /* New in Orthanc 1.12.0 */
 
     /* Primitives for handling images */
     _OrthancPluginService_GetImagePixelFormat = 6000,
--- a/OrthancServer/Sources/Database/IDatabaseWrapper.h	Wed Sep 14 17:11:05 2022 +0200
+++ b/OrthancServer/Sources/Database/IDatabaseWrapper.h	Wed Sep 14 17:11:45 2022 +0200
@@ -259,5 +259,7 @@
                          IStorageArea& storageArea) = 0;
 
     virtual bool HasRevisionsSupport() const = 0;
+
+    virtual bool HasAttachmentCustomDataSupport() const = 0;
   };
 }
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h	Wed Sep 14 17:11:05 2022 +0200
+++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h	Wed Sep 14 17:11:45 2022 +0200
@@ -97,6 +97,10 @@
       return false;  // TODO - REVISIONS
     }
 
+    virtual bool HasAttachmentCustomDataSupport() const ORTHANC_OVERRIDE
+    {
+      return true;
+    }
 
     /**
      * The "StartTransaction()" method is guaranteed to return a class
--- a/TODO	Wed Sep 14 17:11:05 2022 +0200
+++ b/TODO	Wed Sep 14 17:11:45 2022 +0200
@@ -1,5 +1,7 @@
 TODO_CUSTOM_DATA branch
 - add REVISIONS in SQLite since we change the DB schema
+- add revisions and customData in all Database plugins
+- check if we can play with GlobalProperty_DatabasePatchLevel instead of upgrading the DB version !
 - upgrade DB automatically such that it does not need a specific launch with --update , add --downgrade + --no-auto-upgrade command lines
 - expand the DB plugin SDK to handle customDATA
 - implement OrthancPluginDataBaseV4 and refuse to instantiate a PluginStorage3 if a DBv4 is not instantiated !