changeset 4630:ee8706477b61

integration db-changes->mainline
author Sebastien Jodogne <s.jodogne@gmail.com>
date Tue, 20 Apr 2021 18:11:29 +0200
parents 844ec5ecb6ef (current diff) 88e892e25a51 (diff)
children 37357df3dc27
files
diffstat 62 files changed, 10192 insertions(+), 6058 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Mon Apr 19 10:28:24 2021 +0200
+++ b/NEWS	Tue Apr 20 18:11:29 2021 +0200
@@ -1,12 +1,27 @@
 Pending changes in the mainline
 ===============================
 
+General
+-------
+
+* New configuration options related to multiple readers/writers:
+  - "DatabaseServerIdentifier" identifies the server in the DB among a pool of Orthanc servers
+  - "CheckRevisions" to protect against concurrent modifications of metadata and attachments
+
+REST API
+--------
+
+* API version upgraded to 12
+* "/system" reports the value of the "CheckRevisions" global option
+* "/.../{id}/metadata/{name}" and "/.../{id}/attachments/{name}/..." URIs handle the
+  HTTP headers "If-Match", "If-None-Match" and "ETag" to cope with revisions
 
 Plugins
 -------
 
-* New functions in the SDK:
-  - OrthancPluginCallRestApi()
+* New function in the SDK: OrthancPluginCallRestApi()
+* Full refactoring of the database engine to handle multiple readers/writers, which notably
+  implies the handling of retries in the case of collisions
 
 Maintenance
 -----------
--- a/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake	Tue Apr 20 18:11:29 2021 +0200
@@ -37,7 +37,7 @@
 # Version of the Orthanc API, can be retrieved from "/system" URI in
 # order to check whether new URI endpoints are available even if using
 # the mainline version of Orthanc
-set(ORTHANC_API_VERSION "11")
+set(ORTHANC_API_VERSION "12")
 
 
 #####################################################################
--- a/OrthancFramework/Resources/CodeGeneration/ErrorCodes.json	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancFramework/Resources/CodeGeneration/ErrorCodes.json	Tue Apr 20 18:11:29 2021 +0200
@@ -232,6 +232,18 @@
     "HttpStatus" : 416,
     "Name": "BadRange",
     "Description": "Incorrect range request"
+  },
+  {
+    "Code": 42,
+    "HttpStatus": 503,
+    "Name": "DatabaseCannotSerialize",
+    "Description": "Database could not serialize access due to concurrent update, the transaction should be retried"
+  }, 
+  {
+    "Code": 43,
+    "HttpStatus": 409,
+    "Name": "Revision",
+    "Description": "A bad revision number was provided, which might indicate conflict between multiple writers"
   }, 
 
 
--- a/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp	Tue Apr 20 18:11:29 2021 +0200
@@ -420,28 +420,6 @@
 #endif
 
   
-  static inline uint16_t GetCharValue(char c)
-  {
-    if (c >= '0' && c <= '9')
-      return c - '0';
-    else if (c >= 'a' && c <= 'f')
-      return c - 'a' + 10;
-    else if (c >= 'A' && c <= 'F')
-      return c - 'A' + 10;
-    else
-      return 0;
-  }
-
-  
-  static inline uint16_t GetTagValue(const char* c)
-  {
-    return ((GetCharValue(c[0]) << 12) + 
-            (GetCharValue(c[1]) << 8) + 
-            (GetCharValue(c[2]) << 4) + 
-            GetCharValue(c[3]));
-  }
-
-
 #if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
   void ParsedDicomFile::SendPathValue(RestApiOutput& output,
                                       const UriComponents& uri) const
--- a/OrthancFramework/Sources/Enumerations.cpp	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancFramework/Sources/Enumerations.cpp	Tue Apr 20 18:11:29 2021 +0200
@@ -187,6 +187,12 @@
       case ErrorCode_BadRange:
         return "Incorrect range request";
 
+      case ErrorCode_DatabaseCannotSerialize:
+        return "Database could not serialize access due to concurrent update, the transaction should be retried";
+
+      case ErrorCode_Revision:
+        return "A bad revision number was provided, which might indicate conflict between multiple writers";
+
       case ErrorCode_SQLiteNotOpened:
         return "SQLite: The database is not opened";
 
@@ -2141,6 +2147,12 @@
       case ErrorCode_BadRange:
         return HttpStatus_416_RequestedRangeNotSatisfiable;
 
+      case ErrorCode_DatabaseCannotSerialize:
+        return HttpStatus_503_ServiceUnavailable;
+
+      case ErrorCode_Revision:
+        return HttpStatus_409_Conflict;
+
       case ErrorCode_CreateDicomNotString:
         return HttpStatus_400_BadRequest;
 
--- a/OrthancFramework/Sources/Enumerations.h	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancFramework/Sources/Enumerations.h	Tue Apr 20 18:11:29 2021 +0200
@@ -135,6 +135,8 @@
     ErrorCode_SslInitialization = 39    /*!< Cannot initialize SSL encryption, check out your certificates */,
     ErrorCode_DiscontinuedAbi = 40    /*!< Calling a function that has been removed from the Orthanc Framework */,
     ErrorCode_BadRange = 41    /*!< Incorrect range request */,
+    ErrorCode_DatabaseCannotSerialize = 42    /*!< Database could not serialize access due to concurrent update, the transaction should be retried */,
+    ErrorCode_Revision = 43    /*!< A bad revision number was provided, which might indicate conflict between multiple writers */,
     ErrorCode_SQLiteNotOpened = 1000    /*!< SQLite: The database is not opened */,
     ErrorCode_SQLiteAlreadyOpened = 1001    /*!< SQLite: Connection is already open */,
     ErrorCode_SQLiteCannotOpen = 1002    /*!< SQLite: Unable to open the database */,
--- a/OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp	Tue Apr 20 18:11:29 2021 +0200
@@ -176,6 +176,21 @@
   }
 
 
+  RestApiCallDocumentation& RestApiCallDocumentation::SetAnswerHeader(const std::string& name,
+                                                                    const std::string& description)
+  {
+    if (answerHeaders_.find(name) != answerHeaders_.end())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange, "Answer HTTP header \"" + name + "\" is already documented");
+    }
+    else
+    {
+      answerHeaders_[name] = Parameter(Type_String, description, false);
+      return *this;
+    }
+  }
+
+
   void RestApiCallDocumentation::SetHttpGetSample(const std::string& url,
                                                   bool isJson)
   {
@@ -430,6 +445,20 @@
         target["responses"]["200"]["content"][EnumerationToString(MimeType_PlainText)]["example"] = sampleText_;
       }
 
+      if (!answerHeaders_.empty())
+      {
+        Json::Value answerHeaders = Json::objectValue;
+
+        for (Parameters::const_iterator it = answerHeaders_.begin(); it != answerHeaders_.end(); ++it)
+        {
+          Json::Value h = Json::objectValue;
+          h["description"] = it->second.GetDescription();          
+          answerHeaders[it->first] = h;
+        }
+
+        target["responses"]["200"]["headers"] = answerHeaders;
+      }
+
       Json::Value parameters = Json::arrayValue;
         
       for (Parameters::const_iterator it = getArguments_.begin();
--- a/OrthancFramework/Sources/RestApi/RestApiCallDocumentation.h	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancFramework/Sources/RestApi/RestApiCallDocumentation.h	Tue Apr 20 18:11:29 2021 +0200
@@ -102,6 +102,7 @@
     AllowedTypes  answerTypes_;
     Parameters    answerFields_;  // Only if JSON object
     std::string   answerDescription_;
+    Parameters    answerHeaders_;
     bool          hasSampleText_;
     std::string   sampleText_;
     Json::Value   sampleJson_;
@@ -172,6 +173,9 @@
                                              Type type,
                                              const std::string& description);
 
+    RestApiCallDocumentation& SetAnswerHeader(const std::string& name,
+                                              const std::string& description);
+
     void SetHttpGetSample(const std::string& url,
                           bool isJson);
 
--- a/OrthancFramework/Sources/RestApi/RestApiOutput.h	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancFramework/Sources/RestApi/RestApiOutput.h	Tue Apr 20 18:11:29 2021 +0200
@@ -59,6 +59,11 @@
       return convertJsonToXml_;
     }
 
+    HttpOutput& GetLowLevelOutput() const
+    {
+      return output_;
+    }
+
     void AnswerStream(IHttpStreamAnswer& stream);
 
     void AnswerJson(const Json::Value& value);
--- a/OrthancFramework/Sources/Toolbox.cpp	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancFramework/Sources/Toolbox.cpp	Tue Apr 20 18:11:29 2021 +0200
@@ -2385,6 +2385,17 @@
     target = Json::writeString(builder, source);
 #endif
   }
+
+
+  void Toolbox::RemoveSurroundingQuotes(std::string& value)
+  {
+    if (!value.empty() &&
+        value[0] == '\"' &&
+        value[value.size() - 1] == '\"')
+    {
+      value = value.substr(1, value.size() - 2);
+    }
+  }
 }
 
 
--- a/OrthancFramework/Sources/Toolbox.h	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancFramework/Sources/Toolbox.h	Tue Apr 20 18:11:29 2021 +0200
@@ -280,6 +280,8 @@
 
     static void WriteStyledJson(std::string& target,
                                 const Json::Value& source);
+
+    static void RemoveSurroundingQuotes(std::string& value);
   };
 }
 
--- a/OrthancFramework/UnitTestsSources/FrameworkTests.cpp	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancFramework/UnitTestsSources/FrameworkTests.cpp	Tue Apr 20 18:11:29 2021 +0200
@@ -401,6 +401,27 @@
   ASSERT_EQ("coucou", Toolbox::StripSpaces("    coucou   \t  \r   \n  "));
   ASSERT_EQ("cou   cou", Toolbox::StripSpaces("    cou   cou    \n  "));
   ASSERT_EQ("c", Toolbox::StripSpaces("    \n\t c\r    \n  "));
+
+  std::string s = "\"  abd \"";
+  Toolbox::RemoveSurroundingQuotes(s); ASSERT_EQ("  abd ", s);
+
+  s = "  \"  abd \"  ";
+  Toolbox::RemoveSurroundingQuotes(s); ASSERT_EQ("  \"  abd \"  ", s);
+
+  s = Toolbox::StripSpaces(s);
+  Toolbox::RemoveSurroundingQuotes(s); ASSERT_EQ("  abd ", s);
+
+  s = "\"";
+  Toolbox::RemoveSurroundingQuotes(s); ASSERT_EQ("", s);  
+
+  s = "\"\"";
+  Toolbox::RemoveSurroundingQuotes(s); ASSERT_EQ("", s);  
+
+  s = "\"_\"";
+  Toolbox::RemoveSurroundingQuotes(s); ASSERT_EQ("_", s);
+
+  s = "\"\"\"";
+  Toolbox::RemoveSurroundingQuotes(s); ASSERT_EQ("\"", s);
 }
 
 TEST(Toolbox, Case)
--- a/OrthancServer/CMakeLists.txt	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancServer/CMakeLists.txt	Tue Apr 20 18:11:29 2021 +0200
@@ -98,6 +98,8 @@
   ${CMAKE_SOURCE_DIR}/Sources/Database/Compatibility/SetOfResources.cpp
   ${CMAKE_SOURCE_DIR}/Sources/Database/ResourcesContent.cpp
   ${CMAKE_SOURCE_DIR}/Sources/Database/SQLiteDatabaseWrapper.cpp
+  ${CMAKE_SOURCE_DIR}/Sources/Database/StatelessDatabaseOperations.cpp
+  ${CMAKE_SOURCE_DIR}/Sources/Database/VoidDatabaseListener.cpp
   ${CMAKE_SOURCE_DIR}/Sources/DicomInstanceOrigin.cpp
   ${CMAKE_SOURCE_DIR}/Sources/DicomInstanceToStore.cpp
   ${CMAKE_SOURCE_DIR}/Sources/EmbeddedResourceHttpHandler.cpp
@@ -186,6 +188,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/OrthancPlugins.cpp
     ${CMAKE_SOURCE_DIR}/Plugins/Engine/PluginsEnumerations.cpp
     ${CMAKE_SOURCE_DIR}/Plugins/Engine/PluginsErrorDictionary.cpp
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp	Tue Apr 20 18:11:29 2021 +0200
@@ -35,39 +35,231 @@
 #include "OrthancPluginDatabase.h"
 
 #if ORTHANC_ENABLE_PLUGINS != 1
-#error The plugin support is disabled
+#  error The plugin support is disabled
 #endif
 
 
 #include "../../../OrthancFramework/Sources/Logging.h"
 #include "../../../OrthancFramework/Sources/OrthancException.h"
+#include "../../Sources/Database/Compatibility/ICreateInstance.h"
+#include "../../Sources/Database/Compatibility/IGetChildrenMetadata.h"
+#include "../../Sources/Database/Compatibility/ILookupResourceAndParent.h"
+#include "../../Sources/Database/Compatibility/ILookupResources.h"
+#include "../../Sources/Database/Compatibility/ISetResourcesContent.h"
+#include "../../Sources/Database/VoidDatabaseListener.h"
 #include "PluginsEnumerations.h"
 
 #include <cassert>
 
 namespace Orthanc
 {
-  class OrthancPluginDatabase::Transaction : public IDatabaseWrapper::ITransaction
+  class OrthancPluginDatabase::Transaction :
+    public IDatabaseWrapper::ITransaction,
+    public Compatibility::ICreateInstance,
+    public Compatibility::IGetChildrenMetadata,
+    public Compatibility::ILookupResources,
+    public Compatibility::ILookupResourceAndParent,
+    public Compatibility::ISetResourcesContent
   {
   private:
-    OrthancPluginDatabase&  that_;
+    typedef std::pair<int64_t, ResourceType>     AnswerResource;
+    typedef std::map<MetadataType, std::string>  AnswerMetadata;
+
+    OrthancPluginDatabase&               that_;
+    boost::recursive_mutex::scoped_lock  lock_;
+    IDatabaseListener&                   listener_;
+    _OrthancPluginDatabaseAnswerType     type_;
+
+    std::list<std::string>         answerStrings_;
+    std::list<int32_t>             answerInt32_;
+    std::list<int64_t>             answerInt64_;
+    std::list<AnswerResource>      answerResources_;
+    std::list<FileInfo>            answerAttachments_;
+
+    DicomMap*                      answerDicomMap_;
+    std::list<ServerIndexChange>*  answerChanges_;
+    std::list<ExportedResource>*   answerExportedResources_;
+    bool*                          answerDone_;
+    bool                           answerDoneIgnored_;
+    std::list<std::string>*        answerMatchingResources_;
+    std::list<std::string>*        answerMatchingInstances_;
+    AnswerMetadata*                answerMetadata_;
 
     void CheckSuccess(OrthancPluginErrorCode code) const
     {
-      if (code != OrthancPluginErrorCode_Success)
+      that_.CheckSuccess(code);
+    }
+
+    
+    static FileInfo Convert(const OrthancPluginAttachment& attachment)
+    {
+      return FileInfo(attachment.uuid,
+                      static_cast<FileContentType>(attachment.contentType),
+                      attachment.uncompressedSize,
+                      attachment.uncompressedHash,
+                      static_cast<CompressionType>(attachment.compressionType),
+                      attachment.compressedSize,
+                      attachment.compressedHash);
+    }
+
+
+    void ResetAnswers()
+    {
+      type_ = _OrthancPluginDatabaseAnswerType_None;
+
+      answerDicomMap_ = NULL;
+      answerChanges_ = NULL;
+      answerExportedResources_ = NULL;
+      answerDone_ = NULL;
+      answerMatchingResources_ = NULL;
+      answerMatchingInstances_ = NULL;
+      answerMetadata_ = NULL;
+    }
+
+
+    void ForwardAnswers(std::list<int64_t>& target)
+    {
+      if (type_ != _OrthancPluginDatabaseAnswerType_None &&
+          type_ != _OrthancPluginDatabaseAnswerType_Int64)
       {
-        that_.errorDictionary_.LogError(code, true);
-        throw OrthancException(static_cast<ErrorCode>(code));
+        throw OrthancException(ErrorCode_DatabasePlugin);
+      }
+
+      target.clear();
+
+      if (type_ == _OrthancPluginDatabaseAnswerType_Int64)
+      {
+        for (std::list<int64_t>::const_iterator 
+               it = answerInt64_.begin(); it != answerInt64_.end(); ++it)
+        {
+          target.push_back(*it);
+        }
+      }
+    }
+
+
+    void ForwardAnswers(std::list<std::string>& target)
+    {
+      if (type_ != _OrthancPluginDatabaseAnswerType_None &&
+          type_ != _OrthancPluginDatabaseAnswerType_String)
+      {
+        throw OrthancException(ErrorCode_DatabasePlugin);
+      }
+
+      target.clear();
+
+      if (type_ == _OrthancPluginDatabaseAnswerType_String)
+      {
+        for (std::list<std::string>::const_iterator 
+               it = answerStrings_.begin(); it != answerStrings_.end(); ++it)
+        {
+          target.push_back(*it);
+        }
       }
     }
 
-  public:
-    explicit Transaction(OrthancPluginDatabase& that) :
-      that_(that)
+
+    bool ForwardSingleAnswer(std::string& target)
     {
+      if (type_ == _OrthancPluginDatabaseAnswerType_None)
+      {
+        return false;
+      }
+      else if (type_ == _OrthancPluginDatabaseAnswerType_String &&
+               answerStrings_.size() == 1)
+      {
+        target = answerStrings_.front();
+        return true; 
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_DatabasePlugin);
+      }
+    }
+
+
+    bool ForwardSingleAnswer(int64_t& target)
+    {
+      if (type_ == _OrthancPluginDatabaseAnswerType_None)
+      {
+        return false;
+      }
+      else if (type_ == _OrthancPluginDatabaseAnswerType_Int64 &&
+               answerInt64_.size() == 1)
+      {
+        target = answerInt64_.front();
+        return true; 
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_DatabasePlugin);
+      }
     }
 
-    virtual void Begin() ORTHANC_OVERRIDE
+
+    void ProcessEvent(const _OrthancPluginDatabaseAnswer& answer)
+    {
+      switch (answer.type)
+      {
+        case _OrthancPluginDatabaseAnswerType_DeletedAttachment:
+        {
+          const OrthancPluginAttachment& attachment = 
+            *reinterpret_cast<const OrthancPluginAttachment*>(answer.valueGeneric);
+          listener_.SignalAttachmentDeleted(Convert(attachment));
+          break;
+        }
+        
+        case _OrthancPluginDatabaseAnswerType_RemainingAncestor:
+        {
+          ResourceType type = Plugins::Convert(static_cast<OrthancPluginResourceType>(answer.valueInt32));
+          listener_.SignalRemainingAncestor(type, answer.valueString);
+          break;
+        }
+      
+        case _OrthancPluginDatabaseAnswerType_DeletedResource:
+        {
+          ResourceType type = Plugins::Convert(static_cast<OrthancPluginResourceType>(answer.valueInt32));
+          listener_.SignalResourceDeleted(type, answer.valueString);
+          break;
+        }
+
+        default:
+          throw OrthancException(ErrorCode_DatabasePlugin);
+      }
+    }
+
+
+  public:
+    explicit Transaction(OrthancPluginDatabase& that,
+                         IDatabaseListener& listener) :
+      that_(that),
+      lock_(that.mutex_),
+      listener_(listener),
+      type_(_OrthancPluginDatabaseAnswerType_None),
+      answerDoneIgnored_(false)
+    {
+      if (that_.activeTransaction_ != NULL)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+      
+      that_.activeTransaction_ = this;
+
+      ResetAnswers();
+    }
+
+    virtual ~Transaction()
+    {
+      assert(that_.activeTransaction_ != NULL);    
+      that_.activeTransaction_ = NULL;
+    }
+
+    IDatabaseListener& GetDatabaseListener() const
+    {
+      return listener_;
+    }
+
+    void Begin()
     {
       CheckSuccess(that_.backend_.startTransaction(that_.payload_));
     }
@@ -92,7 +284,7 @@
 
         uint64_t newDiskSize = (that_.currentDiskSize_ + diskSizeDelta);
 
-        assert(newDiskSize == that_.GetTotalCompressedSize());
+        assert(newDiskSize == GetTotalCompressedSize());
 
         CheckSuccess(that_.backend_.commitTransaction(that_.payload_));
 
@@ -100,19 +292,1144 @@
         that_.currentDiskSize_ = newDiskSize;
       }
     }
-  };
+
+
+    void AnswerReceived(const _OrthancPluginDatabaseAnswer& answer)
+    {
+      if (answer.type == _OrthancPluginDatabaseAnswerType_None)
+      {
+        throw OrthancException(ErrorCode_DatabasePlugin);
+      }
+
+      if (answer.type == _OrthancPluginDatabaseAnswerType_DeletedAttachment ||
+          answer.type == _OrthancPluginDatabaseAnswerType_DeletedResource ||
+          answer.type == _OrthancPluginDatabaseAnswerType_RemainingAncestor)
+      {
+        ProcessEvent(answer);
+        return;
+      }
+
+      if (type_ == _OrthancPluginDatabaseAnswerType_None)
+      {
+        type_ = answer.type;
+
+        switch (type_)
+        {
+          case _OrthancPluginDatabaseAnswerType_Int32:
+            answerInt32_.clear();
+            break;
+
+          case _OrthancPluginDatabaseAnswerType_Int64:
+            answerInt64_.clear();
+            break;
+
+          case _OrthancPluginDatabaseAnswerType_Resource:
+            answerResources_.clear();
+            break;
+
+          case _OrthancPluginDatabaseAnswerType_Attachment:
+            answerAttachments_.clear();
+            break;
+
+          case _OrthancPluginDatabaseAnswerType_String:
+            answerStrings_.clear();
+            break;
+
+          case _OrthancPluginDatabaseAnswerType_DicomTag:
+            assert(answerDicomMap_ != NULL);
+            answerDicomMap_->Clear();
+            break;
+
+          case _OrthancPluginDatabaseAnswerType_Change:
+            assert(answerChanges_ != NULL);
+            answerChanges_->clear();
+            break;
+
+          case _OrthancPluginDatabaseAnswerType_ExportedResource:
+            assert(answerExportedResources_ != NULL);
+            answerExportedResources_->clear();
+            break;
+
+          case _OrthancPluginDatabaseAnswerType_MatchingResource:
+            assert(answerMatchingResources_ != NULL);
+            answerMatchingResources_->clear();
+
+            if (answerMatchingInstances_ != NULL)
+            {
+              answerMatchingInstances_->clear();
+            }
+          
+            break;
+
+          case _OrthancPluginDatabaseAnswerType_Metadata:
+            assert(answerMetadata_ != NULL);
+            answerMetadata_->clear();
+            break;
+
+          default:
+            throw OrthancException(ErrorCode_DatabasePlugin,
+                                   "Unhandled type of answer for custom index plugin: " +
+                                   boost::lexical_cast<std::string>(answer.type));
+        }
+      }
+      else if (type_ != answer.type)
+      {
+        throw OrthancException(ErrorCode_DatabasePlugin,
+                               "Error in the plugin protocol: Cannot change the answer type");
+      }
+
+      switch (answer.type)
+      {
+        case _OrthancPluginDatabaseAnswerType_Int32:
+        {
+          answerInt32_.push_back(answer.valueInt32);
+          break;
+        }
+
+        case _OrthancPluginDatabaseAnswerType_Int64:
+        {
+          answerInt64_.push_back(answer.valueInt64);
+          break;
+        }
+
+        case _OrthancPluginDatabaseAnswerType_Resource:
+        {
+          OrthancPluginResourceType type = static_cast<OrthancPluginResourceType>(answer.valueInt32);
+          answerResources_.push_back(std::make_pair(answer.valueInt64, Plugins::Convert(type)));
+          break;
+        }
+
+        case _OrthancPluginDatabaseAnswerType_Attachment:
+        {
+          const OrthancPluginAttachment& attachment = 
+            *reinterpret_cast<const OrthancPluginAttachment*>(answer.valueGeneric);
+
+          answerAttachments_.push_back(Convert(attachment));
+          break;
+        }
+
+        case _OrthancPluginDatabaseAnswerType_DicomTag:
+        {
+          const OrthancPluginDicomTag& tag = *reinterpret_cast<const OrthancPluginDicomTag*>(answer.valueGeneric);
+          assert(answerDicomMap_ != NULL);
+          answerDicomMap_->SetValue(tag.group, tag.element, std::string(tag.value), false);
+          break;
+        }
+
+        case _OrthancPluginDatabaseAnswerType_String:
+        {
+          if (answer.valueString == NULL)
+          {
+            throw OrthancException(ErrorCode_DatabasePlugin);
+          }
+
+          if (type_ == _OrthancPluginDatabaseAnswerType_None)
+          {
+            type_ = _OrthancPluginDatabaseAnswerType_String;
+            answerStrings_.clear();
+          }
+          else if (type_ != _OrthancPluginDatabaseAnswerType_String)
+          {
+            throw OrthancException(ErrorCode_DatabasePlugin);
+          }
+
+          answerStrings_.push_back(std::string(answer.valueString));
+          break;
+        }
+
+        case _OrthancPluginDatabaseAnswerType_Change:
+        {
+          assert(answerDone_ != NULL);
+          if (answer.valueUint32 == 1)
+          {
+            *answerDone_ = true;
+          }
+          else if (*answerDone_)
+          {
+            throw OrthancException(ErrorCode_DatabasePlugin);
+          }
+          else
+          {
+            const OrthancPluginChange& change =
+              *reinterpret_cast<const OrthancPluginChange*>(answer.valueGeneric);
+            assert(answerChanges_ != NULL);
+            answerChanges_->push_back
+              (ServerIndexChange(change.seq,
+                                 static_cast<ChangeType>(change.changeType),
+                                 Plugins::Convert(change.resourceType),
+                                 change.publicId,
+                                 change.date));                                   
+          }
+
+          break;
+        }
+
+        case _OrthancPluginDatabaseAnswerType_ExportedResource:
+        {
+          assert(answerDone_ != NULL);
+          if (answer.valueUint32 == 1)
+          {
+            *answerDone_ = true;
+          }
+          else if (*answerDone_)
+          {
+            throw OrthancException(ErrorCode_DatabasePlugin);
+          }
+          else
+          {
+            const OrthancPluginExportedResource& exported = 
+              *reinterpret_cast<const OrthancPluginExportedResource*>(answer.valueGeneric);
+            assert(answerExportedResources_ != NULL);
+            answerExportedResources_->push_back
+              (ExportedResource(exported.seq,
+                                Plugins::Convert(exported.resourceType),
+                                exported.publicId,
+                                exported.modality,
+                                exported.date,
+                                exported.patientId,
+                                exported.studyInstanceUid,
+                                exported.seriesInstanceUid,
+                                exported.sopInstanceUid));
+          }
+
+          break;
+        }
+
+        case _OrthancPluginDatabaseAnswerType_MatchingResource:
+        {
+          const OrthancPluginMatchingResource& match = 
+            *reinterpret_cast<const OrthancPluginMatchingResource*>(answer.valueGeneric);
+
+          if (match.resourceId == NULL)
+          {
+            throw OrthancException(ErrorCode_DatabasePlugin);
+          }
+
+          assert(answerMatchingResources_ != NULL);
+          answerMatchingResources_->push_back(match.resourceId);
+
+          if (answerMatchingInstances_ != NULL)
+          {
+            if (match.someInstanceId == NULL)
+            {
+              throw OrthancException(ErrorCode_DatabasePlugin);
+            }
+
+            answerMatchingInstances_->push_back(match.someInstanceId);
+          }
+ 
+          break;
+        }
+
+        case _OrthancPluginDatabaseAnswerType_Metadata:
+        {
+          const OrthancPluginResourcesContentMetadata& metadata =
+            *reinterpret_cast<const OrthancPluginResourcesContentMetadata*>(answer.valueGeneric);
+
+          MetadataType type = static_cast<MetadataType>(metadata.metadata);
+
+          if (metadata.value == NULL)
+          {
+            throw OrthancException(ErrorCode_DatabasePlugin);
+          }
+
+          assert(answerMetadata_ != NULL &&
+                 answerMetadata_->find(type) == answerMetadata_->end());
+          (*answerMetadata_) [type] = metadata.value;
+          break;
+        }
+
+        default:
+          throw OrthancException(ErrorCode_DatabasePlugin,
+                                 "Unhandled type of answer for custom index plugin: " +
+                                 boost::lexical_cast<std::string>(answer.type));
+      }
+    }
+    
+
+    // From the "ILookupResources" interface
+    virtual void LookupIdentifier(std::list<int64_t>& result,
+                                  ResourceType level,
+                                  const DicomTag& tag,
+                                  Compatibility::IdentifierConstraintType type,
+                                  const std::string& value) ORTHANC_OVERRIDE
+    {
+      if (that_.extensions_.lookupIdentifier3 == NULL)
+      {
+        throw OrthancException(ErrorCode_DatabasePlugin,
+                               "The database plugin does not implement the mandatory LookupIdentifier3() extension");
+      }
+
+      OrthancPluginDicomTag tmp;
+      tmp.group = tag.GetGroup();
+      tmp.element = tag.GetElement();
+      tmp.value = value.c_str();
+
+      ResetAnswers();
+      CheckSuccess(that_.extensions_.lookupIdentifier3(that_.GetContext(), that_.payload_, Plugins::Convert(level),
+                                                       &tmp, Compatibility::Convert(type)));
+      ForwardAnswers(result);
+    }
+
+    
+    virtual void ApplyLookupResources(std::list<std::string>& resourcesId,
+                                      std::list<std::string>* instancesId,
+                                      const std::vector<DatabaseConstraint>& lookup,
+                                      ResourceType queryLevel,
+                                      size_t limit) ORTHANC_OVERRIDE
+    {
+      if (that_.extensions_.lookupResources == NULL)
+      {
+        // Fallback to compatibility mode
+        ILookupResources::Apply
+          (*this, *this, resourcesId, instancesId, lookup, queryLevel, limit);
+      }
+      else
+      {
+        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]);
+        }
+
+        ResetAnswers();
+        answerMatchingResources_ = &resourcesId;
+        answerMatchingInstances_ = instancesId;
+      
+        CheckSuccess(that_.extensions_.lookupResources(that_.GetContext(), that_.payload_, lookup.size(),
+                                                       (lookup.empty() ? NULL : &constraints[0]),
+                                                       Plugins::Convert(queryLevel),
+                                                       limit, (instancesId == NULL ? 0 : 1)));
+      }
+    }
+
+
+    virtual bool CreateInstance(IDatabaseWrapper::CreateInstanceResult& result,
+                                int64_t& instanceId,
+                                const std::string& patient,
+                                const std::string& study,
+                                const std::string& series,
+                                const std::string& instance) ORTHANC_OVERRIDE
+    {
+      if (that_.extensions_.createInstance == NULL)
+      {
+        // Fallback to compatibility mode
+        return ICreateInstance::Apply
+          (*this, result, instanceId, patient, study, series, instance);
+      }
+      else
+      {
+        OrthancPluginCreateInstanceResult output;
+        memset(&output, 0, sizeof(output));
+
+        CheckSuccess(that_.extensions_.createInstance(&output, that_.payload_, patient.c_str(),
+                                                      study.c_str(), series.c_str(), instance.c_str()));
+
+        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 AddAttachment(int64_t id,
+                               const FileInfo& attachment,
+                               int64_t revision) ORTHANC_OVERRIDE
+    {
+      // "revision" is not used, as it was added in Orthanc 1.9.2
+      OrthancPluginAttachment 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();
+
+      CheckSuccess(that_.backend_.addAttachment(that_.payload_, id, &tmp));
+    }
+
+
+    // From the "ICreateInstance" interface
+    virtual void AttachChild(int64_t parent,
+                             int64_t child) ORTHANC_OVERRIDE
+    {
+      CheckSuccess(that_.backend_.attachChild(that_.payload_, parent, child));
+    }
+
+
+    virtual void ClearChanges() ORTHANC_OVERRIDE
+    {
+      CheckSuccess(that_.backend_.clearChanges(that_.payload_));
+    }
+
+
+    virtual void ClearExportedResources() ORTHANC_OVERRIDE
+    {
+      CheckSuccess(that_.backend_.clearExportedResources(that_.payload_));
+    }
+
+
+    virtual void ClearMainDicomTags(int64_t id) ORTHANC_OVERRIDE
+    {
+      if (that_.extensions_.clearMainDicomTags == NULL)
+      {
+        throw OrthancException(ErrorCode_DatabasePlugin,
+                               "Your custom index plugin does not implement the mandatory ClearMainDicomTags() extension");
+      }
+
+      CheckSuccess(that_.extensions_.clearMainDicomTags(that_.payload_, id));
+    }
+
+
+    // From the "ICreateInstance" interface
+    virtual int64_t CreateResource(const std::string& publicId,
+                                   ResourceType type) ORTHANC_OVERRIDE
+    {
+      int64_t id;
+      CheckSuccess(that_.backend_.createResource(&id, that_.payload_, publicId.c_str(), Plugins::Convert(type)));
+      return id;
+    }
+
+
+    virtual void DeleteAttachment(int64_t id,
+                                  FileContentType attachment) ORTHANC_OVERRIDE
+    {
+      CheckSuccess(that_.backend_.deleteAttachment(that_.payload_, id, static_cast<int32_t>(attachment)));
+    }
+
+
+    virtual void DeleteMetadata(int64_t id,
+                                MetadataType type) ORTHANC_OVERRIDE
+    {
+      CheckSuccess(that_.backend_.deleteMetadata(that_.payload_, id, static_cast<int32_t>(type)));
+    }
+
+
+    virtual void DeleteResource(int64_t id) ORTHANC_OVERRIDE
+    {
+      CheckSuccess(that_.backend_.deleteResource(that_.payload_, id));
+    }
+
+
+    // From the "ILookupResources" interface
+    void GetAllInternalIds(std::list<int64_t>& target,
+                           ResourceType resourceType) ORTHANC_OVERRIDE
+    {
+      if (that_.extensions_.getAllInternalIds == NULL)
+      {
+        throw OrthancException(ErrorCode_DatabasePlugin,
+                               "The database plugin does not implement the mandatory GetAllInternalIds() extension");
+      }
+
+      ResetAnswers();
+      CheckSuccess(that_.extensions_.getAllInternalIds(that_.GetContext(), that_.payload_, Plugins::Convert(resourceType)));
+      ForwardAnswers(target);
+    }
+
+
+
+    virtual void GetAllMetadata(std::map<MetadataType, std::string>& target,
+                                int64_t id) ORTHANC_OVERRIDE
+    {
+      if (that_.extensions_.getAllMetadata == NULL)
+      {
+        // Fallback implementation if extension is missing
+        target.clear();
+
+        ResetAnswers();
+        CheckSuccess(that_.backend_.listAvailableMetadata(that_.GetContext(), that_.payload_, id));
+
+        if (type_ != _OrthancPluginDatabaseAnswerType_None &&
+            type_ != _OrthancPluginDatabaseAnswerType_Int32)
+        {
+          throw OrthancException(ErrorCode_DatabasePlugin);
+        }
+
+        target.clear();
+
+        if (type_ == _OrthancPluginDatabaseAnswerType_Int32)
+        {
+          for (std::list<int32_t>::const_iterator 
+                 it = answerInt32_.begin(); it != answerInt32_.end(); ++it)
+          {
+            MetadataType type = static_cast<MetadataType>(*it);
+
+            std::string value;
+            int64_t revision;  // Ignored
+            if (LookupMetadata(value, revision, id, type))
+            {
+              target[type] = value;
+            }
+          }
+        }
+      }
+      else
+      {
+        ResetAnswers();
+
+        answerMetadata_ = &target;
+        target.clear();
+      
+        CheckSuccess(that_.extensions_.getAllMetadata(that_.GetContext(), that_.payload_, id));
+
+        if (type_ != _OrthancPluginDatabaseAnswerType_None &&
+            type_ != _OrthancPluginDatabaseAnswerType_Metadata)
+        {
+          throw OrthancException(ErrorCode_DatabasePlugin);
+        }
+      }
+    }
+
+
+    virtual void GetAllPublicIds(std::list<std::string>& target,
+                                 ResourceType resourceType) ORTHANC_OVERRIDE
+    {
+      ResetAnswers();
+      CheckSuccess(that_.backend_.getAllPublicIds(that_.GetContext(), that_.payload_, Plugins::Convert(resourceType)));
+      ForwardAnswers(target);
+    }
+
+
+    virtual void GetAllPublicIds(std::list<std::string>& target,
+                                 ResourceType resourceType,
+                                 size_t since,
+                                 size_t limit) ORTHANC_OVERRIDE
+    {
+      if (that_.extensions_.getAllPublicIdsWithLimit != NULL)
+      {
+        // This extension is available since Orthanc 0.9.4
+        ResetAnswers();
+        CheckSuccess(that_.extensions_.getAllPublicIdsWithLimit
+                     (that_.GetContext(), that_.payload_, Plugins::Convert(resourceType), since, limit));
+        ForwardAnswers(target);
+      }
+      else
+      {
+        // The extension is not available in the database plugin, use a
+        // fallback implementation
+        target.clear();
+
+        if (limit == 0)
+        {
+          return;
+        }
+
+        std::list<std::string> tmp;
+        GetAllPublicIds(tmp, resourceType);
+    
+        if (tmp.size() <= since)
+        {
+          // Not enough results => empty answer
+          return;
+        }
+
+        std::list<std::string>::iterator current = tmp.begin();
+        std::advance(current, since);
+
+        while (limit > 0 && current != tmp.end())
+        {
+          target.push_back(*current);
+          --limit;
+          ++current;
+        }
+      }
+    }
 
 
-  static FileInfo Convert(const OrthancPluginAttachment& attachment)
-  {
-    return FileInfo(attachment.uuid,
-                    static_cast<FileContentType>(attachment.contentType),
-                    attachment.uncompressedSize,
-                    attachment.uncompressedHash,
-                    static_cast<CompressionType>(attachment.compressionType),
-                    attachment.compressedSize,
-                    attachment.compressedHash);
-  }
+    virtual void GetChanges(std::list<ServerIndexChange>& target /*out*/,
+                            bool& done /*out*/,
+                            int64_t since,
+                            uint32_t maxResults) ORTHANC_OVERRIDE
+    {
+      ResetAnswers();
+      answerChanges_ = &target;
+      answerDone_ = &done;
+      done = false;
+
+      CheckSuccess(that_.backend_.getChanges(that_.GetContext(), that_.payload_, since, maxResults));
+    }
+
+
+    virtual void GetChildrenInternalId(std::list<int64_t>& target,
+                                       int64_t id) ORTHANC_OVERRIDE
+    {
+      ResetAnswers();
+      CheckSuccess(that_.backend_.getChildrenInternalId(that_.GetContext(), that_.payload_, id));
+      ForwardAnswers(target);
+    }
+
+
+    virtual void GetChildrenMetadata(std::list<std::string>& target,
+                                     int64_t resourceId,
+                                     MetadataType metadata) ORTHANC_OVERRIDE
+    {
+      if (that_.extensions_.getChildrenMetadata == NULL)
+      {
+        IGetChildrenMetadata::Apply(*this, target, resourceId, metadata);
+      }
+      else
+      {
+        ResetAnswers();
+        CheckSuccess(that_.extensions_.getChildrenMetadata
+                     (that_.GetContext(), that_.payload_, resourceId, static_cast<int32_t>(metadata)));
+        ForwardAnswers(target);
+      }
+    }
+
+
+    virtual void GetChildrenPublicId(std::list<std::string>& target,
+                                     int64_t id) ORTHANC_OVERRIDE
+    {
+      ResetAnswers();
+      CheckSuccess(that_.backend_.getChildrenPublicId(that_.GetContext(), that_.payload_, id));
+      ForwardAnswers(target);
+    }
+
+
+    virtual void GetExportedResources(std::list<ExportedResource>& target /*out*/,
+                                      bool& done /*out*/,
+                                      int64_t since,
+                                      uint32_t maxResults) ORTHANC_OVERRIDE
+    {
+      ResetAnswers();
+      answerExportedResources_ = &target;
+      answerDone_ = &done;
+      done = false;
+
+      CheckSuccess(that_.backend_.getExportedResources(that_.GetContext(), that_.payload_, since, maxResults));
+    }
+
+
+    virtual void GetLastChange(std::list<ServerIndexChange>& target /*out*/) ORTHANC_OVERRIDE
+    {
+      answerDoneIgnored_ = false;
+
+      ResetAnswers();
+      answerChanges_ = &target;
+      answerDone_ = &answerDoneIgnored_;
+
+      CheckSuccess(that_.backend_.getLastChange(that_.GetContext(), that_.payload_));
+    }
+
+
+    int64_t GetLastChangeIndex() ORTHANC_OVERRIDE
+    {
+      if (that_.extensions_.getLastChangeIndex == NULL)
+      {
+        // This was the default behavior in Orthanc <= 1.5.1
+        // https://groups.google.com/d/msg/orthanc-users/QhzB6vxYeZ0/YxabgqpfBAAJ
+        return 0;
+      }
+      else
+      {
+        int64_t result = 0;
+        CheckSuccess(that_.extensions_.getLastChangeIndex(&result, that_.payload_));
+        return result;
+      }
+    }
+
+  
+    virtual void GetLastExportedResource(std::list<ExportedResource>& target /*out*/) ORTHANC_OVERRIDE
+    {
+      answerDoneIgnored_ = false;
+
+      ResetAnswers();
+      answerExportedResources_ = &target;
+      answerDone_ = &answerDoneIgnored_;
+
+      CheckSuccess(that_.backend_.getLastExportedResource(that_.GetContext(), that_.payload_));
+    }
+
+
+    virtual void GetMainDicomTags(DicomMap& map,
+                                  int64_t id) ORTHANC_OVERRIDE
+    {
+      ResetAnswers();
+      answerDicomMap_ = &map;
+
+      CheckSuccess(that_.backend_.getMainDicomTags(that_.GetContext(), that_.payload_, id));
+    }
+
+
+    virtual std::string GetPublicId(int64_t resourceId) ORTHANC_OVERRIDE
+    {
+      ResetAnswers();
+      std::string s;
+
+      CheckSuccess(that_.backend_.getPublicId(that_.GetContext(), that_.payload_, resourceId));
+
+      if (!ForwardSingleAnswer(s))
+      {
+        throw OrthancException(ErrorCode_DatabasePlugin);
+      }
+
+      return s;
+    }
+
+
+    virtual uint64_t GetResourcesCount(ResourceType resourceType) ORTHANC_OVERRIDE
+    {
+      uint64_t count;
+      CheckSuccess(that_.backend_.getResourceCount(&count, that_.payload_, Plugins::Convert(resourceType)));
+      return count;
+    }
+
+
+    virtual ResourceType GetResourceType(int64_t resourceId) ORTHANC_OVERRIDE
+    {
+      OrthancPluginResourceType type;
+      CheckSuccess(that_.backend_.getResourceType(&type, that_.payload_, resourceId));
+      return Plugins::Convert(type);
+    }
+
+
+    virtual uint64_t GetTotalCompressedSize() ORTHANC_OVERRIDE
+    {
+      uint64_t size;
+      CheckSuccess(that_.backend_.getTotalCompressedSize(&size, that_.payload_));
+      return size;
+    }
+
+    
+    virtual uint64_t GetTotalUncompressedSize() ORTHANC_OVERRIDE
+    {
+      uint64_t size;
+      CheckSuccess(that_.backend_.getTotalUncompressedSize(&size, that_.payload_));
+      return size;
+    }
+    
+
+    virtual bool IsDiskSizeAbove(uint64_t threshold) ORTHANC_OVERRIDE
+    {
+      if (that_.fastGetTotalSize_)
+      {
+        return GetTotalCompressedSize() > threshold;
+      }
+      else
+      {
+        assert(GetTotalCompressedSize() == that_.currentDiskSize_);
+        return that_.currentDiskSize_ > threshold;
+      }      
+    }
+
+
+    virtual bool IsExistingResource(int64_t internalId) ORTHANC_OVERRIDE
+    {
+      int32_t existing;
+      CheckSuccess(that_.backend_.isExistingResource(&existing, that_.payload_, internalId));
+      return (existing != 0);
+    }
+
+
+    virtual bool IsProtectedPatient(int64_t internalId) ORTHANC_OVERRIDE
+    {
+      int32_t isProtected;
+      CheckSuccess(that_.backend_.isProtectedPatient(&isProtected, that_.payload_, internalId));
+      return (isProtected != 0);
+    }
+
+
+    virtual void ListAvailableAttachments(std::set<FileContentType>& target,
+                                          int64_t id) ORTHANC_OVERRIDE
+    {
+      ResetAnswers();
+
+      CheckSuccess(that_.backend_.listAvailableAttachments(that_.GetContext(), that_.payload_, id));
+
+      if (type_ != _OrthancPluginDatabaseAnswerType_None &&
+          type_ != _OrthancPluginDatabaseAnswerType_Int32)
+      {
+        throw OrthancException(ErrorCode_DatabasePlugin);
+      }
+
+      target.clear();
+
+      if (type_ == _OrthancPluginDatabaseAnswerType_Int32)
+      {
+        for (std::list<int32_t>::const_iterator 
+               it = answerInt32_.begin(); it != answerInt32_.end(); ++it)
+        {
+          target.insert(static_cast<FileContentType>(*it));
+        }
+      }
+    }
+
+
+    virtual void LogChange(int64_t internalId,
+                           const ServerIndexChange& change) ORTHANC_OVERRIDE
+    {
+      OrthancPluginChange tmp;
+      tmp.seq = change.GetSeq();
+      tmp.changeType = static_cast<int32_t>(change.GetChangeType());
+      tmp.resourceType = Plugins::Convert(change.GetResourceType());
+      tmp.publicId = change.GetPublicId().c_str();
+      tmp.date = change.GetDate().c_str();
+
+      CheckSuccess(that_.backend_.logChange(that_.payload_, &tmp));
+    }
+
+
+    virtual void LogExportedResource(const ExportedResource& resource) ORTHANC_OVERRIDE
+    {
+      OrthancPluginExportedResource tmp;
+      tmp.seq = resource.GetSeq();
+      tmp.resourceType = Plugins::Convert(resource.GetResourceType());
+      tmp.publicId = resource.GetPublicId().c_str();
+      tmp.modality = resource.GetModality().c_str();
+      tmp.date = resource.GetDate().c_str();
+      tmp.patientId = resource.GetPatientId().c_str();
+      tmp.studyInstanceUid = resource.GetStudyInstanceUid().c_str();
+      tmp.seriesInstanceUid = resource.GetSeriesInstanceUid().c_str();
+      tmp.sopInstanceUid = resource.GetSopInstanceUid().c_str();
+
+      CheckSuccess(that_.backend_.logExportedResource(that_.payload_, &tmp));
+    }
+
+    
+    virtual bool LookupAttachment(FileInfo& attachment,
+                                  int64_t& revision,
+                                  int64_t id,
+                                  FileContentType contentType) ORTHANC_OVERRIDE
+    {
+      ResetAnswers();
+
+      CheckSuccess(that_.backend_.lookupAttachment
+                   (that_.GetContext(), that_.payload_, id, static_cast<int32_t>(contentType)));
+      
+      revision = 0;  // Dummy value, as revisions were added in Orthanc 1.9.2
+
+      if (type_ == _OrthancPluginDatabaseAnswerType_None)
+      {
+        return false;
+      }
+      else if (type_ == _OrthancPluginDatabaseAnswerType_Attachment &&
+               answerAttachments_.size() == 1)
+      {
+        attachment = answerAttachments_.front();
+        return true; 
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_DatabasePlugin);
+      }
+    }
+
+
+    virtual bool LookupGlobalProperty(std::string& target,
+                                      GlobalProperty property,
+                                      bool shared) ORTHANC_OVERRIDE
+    {
+      // "shared" is unused, as database plugins using Orthanc SDK <=
+      // 1.9.1 are not compatible with multiple readers/writers
+      
+      ResetAnswers();
+
+      CheckSuccess(that_.backend_.lookupGlobalProperty
+                   (that_.GetContext(), that_.payload_, static_cast<int32_t>(property)));
+
+      return ForwardSingleAnswer(target);
+    }
+
+
+    // From the "ILookupResources" interface
+    virtual void LookupIdentifierRange(std::list<int64_t>& result,
+                                       ResourceType level,
+                                       const DicomTag& tag,
+                                       const std::string& start,
+                                       const std::string& end) ORTHANC_OVERRIDE
+    {
+      if (that_.extensions_.lookupIdentifierRange == NULL)
+      {
+        // Default implementation, for plugins using Orthanc SDK <= 1.3.2
+
+        LookupIdentifier(result, level, tag, Compatibility::IdentifierConstraintType_GreaterOrEqual, start);
+
+        std::list<int64_t> b;
+        LookupIdentifier(result, level, tag, Compatibility::IdentifierConstraintType_SmallerOrEqual, end);
+
+        result.splice(result.end(), b);
+      }
+      else
+      {
+        ResetAnswers();
+        CheckSuccess(that_.extensions_.lookupIdentifierRange(that_.GetContext(), that_.payload_, Plugins::Convert(level),
+                                                             tag.GetGroup(), tag.GetElement(),
+                                                             start.c_str(), end.c_str()));
+        ForwardAnswers(result);
+      }
+    }
+
+
+    virtual bool LookupMetadata(std::string& target,
+                                int64_t& revision,
+                                int64_t id,
+                                MetadataType type) ORTHANC_OVERRIDE
+    {
+      ResetAnswers();
+      CheckSuccess(that_.backend_.lookupMetadata(that_.GetContext(), that_.payload_, id, static_cast<int32_t>(type)));
+      revision = 0;  // Dummy value, as revisions were added in Orthanc 1.9.2
+      return ForwardSingleAnswer(target);
+    }
+
+
+    virtual bool LookupParent(int64_t& parentId,
+                              int64_t resourceId) ORTHANC_OVERRIDE
+    {
+      ResetAnswers();
+      CheckSuccess(that_.backend_.lookupParent(that_.GetContext(), that_.payload_, resourceId));
+      return ForwardSingleAnswer(parentId);
+    }
+
+
+    virtual bool LookupResource(int64_t& id,
+                                ResourceType& type,
+                                const std::string& publicId) ORTHANC_OVERRIDE
+    {
+      ResetAnswers();
+
+      CheckSuccess(that_.backend_.lookupResource(that_.GetContext(), that_.payload_, publicId.c_str()));
+
+      if (type_ == _OrthancPluginDatabaseAnswerType_None)
+      {
+        return false;
+      }
+      else if (type_ == _OrthancPluginDatabaseAnswerType_Resource &&
+               answerResources_.size() == 1)
+      {
+        id = answerResources_.front().first;
+        type = answerResources_.front().second;
+        return true; 
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_DatabasePlugin);
+      }
+    }
+
+
+    virtual bool LookupResourceAndParent(int64_t& id,
+                                         ResourceType& type,
+                                         std::string& parentPublicId,
+                                         const std::string& publicId) ORTHANC_OVERRIDE
+    {
+      if (that_.extensions_.lookupResourceAndParent == NULL)
+      {
+        return ILookupResourceAndParent::Apply(*this, id, type, parentPublicId, publicId);
+      }
+      else
+      {
+        std::list<std::string> parent;
+
+        uint8_t isExisting;
+        OrthancPluginResourceType pluginType = OrthancPluginResourceType_Patient;
+      
+        ResetAnswers();
+        CheckSuccess(that_.extensions_.lookupResourceAndParent
+                     (that_.GetContext(), &isExisting, &id, &pluginType, that_.payload_, publicId.c_str()));
+        ForwardAnswers(parent);
+
+        if (isExisting)
+        {
+          type = Plugins::Convert(pluginType);
+
+          if (parent.empty())
+          {
+            if (type != ResourceType_Patient)
+            {
+              throw OrthancException(ErrorCode_DatabasePlugin);
+            }
+          }
+          else if (parent.size() == 1)
+          {
+            if ((type != ResourceType_Study &&
+                 type != ResourceType_Series &&
+                 type != ResourceType_Instance) ||
+                parent.front().empty())
+            {
+              throw OrthancException(ErrorCode_DatabasePlugin);
+            }
+
+            parentPublicId = parent.front();
+          }
+          else
+          {
+            throw OrthancException(ErrorCode_DatabasePlugin);
+          }
+
+          return true;
+        }
+        else
+        {
+          return false;
+        }
+      }
+    }
+
+
+    virtual bool SelectPatientToRecycle(int64_t& internalId) ORTHANC_OVERRIDE
+    {
+      ResetAnswers();
+      CheckSuccess(that_.backend_.selectPatientToRecycle(that_.GetContext(), that_.payload_));
+      return ForwardSingleAnswer(internalId);
+    }
+
+
+    virtual bool SelectPatientToRecycle(int64_t& internalId,
+                                        int64_t patientIdToAvoid) ORTHANC_OVERRIDE
+    {
+      ResetAnswers();
+      CheckSuccess(that_.backend_.selectPatientToRecycle2(that_.GetContext(), that_.payload_, patientIdToAvoid));
+      return ForwardSingleAnswer(internalId);
+    }
+
+
+    virtual void SetGlobalProperty(GlobalProperty property,
+                                   bool shared,
+                                   const std::string& value) ORTHANC_OVERRIDE
+    {
+      // "shared" is unused, as database plugins using Orthanc SDK <=
+      // 1.9.1 are not compatible with multiple readers/writers
+      
+      CheckSuccess(that_.backend_.setGlobalProperty
+                   (that_.payload_, static_cast<int32_t>(property), value.c_str()));
+    }
+
+
+    // From the "ISetResourcesContent" interface
+    virtual void SetIdentifierTag(int64_t id,
+                                  const DicomTag& tag,
+                                  const std::string& value) ORTHANC_OVERRIDE
+    {
+      OrthancPluginDicomTag tmp;
+      tmp.group = tag.GetGroup();
+      tmp.element = tag.GetElement();
+      tmp.value = value.c_str();
+
+      CheckSuccess(that_.backend_.setIdentifierTag(that_.payload_, id, &tmp));
+    }
+
+
+    // From the "ISetResourcesContent" interface
+    virtual void SetMainDicomTag(int64_t id,
+                                 const DicomTag& tag,
+                                 const std::string& value) ORTHANC_OVERRIDE
+    {
+      OrthancPluginDicomTag tmp;
+      tmp.group = tag.GetGroup();
+      tmp.element = tag.GetElement();
+      tmp.value = value.c_str();
+
+      CheckSuccess(that_.backend_.setMainDicomTag(that_.payload_, id, &tmp));
+    }
+
+
+    virtual void SetMetadata(int64_t id,
+                             MetadataType type,
+                             const std::string& value,
+                             int64_t revision) ORTHANC_OVERRIDE
+    {
+      // "revision" is not used, as it was added in Orthanc 1.9.2
+      CheckSuccess(that_.backend_.setMetadata
+                   (that_.payload_, id, static_cast<int32_t>(type), value.c_str()));
+    }
+
+
+    virtual void SetProtectedPatient(int64_t internalId, 
+                                     bool isProtected) ORTHANC_OVERRIDE
+    {
+      CheckSuccess(that_.backend_.setProtectedPatient(that_.payload_, internalId, isProtected));
+    }
+
+
+    // From the "ISetResourcesContent" interface
+    virtual void SetResourcesContent(const Orthanc::ResourcesContent& content) ORTHANC_OVERRIDE
+    {
+      if (that_.extensions_.setResourcesContent == NULL)
+      {
+        ISetResourcesContent::Apply(*this, content);
+      }
+      else
+      {
+        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_.extensions_.setResourcesContent(
+                       that_.payload_,
+                       identifierTags.size(),
+                       (identifierTags.empty() ? NULL : &identifierTags[0]),
+                       mainDicomTags.size(),
+                       (mainDicomTags.empty() ? NULL : &mainDicomTags[0]),
+                       metadata.size(),
+                       (metadata.empty() ? NULL : &metadata[0])));
+      }
+    }
+
+
+    // From the "ICreateInstance" interface
+    virtual void TagMostRecentPatient(int64_t patient) ORTHANC_OVERRIDE
+    {
+      if (that_.extensions_.tagMostRecentPatient != NULL)
+      {
+        CheckSuccess(that_.extensions_.tagMostRecentPatient(that_.payload_, patient));
+      }
+    }
+  };
 
 
   void OrthancPluginDatabase::CheckSuccess(OrthancPluginErrorCode code)
@@ -125,100 +1442,6 @@
   }
 
 
-  void OrthancPluginDatabase::ResetAnswers()
-  {
-    type_ = _OrthancPluginDatabaseAnswerType_None;
-
-    answerDicomMap_ = NULL;
-    answerChanges_ = NULL;
-    answerExportedResources_ = NULL;
-    answerDone_ = NULL;
-    answerMatchingResources_ = NULL;
-    answerMatchingInstances_ = NULL;
-    answerMetadata_ = NULL;
-  }
-
-
-  void OrthancPluginDatabase::ForwardAnswers(std::list<int64_t>& target)
-  {
-    if (type_ != _OrthancPluginDatabaseAnswerType_None &&
-        type_ != _OrthancPluginDatabaseAnswerType_Int64)
-    {
-      throw OrthancException(ErrorCode_DatabasePlugin);
-    }
-
-    target.clear();
-
-    if (type_ == _OrthancPluginDatabaseAnswerType_Int64)
-    {
-      for (std::list<int64_t>::const_iterator 
-             it = answerInt64_.begin(); it != answerInt64_.end(); ++it)
-      {
-        target.push_back(*it);
-      }
-    }
-  }
-
-
-  void OrthancPluginDatabase::ForwardAnswers(std::list<std::string>& target)
-  {
-    if (type_ != _OrthancPluginDatabaseAnswerType_None &&
-        type_ != _OrthancPluginDatabaseAnswerType_String)
-    {
-      throw OrthancException(ErrorCode_DatabasePlugin);
-    }
-
-    target.clear();
-
-    if (type_ == _OrthancPluginDatabaseAnswerType_String)
-    {
-      for (std::list<std::string>::const_iterator 
-             it = answerStrings_.begin(); it != answerStrings_.end(); ++it)
-      {
-        target.push_back(*it);
-      }
-    }
-  }
-
-
-  bool OrthancPluginDatabase::ForwardSingleAnswer(std::string& target)
-  {
-    if (type_ == _OrthancPluginDatabaseAnswerType_None)
-    {
-      return false;
-    }
-    else if (type_ == _OrthancPluginDatabaseAnswerType_String &&
-             answerStrings_.size() == 1)
-    {
-      target = answerStrings_.front();
-      return true; 
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_DatabasePlugin);
-    }
-  }
-
-
-  bool OrthancPluginDatabase::ForwardSingleAnswer(int64_t& target)
-  {
-    if (type_ == _OrthancPluginDatabaseAnswerType_None)
-    {
-      return false;
-    }
-    else if (type_ == _OrthancPluginDatabaseAnswerType_Int64 &&
-             answerInt64_.size() == 1)
-    {
-      target = answerInt64_.front();
-      return true; 
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_DatabasePlugin);
-    }
-  }
-
-
   OrthancPluginDatabase::OrthancPluginDatabase(SharedLibrary& library,
                                                PluginsErrorDictionary&  errorDictionary,
                                                const OrthancPluginDatabaseBackend& backend,
@@ -229,15 +1452,12 @@
     errorDictionary_(errorDictionary),
     backend_(backend),
     payload_(payload),
-    listener_(NULL),
+    activeTransaction_(NULL),
     fastGetTotalSize_(false),
-    currentDiskSize_(0),
-    answerDoneIgnored_(false)
+    currentDiskSize_(0)
   {
     static const char* const MISSING = "  Missing extension in database index plugin: ";
     
-    ResetAnswers();
-
     memset(&extensions_, 0, sizeof(extensions_));
 
     size_t size = sizeof(extensions_);
@@ -312,15 +1532,20 @@
 
   void OrthancPluginDatabase::Open()
   {
-    CheckSuccess(backend_.open(payload_));
+    {
+      boost::recursive_mutex::scoped_lock lock(mutex_);
+      CheckSuccess(backend_.open(payload_));
+    }
 
+    VoidDatabaseListener listener;
+    
     {
-      Transaction transaction(*this);
+      Transaction transaction(*this, listener);
       transaction.Begin();
 
       std::string tmp;
       fastGetTotalSize_ =
-        (LookupGlobalProperty(tmp, GlobalProperty_GetTotalSizeIsFast) &&
+        (transaction.LookupGlobalProperty(tmp, GlobalProperty_GetTotalSizeIsFast, true /* unused in old databases */) &&
          tmp == "1");
       
       if (fastGetTotalSize_)
@@ -331,7 +1556,7 @@
       {
         // This is the case of database plugins using Orthanc SDK <= 1.5.2
         LOG(WARNING) << "Your database index plugin is not compatible with multiple Orthanc writers";
-        currentDiskSize_ = GetTotalCompressedSize();
+        currentDiskSize_ = transaction.GetTotalCompressedSize();
       }
 
       transaction.Commit(0);
@@ -339,593 +1564,21 @@
   }
 
 
-  void OrthancPluginDatabase::AddAttachment(int64_t id,
-                                            const FileInfo& attachment)
-  {
-    OrthancPluginAttachment 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();
-
-    CheckSuccess(backend_.addAttachment(payload_, id, &tmp));
-  }
-
-
-  void OrthancPluginDatabase::AttachChild(int64_t parent,
-                                          int64_t child)
-  {
-    CheckSuccess(backend_.attachChild(payload_, parent, child));
-  }
-
-
-  void OrthancPluginDatabase::ClearChanges()
-  {
-    CheckSuccess(backend_.clearChanges(payload_));
-  }
-
-
-  void OrthancPluginDatabase::ClearExportedResources()
-  {
-    CheckSuccess(backend_.clearExportedResources(payload_));
-  }
-
-
-  int64_t OrthancPluginDatabase::CreateResource(const std::string& publicId,
-                                                ResourceType type)
-  {
-    int64_t id;
-    CheckSuccess(backend_.createResource(&id, payload_, publicId.c_str(), Plugins::Convert(type)));
-    return id;
-  }
-
-
-  void OrthancPluginDatabase::DeleteAttachment(int64_t id,
-                                               FileContentType attachment)
-  {
-    CheckSuccess(backend_.deleteAttachment(payload_, id, static_cast<int32_t>(attachment)));
-  }
-
-
-  void OrthancPluginDatabase::DeleteMetadata(int64_t id,
-                                             MetadataType type)
-  {
-    CheckSuccess(backend_.deleteMetadata(payload_, id, static_cast<int32_t>(type)));
-  }
-
-
-  void OrthancPluginDatabase::DeleteResource(int64_t id)
-  {
-    CheckSuccess(backend_.deleteResource(payload_, id));
-  }
-
-
-  void OrthancPluginDatabase::GetAllMetadata(std::map<MetadataType, std::string>& target,
-                                             int64_t id)
-  {
-    if (extensions_.getAllMetadata == NULL)
-    {
-      // Fallback implementation if extension is missing
-      target.clear();
-
-      ResetAnswers();
-      CheckSuccess(backend_.listAvailableMetadata(GetContext(), payload_, id));
-
-      if (type_ != _OrthancPluginDatabaseAnswerType_None &&
-          type_ != _OrthancPluginDatabaseAnswerType_Int32)
-      {
-        throw OrthancException(ErrorCode_DatabasePlugin);
-      }
-
-      target.clear();
-
-      if (type_ == _OrthancPluginDatabaseAnswerType_Int32)
-      {
-        for (std::list<int32_t>::const_iterator 
-               it = answerInt32_.begin(); it != answerInt32_.end(); ++it)
-        {
-          MetadataType type = static_cast<MetadataType>(*it);
-
-          std::string value;
-          if (LookupMetadata(value, id, type))
-          {
-            target[type] = value;
-          }
-        }
-      }
-    }
-    else
-    {
-      ResetAnswers();
-
-      answerMetadata_ = &target;
-      target.clear();
-      
-      CheckSuccess(extensions_.getAllMetadata(GetContext(), payload_, id));
-
-      if (type_ != _OrthancPluginDatabaseAnswerType_None &&
-          type_ != _OrthancPluginDatabaseAnswerType_Metadata)
-      {
-        throw OrthancException(ErrorCode_DatabasePlugin);
-      }
-    }
-  }
-
-
-  void OrthancPluginDatabase::GetAllInternalIds(std::list<int64_t>& target,
-                                                ResourceType resourceType)
-  {
-    if (extensions_.getAllInternalIds == NULL)
-    {
-      throw OrthancException(ErrorCode_DatabasePlugin,
-                             "The database plugin does not implement the mandatory GetAllInternalIds() extension");
-    }
-
-    ResetAnswers();
-    CheckSuccess(extensions_.getAllInternalIds(GetContext(), payload_, Plugins::Convert(resourceType)));
-    ForwardAnswers(target);
-  }
-
-
-  void OrthancPluginDatabase::GetAllPublicIds(std::list<std::string>& target,
-                                              ResourceType resourceType)
+  void OrthancPluginDatabase::Close()
   {
-    ResetAnswers();
-    CheckSuccess(backend_.getAllPublicIds(GetContext(), payload_, Plugins::Convert(resourceType)));
-    ForwardAnswers(target);
-  }
-
-
-  void OrthancPluginDatabase::GetAllPublicIds(std::list<std::string>& target,
-                                              ResourceType resourceType,
-                                              size_t since,
-                                              size_t limit)
-  {
-    if (extensions_.getAllPublicIdsWithLimit != NULL)
-    {
-      // This extension is available since Orthanc 0.9.4
-      ResetAnswers();
-      CheckSuccess(extensions_.getAllPublicIdsWithLimit
-                   (GetContext(), payload_, Plugins::Convert(resourceType), since, limit));
-      ForwardAnswers(target);
-    }
-    else
-    {
-      // The extension is not available in the database plugin, use a
-      // fallback implementation
-      target.clear();
-
-      if (limit == 0)
-      {
-        return;
-      }
-
-      std::list<std::string> tmp;
-      GetAllPublicIds(tmp, resourceType);
-    
-      if (tmp.size() <= since)
-      {
-        // Not enough results => empty answer
-        return;
-      }
-
-      std::list<std::string>::iterator current = tmp.begin();
-      std::advance(current, since);
-
-      while (limit > 0 && current != tmp.end())
-      {
-        target.push_back(*current);
-        --limit;
-        ++current;
-      }
-    }
-  }
-
-
-
-  void OrthancPluginDatabase::GetChanges(std::list<ServerIndexChange>& target /*out*/,
-                                         bool& done /*out*/,
-                                         int64_t since,
-                                         uint32_t maxResults)
-  {
-    ResetAnswers();
-    answerChanges_ = &target;
-    answerDone_ = &done;
-    done = false;
-
-    CheckSuccess(backend_.getChanges(GetContext(), payload_, since, maxResults));
-  }
-
-
-  void OrthancPluginDatabase::GetChildrenInternalId(std::list<int64_t>& target,
-                                                    int64_t id)
-  {
-    ResetAnswers();
-    CheckSuccess(backend_.getChildrenInternalId(GetContext(), payload_, id));
-    ForwardAnswers(target);
-  }
-
-
-  void OrthancPluginDatabase::GetChildrenPublicId(std::list<std::string>& target,
-                                                  int64_t id)
-  {
-    ResetAnswers();
-    CheckSuccess(backend_.getChildrenPublicId(GetContext(), payload_, id));
-    ForwardAnswers(target);
-  }
-
-
-  void OrthancPluginDatabase::GetExportedResources(std::list<ExportedResource>& target /*out*/,
-                                                   bool& done /*out*/,
-                                                   int64_t since,
-                                                   uint32_t maxResults)
-  {
-    ResetAnswers();
-    answerExportedResources_ = &target;
-    answerDone_ = &done;
-    done = false;
-
-    CheckSuccess(backend_.getExportedResources(GetContext(), payload_, since, maxResults));
-  }
-
-
-  void OrthancPluginDatabase::GetLastChange(std::list<ServerIndexChange>& target /*out*/)
-  {
-    answerDoneIgnored_ = false;
-
-    ResetAnswers();
-    answerChanges_ = &target;
-    answerDone_ = &answerDoneIgnored_;
-
-    CheckSuccess(backend_.getLastChange(GetContext(), payload_));
-  }
-
-
-  void OrthancPluginDatabase::GetLastExportedResource(std::list<ExportedResource>& target /*out*/)
-  {
-    answerDoneIgnored_ = false;
-
-    ResetAnswers();
-    answerExportedResources_ = &target;
-    answerDone_ = &answerDoneIgnored_;
-
-    CheckSuccess(backend_.getLastExportedResource(GetContext(), payload_));
-  }
-
-
-  void OrthancPluginDatabase::GetMainDicomTags(DicomMap& map,
-                                               int64_t id)
-  {
-    ResetAnswers();
-    answerDicomMap_ = &map;
-
-    CheckSuccess(backend_.getMainDicomTags(GetContext(), payload_, id));
-  }
-
-
-  std::string OrthancPluginDatabase::GetPublicId(int64_t resourceId)
-  {
-    ResetAnswers();
-    std::string s;
-
-    CheckSuccess(backend_.getPublicId(GetContext(), payload_, resourceId));
-
-    if (!ForwardSingleAnswer(s))
-    {
-      throw OrthancException(ErrorCode_DatabasePlugin);
-    }
-
-    return s;
-  }
-
-
-  uint64_t OrthancPluginDatabase::GetResourceCount(ResourceType resourceType)
-  {
-    uint64_t count;
-    CheckSuccess(backend_.getResourceCount(&count, payload_, Plugins::Convert(resourceType)));
-    return count;
+    boost::recursive_mutex::scoped_lock lock(mutex_);
+    CheckSuccess(backend_.close(payload_));
   }
 
 
-  ResourceType OrthancPluginDatabase::GetResourceType(int64_t resourceId)
-  {
-    OrthancPluginResourceType type;
-    CheckSuccess(backend_.getResourceType(&type, payload_, resourceId));
-    return Plugins::Convert(type);
-  }
-
-
-  uint64_t OrthancPluginDatabase::GetTotalCompressedSize()
-  {
-    uint64_t size;
-    CheckSuccess(backend_.getTotalCompressedSize(&size, payload_));
-    return size;
-  }
-
-    
-  uint64_t OrthancPluginDatabase::GetTotalUncompressedSize()
-  {
-    uint64_t size;
-    CheckSuccess(backend_.getTotalUncompressedSize(&size, payload_));
-    return size;
-  }
-
-
-  bool OrthancPluginDatabase::IsExistingResource(int64_t internalId)
-  {
-    int32_t existing;
-    CheckSuccess(backend_.isExistingResource(&existing, payload_, internalId));
-    return (existing != 0);
-  }
-
-
-  bool OrthancPluginDatabase::IsProtectedPatient(int64_t internalId)
-  {
-    int32_t isProtected;
-    CheckSuccess(backend_.isProtectedPatient(&isProtected, payload_, internalId));
-    return (isProtected != 0);
-  }
-
-
-  void OrthancPluginDatabase::ListAvailableAttachments(std::set<FileContentType>& target,
-                                                       int64_t id)
-  {
-    ResetAnswers();
-
-    CheckSuccess(backend_.listAvailableAttachments(GetContext(), payload_, id));
-
-    if (type_ != _OrthancPluginDatabaseAnswerType_None &&
-        type_ != _OrthancPluginDatabaseAnswerType_Int32)
-    {
-      throw OrthancException(ErrorCode_DatabasePlugin);
-    }
-
-    target.clear();
-
-    if (type_ == _OrthancPluginDatabaseAnswerType_Int32)
-    {
-      for (std::list<int32_t>::const_iterator 
-             it = answerInt32_.begin(); it != answerInt32_.end(); ++it)
-      {
-        target.insert(static_cast<FileContentType>(*it));
-      }
-    }
-  }
-
-
-  void OrthancPluginDatabase::LogChange(int64_t internalId,
-                                        const ServerIndexChange& change)
-  {
-    OrthancPluginChange tmp;
-    tmp.seq = change.GetSeq();
-    tmp.changeType = static_cast<int32_t>(change.GetChangeType());
-    tmp.resourceType = Plugins::Convert(change.GetResourceType());
-    tmp.publicId = change.GetPublicId().c_str();
-    tmp.date = change.GetDate().c_str();
-
-    CheckSuccess(backend_.logChange(payload_, &tmp));
-  }
-
-
-  void OrthancPluginDatabase::LogExportedResource(const ExportedResource& resource)
-  {
-    OrthancPluginExportedResource tmp;
-    tmp.seq = resource.GetSeq();
-    tmp.resourceType = Plugins::Convert(resource.GetResourceType());
-    tmp.publicId = resource.GetPublicId().c_str();
-    tmp.modality = resource.GetModality().c_str();
-    tmp.date = resource.GetDate().c_str();
-    tmp.patientId = resource.GetPatientId().c_str();
-    tmp.studyInstanceUid = resource.GetStudyInstanceUid().c_str();
-    tmp.seriesInstanceUid = resource.GetSeriesInstanceUid().c_str();
-    tmp.sopInstanceUid = resource.GetSopInstanceUid().c_str();
-
-    CheckSuccess(backend_.logExportedResource(payload_, &tmp));
-  }
-
-    
-  bool OrthancPluginDatabase::LookupAttachment(FileInfo& attachment,
-                                               int64_t id,
-                                               FileContentType contentType)
-  {
-    ResetAnswers();
-
-    CheckSuccess(backend_.lookupAttachment
-                 (GetContext(), payload_, id, static_cast<int32_t>(contentType)));
-
-    if (type_ == _OrthancPluginDatabaseAnswerType_None)
-    {
-      return false;
-    }
-    else if (type_ == _OrthancPluginDatabaseAnswerType_Attachment &&
-             answerAttachments_.size() == 1)
-    {
-      attachment = answerAttachments_.front();
-      return true; 
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_DatabasePlugin);
-    }
-  }
-
-
-  bool OrthancPluginDatabase::LookupGlobalProperty(std::string& target,
-                                                   GlobalProperty property)
-  {
-    ResetAnswers();
-
-    CheckSuccess(backend_.lookupGlobalProperty
-                 (GetContext(), payload_, static_cast<int32_t>(property)));
-
-    return ForwardSingleAnswer(target);
-  }
-
-
-  bool OrthancPluginDatabase::LookupMetadata(std::string& target,
-                                             int64_t id,
-                                             MetadataType type)
-  {
-    ResetAnswers();
-    CheckSuccess(backend_.lookupMetadata(GetContext(), payload_, id, static_cast<int32_t>(type)));
-    return ForwardSingleAnswer(target);
-  }
-
-
-  bool OrthancPluginDatabase::LookupParent(int64_t& parentId,
-                                           int64_t resourceId)
+  IDatabaseWrapper::ITransaction* OrthancPluginDatabase::StartTransaction(TransactionType type,
+                                                                          IDatabaseListener& listener)
   {
-    ResetAnswers();
-    CheckSuccess(backend_.lookupParent(GetContext(), payload_, resourceId));
-    return ForwardSingleAnswer(parentId);
-  }
-
-
-  bool OrthancPluginDatabase::LookupResource(int64_t& id,
-                                             ResourceType& type,
-                                             const std::string& publicId)
-  {
-    ResetAnswers();
-
-    CheckSuccess(backend_.lookupResource(GetContext(), payload_, publicId.c_str()));
-
-    if (type_ == _OrthancPluginDatabaseAnswerType_None)
-    {
-      return false;
-    }
-    else if (type_ == _OrthancPluginDatabaseAnswerType_Resource &&
-             answerResources_.size() == 1)
-    {
-      id = answerResources_.front().first;
-      type = answerResources_.front().second;
-      return true; 
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_DatabasePlugin);
-    }
-  }
-
-
-  bool OrthancPluginDatabase::SelectPatientToRecycle(int64_t& internalId)
-  {
-    ResetAnswers();
-    CheckSuccess(backend_.selectPatientToRecycle(GetContext(), payload_));
-    return ForwardSingleAnswer(internalId);
-  }
-
-
-  bool OrthancPluginDatabase::SelectPatientToRecycle(int64_t& internalId,
-                                                     int64_t patientIdToAvoid)
-  {
-    ResetAnswers();
-    CheckSuccess(backend_.selectPatientToRecycle2(GetContext(), payload_, patientIdToAvoid));
-    return ForwardSingleAnswer(internalId);
-  }
-
-
-  void OrthancPluginDatabase::SetGlobalProperty(GlobalProperty property,
-                                                const std::string& value)
-  {
-    CheckSuccess(backend_.setGlobalProperty
-                 (payload_, static_cast<int32_t>(property), value.c_str()));
-  }
-
-
-  void OrthancPluginDatabase::ClearMainDicomTags(int64_t id)
-  {
-    if (extensions_.clearMainDicomTags == NULL)
-    {
-      throw OrthancException(ErrorCode_DatabasePlugin,
-                             "Your custom index plugin does not implement the mandatory ClearMainDicomTags() extension");
-    }
-
-    CheckSuccess(extensions_.clearMainDicomTags(payload_, id));
-  }
-
+    // TODO - Take advantage of "type"
 
-  void OrthancPluginDatabase::SetMainDicomTag(int64_t id,
-                                              const DicomTag& tag,
-                                              const std::string& value)
-  {
-    OrthancPluginDicomTag tmp;
-    tmp.group = tag.GetGroup();
-    tmp.element = tag.GetElement();
-    tmp.value = value.c_str();
-
-    CheckSuccess(backend_.setMainDicomTag(payload_, id, &tmp));
-  }
-
-
-  void OrthancPluginDatabase::SetIdentifierTag(int64_t id,
-                                               const DicomTag& tag,
-                                               const std::string& value)
-  {
-    OrthancPluginDicomTag tmp;
-    tmp.group = tag.GetGroup();
-    tmp.element = tag.GetElement();
-    tmp.value = value.c_str();
-
-    CheckSuccess(backend_.setIdentifierTag(payload_, id, &tmp));
-  }
-
-
-  void OrthancPluginDatabase::SetMetadata(int64_t id,
-                                          MetadataType type,
-                                          const std::string& value)
-  {
-    CheckSuccess(backend_.setMetadata
-                 (payload_, id, static_cast<int32_t>(type), value.c_str()));
-  }
-
-
-  void OrthancPluginDatabase::SetProtectedPatient(int64_t internalId, 
-                                                  bool isProtected)
-  {
-    CheckSuccess(backend_.setProtectedPatient(payload_, internalId, isProtected));
-  }
-
-
-  IDatabaseWrapper::ITransaction* OrthancPluginDatabase::StartTransaction()
-  {
-    return new Transaction(*this);
-  }
-
-
-  static void ProcessEvent(IDatabaseListener& listener,
-                           const _OrthancPluginDatabaseAnswer& answer)
-  {
-    switch (answer.type)
-    {
-      case _OrthancPluginDatabaseAnswerType_DeletedAttachment:
-      {
-        const OrthancPluginAttachment& attachment = 
-          *reinterpret_cast<const OrthancPluginAttachment*>(answer.valueGeneric);
-        listener.SignalFileDeleted(Convert(attachment));
-        break;
-      }
-        
-      case _OrthancPluginDatabaseAnswerType_RemainingAncestor:
-      {
-        ResourceType type = Plugins::Convert(static_cast<OrthancPluginResourceType>(answer.valueInt32));
-        listener.SignalRemainingAncestor(type, answer.valueString);
-        break;
-      }
-      
-      case _OrthancPluginDatabaseAnswerType_DeletedResource:
-      {
-        ResourceType type = Plugins::Convert(static_cast<OrthancPluginResourceType>(answer.valueInt32));
-        ServerIndexChange change(ChangeType_Deleted, type, answer.valueString);
-        listener.SignalChange(change);
-        break;
-      }
-
-      default:
-        throw OrthancException(ErrorCode_DatabasePlugin);
-    }
+    std::unique_ptr<Transaction> transaction(new Transaction(*this, listener));
+    transaction->Begin();
+    return transaction.release();
   }
 
 
@@ -950,9 +1603,11 @@
   void OrthancPluginDatabase::Upgrade(unsigned int targetVersion,
                                       IStorageArea& storageArea)
   {
+    VoidDatabaseListener listener;
+    
     if (extensions_.upgradeDatabase != NULL)
     {
-      Transaction transaction(*this);
+      Transaction transaction(*this, listener);
       transaction.Begin();
 
       OrthancPluginErrorCode code = extensions_.upgradeDatabase(
@@ -975,563 +1630,15 @@
 
   void OrthancPluginDatabase::AnswerReceived(const _OrthancPluginDatabaseAnswer& answer)
   {
-    if (answer.type == _OrthancPluginDatabaseAnswerType_None)
-    {
-      throw OrthancException(ErrorCode_DatabasePlugin);
-    }
-
-    if (answer.type == _OrthancPluginDatabaseAnswerType_DeletedAttachment ||
-        answer.type == _OrthancPluginDatabaseAnswerType_DeletedResource ||
-        answer.type == _OrthancPluginDatabaseAnswerType_RemainingAncestor)
-    {
-      assert(listener_ != NULL);
-      ProcessEvent(*listener_, answer);
-      return;
-    }
-
-    if (type_ == _OrthancPluginDatabaseAnswerType_None)
-    {
-      type_ = answer.type;
-
-      switch (type_)
-      {
-        case _OrthancPluginDatabaseAnswerType_Int32:
-          answerInt32_.clear();
-          break;
-
-        case _OrthancPluginDatabaseAnswerType_Int64:
-          answerInt64_.clear();
-          break;
-
-        case _OrthancPluginDatabaseAnswerType_Resource:
-          answerResources_.clear();
-          break;
-
-        case _OrthancPluginDatabaseAnswerType_Attachment:
-          answerAttachments_.clear();
-          break;
-
-        case _OrthancPluginDatabaseAnswerType_String:
-          answerStrings_.clear();
-          break;
-
-        case _OrthancPluginDatabaseAnswerType_DicomTag:
-          assert(answerDicomMap_ != NULL);
-          answerDicomMap_->Clear();
-          break;
-
-        case _OrthancPluginDatabaseAnswerType_Change:
-          assert(answerChanges_ != NULL);
-          answerChanges_->clear();
-          break;
-
-        case _OrthancPluginDatabaseAnswerType_ExportedResource:
-          assert(answerExportedResources_ != NULL);
-          answerExportedResources_->clear();
-          break;
-
-        case _OrthancPluginDatabaseAnswerType_MatchingResource:
-          assert(answerMatchingResources_ != NULL);
-          answerMatchingResources_->clear();
-
-          if (answerMatchingInstances_ != NULL)
-          {
-            answerMatchingInstances_->clear();
-          }
-          
-          break;
-
-        case _OrthancPluginDatabaseAnswerType_Metadata:
-          assert(answerMetadata_ != NULL);
-          answerMetadata_->clear();
-          break;
-
-        default:
-          throw OrthancException(ErrorCode_DatabasePlugin,
-                                 "Unhandled type of answer for custom index plugin: " +
-                                 boost::lexical_cast<std::string>(answer.type));
-      }
-    }
-    else if (type_ != answer.type)
-    {
-      throw OrthancException(ErrorCode_DatabasePlugin,
-                             "Error in the plugin protocol: Cannot change the answer type");
-    }
-
-    switch (answer.type)
-    {
-      case _OrthancPluginDatabaseAnswerType_Int32:
-      {
-        answerInt32_.push_back(answer.valueInt32);
-        break;
-      }
-
-      case _OrthancPluginDatabaseAnswerType_Int64:
-      {
-        answerInt64_.push_back(answer.valueInt64);
-        break;
-      }
-
-      case _OrthancPluginDatabaseAnswerType_Resource:
-      {
-        OrthancPluginResourceType type = static_cast<OrthancPluginResourceType>(answer.valueInt32);
-        answerResources_.push_back(std::make_pair(answer.valueInt64, Plugins::Convert(type)));
-        break;
-      }
-
-      case _OrthancPluginDatabaseAnswerType_Attachment:
-      {
-        const OrthancPluginAttachment& attachment = 
-          *reinterpret_cast<const OrthancPluginAttachment*>(answer.valueGeneric);
-
-        answerAttachments_.push_back(Convert(attachment));
-        break;
-      }
-
-      case _OrthancPluginDatabaseAnswerType_DicomTag:
-      {
-        const OrthancPluginDicomTag& tag = *reinterpret_cast<const OrthancPluginDicomTag*>(answer.valueGeneric);
-        assert(answerDicomMap_ != NULL);
-        answerDicomMap_->SetValue(tag.group, tag.element, std::string(tag.value), false);
-        break;
-      }
-
-      case _OrthancPluginDatabaseAnswerType_String:
-      {
-        if (answer.valueString == NULL)
-        {
-          throw OrthancException(ErrorCode_DatabasePlugin);
-        }
-
-        if (type_ == _OrthancPluginDatabaseAnswerType_None)
-        {
-          type_ = _OrthancPluginDatabaseAnswerType_String;
-          answerStrings_.clear();
-        }
-        else if (type_ != _OrthancPluginDatabaseAnswerType_String)
-        {
-          throw OrthancException(ErrorCode_DatabasePlugin);
-        }
+    boost::recursive_mutex::scoped_lock lock(mutex_);
 
-        answerStrings_.push_back(std::string(answer.valueString));
-        break;
-      }
-
-      case _OrthancPluginDatabaseAnswerType_Change:
-      {
-        assert(answerDone_ != NULL);
-        if (answer.valueUint32 == 1)
-        {
-          *answerDone_ = true;
-        }
-        else if (*answerDone_)
-        {
-          throw OrthancException(ErrorCode_DatabasePlugin);
-        }
-        else
-        {
-          const OrthancPluginChange& change =
-            *reinterpret_cast<const OrthancPluginChange*>(answer.valueGeneric);
-          assert(answerChanges_ != NULL);
-          answerChanges_->push_back
-            (ServerIndexChange(change.seq,
-                               static_cast<ChangeType>(change.changeType),
-                               Plugins::Convert(change.resourceType),
-                               change.publicId,
-                               change.date));                                   
-        }
-
-        break;
-      }
-
-      case _OrthancPluginDatabaseAnswerType_ExportedResource:
-      {
-        assert(answerDone_ != NULL);
-        if (answer.valueUint32 == 1)
-        {
-          *answerDone_ = true;
-        }
-        else if (*answerDone_)
-        {
-          throw OrthancException(ErrorCode_DatabasePlugin);
-        }
-        else
-        {
-          const OrthancPluginExportedResource& exported = 
-            *reinterpret_cast<const OrthancPluginExportedResource*>(answer.valueGeneric);
-          assert(answerExportedResources_ != NULL);
-          answerExportedResources_->push_back
-            (ExportedResource(exported.seq,
-                              Plugins::Convert(exported.resourceType),
-                              exported.publicId,
-                              exported.modality,
-                              exported.date,
-                              exported.patientId,
-                              exported.studyInstanceUid,
-                              exported.seriesInstanceUid,
-                              exported.sopInstanceUid));
-        }
-
-        break;
-      }
-
-      case _OrthancPluginDatabaseAnswerType_MatchingResource:
-      {
-        const OrthancPluginMatchingResource& match = 
-          *reinterpret_cast<const OrthancPluginMatchingResource*>(answer.valueGeneric);
-
-        if (match.resourceId == NULL)
-        {
-          throw OrthancException(ErrorCode_DatabasePlugin);
-        }
-
-        assert(answerMatchingResources_ != NULL);
-        answerMatchingResources_->push_back(match.resourceId);
-
-        if (answerMatchingInstances_ != NULL)
-        {
-          if (match.someInstanceId == NULL)
-          {
-            throw OrthancException(ErrorCode_DatabasePlugin);
-          }
-
-          answerMatchingInstances_->push_back(match.someInstanceId);
-        }
- 
-        break;
-      }
-
-      case _OrthancPluginDatabaseAnswerType_Metadata:
-      {
-        const OrthancPluginResourcesContentMetadata& metadata =
-          *reinterpret_cast<const OrthancPluginResourcesContentMetadata*>(answer.valueGeneric);
-
-        MetadataType type = static_cast<MetadataType>(metadata.metadata);
-
-        if (metadata.value == NULL)
-        {
-          throw OrthancException(ErrorCode_DatabasePlugin);
-        }
-
-        assert(answerMetadata_ != NULL &&
-               answerMetadata_->find(type) == answerMetadata_->end());
-        (*answerMetadata_) [type] = metadata.value;
-        break;
-      }
-
-      default:
-        throw OrthancException(ErrorCode_DatabasePlugin,
-                               "Unhandled type of answer for custom index plugin: " +
-                               boost::lexical_cast<std::string>(answer.type));
-    }
-  }
-
-    
-  bool OrthancPluginDatabase::IsDiskSizeAbove(uint64_t threshold)
-  {
-    if (fastGetTotalSize_)
+    if (activeTransaction_ != NULL)
     {
-      return GetTotalCompressedSize() > threshold;
-    }
-    else
-    {
-      assert(GetTotalCompressedSize() == currentDiskSize_);
-      return currentDiskSize_ > threshold;
-    }      
-  }
-
-
-  void OrthancPluginDatabase::ApplyLookupResources(std::list<std::string>& resourcesId,
-                                                   std::list<std::string>* instancesId,
-                                                   const std::vector<DatabaseConstraint>& lookup,
-                                                   ResourceType queryLevel,
-                                                   size_t limit)
-  {
-    if (extensions_.lookupResources == NULL)
-    {
-      // Fallback to compatibility mode
-      ILookupResources::Apply
-        (*this, *this, resourcesId, instancesId, lookup, queryLevel, limit);
+      activeTransaction_->AnswerReceived(answer);
     }
     else
     {
-      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]);
-      }
-
-      ResetAnswers();
-      answerMatchingResources_ = &resourcesId;
-      answerMatchingInstances_ = instancesId;
-      
-      CheckSuccess(extensions_.lookupResources(GetContext(), payload_, lookup.size(),
-                                               (lookup.empty() ? NULL : &constraints[0]),
-                                               Plugins::Convert(queryLevel),
-                                               limit, (instancesId == NULL ? 0 : 1)));
-    }
-  }
-
-
-  bool OrthancPluginDatabase::CreateInstance(
-    IDatabaseWrapper::CreateInstanceResult& result,
-    int64_t& instanceId,
-    const std::string& patient,
-    const std::string& study,
-    const std::string& series,
-    const std::string& instance)
-  {
-    if (extensions_.createInstance == NULL)
-    {
-      // Fallback to compatibility mode
-      return ICreateInstance::Apply
-        (*this, result, instanceId, patient, study, series, instance);
-    }
-    else
-    {
-      OrthancPluginCreateInstanceResult output;
-      memset(&output, 0, sizeof(output));
-
-      CheckSuccess(extensions_.createInstance(&output, payload_, patient.c_str(),
-                                              study.c_str(), series.c_str(), instance.c_str()));
-
-      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;
-      }
-    }
-  }
-
-
-  void OrthancPluginDatabase::LookupIdentifier(std::list<int64_t>& result,
-                                               ResourceType level,
-                                               const DicomTag& tag,
-                                               Compatibility::IdentifierConstraintType type,
-                                               const std::string& value)
-  {
-    if (extensions_.lookupIdentifier3 == NULL)
-    {
-      throw OrthancException(ErrorCode_DatabasePlugin,
-                             "The database plugin does not implement the mandatory LookupIdentifier3() extension");
-    }
-
-    OrthancPluginDicomTag tmp;
-    tmp.group = tag.GetGroup();
-    tmp.element = tag.GetElement();
-    tmp.value = value.c_str();
-
-    ResetAnswers();
-    CheckSuccess(extensions_.lookupIdentifier3(GetContext(), payload_, Plugins::Convert(level),
-                                               &tmp, Compatibility::Convert(type)));
-    ForwardAnswers(result);
-  }
-
-
-  void OrthancPluginDatabase::LookupIdentifierRange(std::list<int64_t>& result,
-                                                    ResourceType level,
-                                                    const DicomTag& tag,
-                                                    const std::string& start,
-                                                    const std::string& end)
-  {
-    if (extensions_.lookupIdentifierRange == NULL)
-    {
-      // Default implementation, for plugins using Orthanc SDK <= 1.3.2
-
-      LookupIdentifier(result, level, tag, Compatibility::IdentifierConstraintType_GreaterOrEqual, start);
-
-      std::list<int64_t> b;
-      LookupIdentifier(result, level, tag, Compatibility::IdentifierConstraintType_SmallerOrEqual, end);
-
-      result.splice(result.end(), b);
-    }
-    else
-    {
-      ResetAnswers();
-      CheckSuccess(extensions_.lookupIdentifierRange(GetContext(), payload_, Plugins::Convert(level),
-                                                     tag.GetGroup(), tag.GetElement(),
-                                                     start.c_str(), end.c_str()));
-      ForwardAnswers(result);
-    }
-  }
-
-
-  void OrthancPluginDatabase::SetResourcesContent(const Orthanc::ResourcesContent& content)
-  {
-    if (extensions_.setResourcesContent == NULL)
-    {
-      ISetResourcesContent::Apply(*this, content);
-    }
-    else
-    {
-      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(extensions_.setResourcesContent(
-                     payload_,
-                     identifierTags.size(),
-                     (identifierTags.empty() ? NULL : &identifierTags[0]),
-                     mainDicomTags.size(),
-                     (mainDicomTags.empty() ? NULL : &mainDicomTags[0]),
-                     metadata.size(),
-                     (metadata.empty() ? NULL : &metadata[0])));
-    }
-  }
-
-
-
-  void OrthancPluginDatabase::GetChildrenMetadata(std::list<std::string>& target,
-                                                  int64_t resourceId,
-                                                  MetadataType metadata)
-  {
-    if (extensions_.getChildrenMetadata == NULL)
-    {
-      IGetChildrenMetadata::Apply(*this, target, resourceId, metadata);
-    }
-    else
-    {
-      ResetAnswers();
-      CheckSuccess(extensions_.getChildrenMetadata
-                   (GetContext(), payload_, resourceId, static_cast<int32_t>(metadata)));
-      ForwardAnswers(target);
-    }
-  }
-
-
-  int64_t OrthancPluginDatabase::GetLastChangeIndex()
-  {
-    if (extensions_.getLastChangeIndex == NULL)
-    {
-      // This was the default behavior in Orthanc <= 1.5.1
-      // https://groups.google.com/d/msg/orthanc-users/QhzB6vxYeZ0/YxabgqpfBAAJ
-      return 0;
-    }
-    else
-    {
-      int64_t result = 0;
-      CheckSuccess(extensions_.getLastChangeIndex(&result, payload_));
-      return result;
-    }
-  }
-
-  
-  void OrthancPluginDatabase::TagMostRecentPatient(int64_t patient)
-  {
-    if (extensions_.tagMostRecentPatient != NULL)
-    {
-      CheckSuccess(extensions_.tagMostRecentPatient(payload_, patient));
-    }
-  }
-
-
-  bool OrthancPluginDatabase::LookupResourceAndParent(int64_t& id,
-                                                      ResourceType& type,
-                                                      std::string& parentPublicId,
-                                                      const std::string& publicId)
-  {
-    if (extensions_.lookupResourceAndParent == NULL)
-    {
-      return ILookupResourceAndParent::Apply(*this, id, type, parentPublicId, publicId);
-    }
-    else
-    {
-      std::list<std::string> parent;
-
-      uint8_t isExisting;
-      OrthancPluginResourceType pluginType = OrthancPluginResourceType_Patient;
-      
-      ResetAnswers();
-      CheckSuccess(extensions_.lookupResourceAndParent
-                   (GetContext(), &isExisting, &id, &pluginType, payload_, publicId.c_str()));
-      ForwardAnswers(parent);
-
-      if (isExisting)
-      {
-        type = Plugins::Convert(pluginType);
-
-        if (parent.empty())
-        {
-          if (type != ResourceType_Patient)
-          {
-            throw OrthancException(ErrorCode_DatabasePlugin);
-          }
-        }
-        else if (parent.size() == 1)
-        {
-          if ((type != ResourceType_Study &&
-               type != ResourceType_Series &&
-               type != ResourceType_Instance) ||
-              parent.front().empty())
-          {
-            throw OrthancException(ErrorCode_DatabasePlugin);
-          }
-
-          parentPublicId = parent.front();
-        }
-        else
-        {
-          throw OrthancException(ErrorCode_DatabasePlugin);
-        }
-
-        return true;
-      }
-      else
-      {
-        return false;
-      }
+      LOG(WARNING) << "Received an answer from the database index plugin, but not transaction is active";
     }
   }
 }
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.h	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.h	Tue Apr 20 18:11:29 2021 +0200
@@ -36,55 +36,46 @@
 #if ORTHANC_ENABLE_PLUGINS == 1
 
 #include "../../../OrthancFramework/Sources/SharedLibrary.h"
-#include "../../Sources/Database/Compatibility/ICreateInstance.h"
-#include "../../Sources/Database/Compatibility/IGetChildrenMetadata.h"
-#include "../../Sources/Database/Compatibility/ILookupResources.h"
-#include "../../Sources/Database/Compatibility/ILookupResourceAndParent.h"
-#include "../../Sources/Database/Compatibility/ISetResourcesContent.h"
+#include "../../Sources/Database/IDatabaseWrapper.h"
 #include "../Include/orthanc/OrthancCDatabasePlugin.h"
 #include "PluginsErrorDictionary.h"
 
+#include <boost/thread/recursive_mutex.hpp>
+
 namespace Orthanc
 {
-  class OrthancPluginDatabase :
-    public IDatabaseWrapper,
-    public Compatibility::ICreateInstance,
-    public Compatibility::IGetChildrenMetadata,
-    public Compatibility::ILookupResources,
-    public Compatibility::ILookupResourceAndParent,
-    public Compatibility::ISetResourcesContent
+  /**
+   * This class is for backward compatibility with database plugins
+   * that don't use the primitives introduced in Orthanc 1.9.2 to deal
+   * with concurrent read-only transactions.
+   *
+   * In Orthanc <= 1.9.1, Orthanc assumed that at most 1 single thread
+   * was accessing the database plugin at anytime, in order to match
+   * the SQLite model. Read-write accesses assumed the plugin to run
+   * the SQL statement "START TRANSACTION SERIALIZABLE" so as to be
+   * able to rollback the modifications. Read-only accesses didn't
+   * start a transaction, as they were protected by the global mutex.
+   **/
+  class OrthancPluginDatabase : public IDatabaseWrapper
   {
   private:
     class Transaction;
 
-    typedef std::pair<int64_t, ResourceType>     AnswerResource;
-    typedef std::map<MetadataType, std::string>  AnswerMetadata;
-
-    SharedLibrary&  library_;
-    PluginsErrorDictionary&  errorDictionary_;
-    _OrthancPluginDatabaseAnswerType type_;
-    OrthancPluginDatabaseBackend backend_;
+    /**
+     * We need a "recursive_mutex" because of "AnswerReceived()" that
+     * is called by the "answer" primitives of the database SDK once a
+     * transaction is running.
+     **/
+    boost::recursive_mutex          mutex_;
+    
+    SharedLibrary&                  library_;
+    PluginsErrorDictionary&         errorDictionary_;
+    OrthancPluginDatabaseBackend    backend_;
     OrthancPluginDatabaseExtensions extensions_;
-    void* payload_;
-    IDatabaseListener* listener_;
-
-    bool      fastGetTotalSize_;
-    uint64_t  currentDiskSize_;
-
-    std::list<std::string>         answerStrings_;
-    std::list<int32_t>             answerInt32_;
-    std::list<int64_t>             answerInt64_;
-    std::list<AnswerResource>      answerResources_;
-    std::list<FileInfo>            answerAttachments_;
-
-    DicomMap*                      answerDicomMap_;
-    std::list<ServerIndexChange>*  answerChanges_;
-    std::list<ExportedResource>*   answerExportedResources_;
-    bool*                          answerDone_;
-    bool                           answerDoneIgnored_;
-    std::list<std::string>*        answerMatchingResources_;
-    std::list<std::string>*        answerMatchingInstances_;
-    AnswerMetadata*                answerMetadata_;
+    void*                           payload_;
+    Transaction*                    activeTransaction_;
+    bool                            fastGetTotalSize_;
+    uint64_t                        currentDiskSize_;
 
     OrthancPluginDatabaseContext* GetContext()
     {
@@ -93,16 +84,6 @@
 
     void CheckSuccess(OrthancPluginErrorCode code);
 
-    void ResetAnswers();
-
-    void ForwardAnswers(std::list<int64_t>& target);
-
-    void ForwardAnswers(std::list<std::string>& target);
-
-    bool ForwardSingleAnswer(std::string& target);
-
-    bool ForwardSingleAnswer(int64_t& target);
-
   public:
     OrthancPluginDatabase(SharedLibrary& library,
                           PluginsErrorDictionary&  errorDictionary,
@@ -111,266 +92,39 @@
                           size_t extensionsSize,
                           void *payload);
 
-    virtual void Open() 
-      ORTHANC_OVERRIDE;
+    virtual void Open() ORTHANC_OVERRIDE;
 
-    virtual void Close() 
-      ORTHANC_OVERRIDE
-    {
-      CheckSuccess(backend_.close(payload_));
-    }
+    virtual void Close() ORTHANC_OVERRIDE;
 
     const SharedLibrary& GetSharedLibrary() const
     {
       return library_;
     }
 
-    virtual void AddAttachment(int64_t id,
-                               const FileInfo& attachment) 
-      ORTHANC_OVERRIDE;
-
-    virtual void AttachChild(int64_t parent,
-                             int64_t child) 
-      ORTHANC_OVERRIDE;
-
-    virtual void ClearChanges() 
-      ORTHANC_OVERRIDE;
-
-    virtual void ClearExportedResources() 
-      ORTHANC_OVERRIDE;
-
-    virtual int64_t CreateResource(const std::string& publicId,
-                                   ResourceType type) 
-      ORTHANC_OVERRIDE;
-
-    virtual void DeleteAttachment(int64_t id,
-                                  FileContentType attachment) 
-      ORTHANC_OVERRIDE;
-
-    virtual void DeleteMetadata(int64_t id,
-                                MetadataType type) 
-      ORTHANC_OVERRIDE;
-
-    virtual void DeleteResource(int64_t id) 
-      ORTHANC_OVERRIDE;
-
-    virtual void FlushToDisk() 
-      ORTHANC_OVERRIDE
+    virtual void FlushToDisk() ORTHANC_OVERRIDE
     {
     }
 
-    virtual bool HasFlushToDisk() const 
-      ORTHANC_OVERRIDE
+    virtual bool HasFlushToDisk() const ORTHANC_OVERRIDE
     {
       return false;
     }
 
-    virtual void GetAllMetadata(std::map<MetadataType, std::string>& target,
-                                int64_t id) 
-      ORTHANC_OVERRIDE;
-
-    virtual void GetAllPublicIds(std::list<std::string>& target,
-                                 ResourceType resourceType) 
-      ORTHANC_OVERRIDE;
-
-    virtual void GetAllPublicIds(std::list<std::string>& target,
-                                 ResourceType resourceType,
-                                 size_t since,
-                                 size_t limit) 
-      ORTHANC_OVERRIDE;
-
-    virtual void GetChanges(std::list<ServerIndexChange>& target /*out*/,
-                            bool& done /*out*/,
-                            int64_t since,
-                            uint32_t maxResults) 
-      ORTHANC_OVERRIDE;
-
-    virtual void GetChildrenInternalId(std::list<int64_t>& target,
-                                       int64_t id) 
-      ORTHANC_OVERRIDE;
-
-    virtual void GetChildrenPublicId(std::list<std::string>& target,
-                                     int64_t id) 
-      ORTHANC_OVERRIDE;
-
-    virtual void GetExportedResources(std::list<ExportedResource>& target /*out*/,
-                                      bool& done /*out*/,
-                                      int64_t since,
-                                      uint32_t maxResults) 
-      ORTHANC_OVERRIDE;
-
-    virtual void GetLastChange(std::list<ServerIndexChange>& target /*out*/) 
-      ORTHANC_OVERRIDE;
-
-    virtual void GetLastExportedResource(std::list<ExportedResource>& target /*out*/) 
-      ORTHANC_OVERRIDE;
-
-    virtual void GetMainDicomTags(DicomMap& map,
-                                  int64_t id) 
-      ORTHANC_OVERRIDE;
-
-    virtual std::string GetPublicId(int64_t resourceId) 
-      ORTHANC_OVERRIDE;
-
-    virtual uint64_t GetResourceCount(ResourceType resourceType) 
-      ORTHANC_OVERRIDE;
-
-    virtual ResourceType GetResourceType(int64_t resourceId) 
-      ORTHANC_OVERRIDE;
-
-    virtual uint64_t GetTotalCompressedSize() 
-      ORTHANC_OVERRIDE;
-    
-    virtual uint64_t GetTotalUncompressedSize() 
-      ORTHANC_OVERRIDE;
-
-    virtual bool IsExistingResource(int64_t internalId) 
-      ORTHANC_OVERRIDE;
-
-    virtual bool IsProtectedPatient(int64_t internalId) 
-      ORTHANC_OVERRIDE;
-
-    virtual void ListAvailableAttachments(std::set<FileContentType>& target,
-                                          int64_t id) 
-      ORTHANC_OVERRIDE;
-
-    virtual void LogChange(int64_t internalId,
-                           const ServerIndexChange& change) 
-      ORTHANC_OVERRIDE;
-
-    virtual void LogExportedResource(const ExportedResource& resource) 
-      ORTHANC_OVERRIDE;
-    
-    virtual bool LookupAttachment(FileInfo& attachment,
-                                  int64_t id,
-                                  FileContentType contentType) 
-      ORTHANC_OVERRIDE;
-
-    virtual bool LookupGlobalProperty(std::string& target,
-                                      GlobalProperty property) 
-      ORTHANC_OVERRIDE;
-
-    virtual bool LookupMetadata(std::string& target,
-                                int64_t id,
-                                MetadataType type) 
-      ORTHANC_OVERRIDE;
-
-    virtual bool LookupParent(int64_t& parentId,
-                              int64_t resourceId) 
-      ORTHANC_OVERRIDE;
-
-    virtual bool LookupResource(int64_t& id,
-                                ResourceType& type,
-                                const std::string& publicId) 
-      ORTHANC_OVERRIDE;
-
-    virtual bool SelectPatientToRecycle(int64_t& internalId) 
+    virtual IDatabaseWrapper::ITransaction* StartTransaction(TransactionType type,
+                                                             IDatabaseListener& listener)
       ORTHANC_OVERRIDE;
 
-    virtual bool SelectPatientToRecycle(int64_t& internalId,
-                                        int64_t patientIdToAvoid) 
-      ORTHANC_OVERRIDE;
-
-    virtual void SetGlobalProperty(GlobalProperty property,
-                                   const std::string& value) 
-      ORTHANC_OVERRIDE;
-
-    virtual void ClearMainDicomTags(int64_t id) 
-      ORTHANC_OVERRIDE;
-
-    virtual void SetMainDicomTag(int64_t id,
-                                 const DicomTag& tag,
-                                 const std::string& value) 
-      ORTHANC_OVERRIDE;
-
-    virtual void SetIdentifierTag(int64_t id,
-                                  const DicomTag& tag,
-                                  const std::string& value) 
-      ORTHANC_OVERRIDE;
-
-    virtual void SetMetadata(int64_t id,
-                             MetadataType type,
-                             const std::string& value) 
-      ORTHANC_OVERRIDE;
-
-    virtual void SetProtectedPatient(int64_t internalId, 
-                                     bool isProtected) 
-      ORTHANC_OVERRIDE;
-
-    virtual IDatabaseWrapper::ITransaction* StartTransaction() 
-      ORTHANC_OVERRIDE;
-
-    virtual void SetListener(IDatabaseListener& listener) 
-      ORTHANC_OVERRIDE
-    {
-      listener_ = &listener;
-    }
-
-    virtual unsigned int GetDatabaseVersion() 
-      ORTHANC_OVERRIDE;
+    virtual unsigned int GetDatabaseVersion() ORTHANC_OVERRIDE;
 
     virtual void Upgrade(unsigned int targetVersion,
-                         IStorageArea& storageArea) 
-      ORTHANC_OVERRIDE;
+                         IStorageArea& storageArea) ORTHANC_OVERRIDE;    
+
+    virtual bool HasRevisionsSupport() const ORTHANC_OVERRIDE
+    {
+      return false;  // No support for revisions in old API
+    }
 
     void AnswerReceived(const _OrthancPluginDatabaseAnswer& answer);
-
-    virtual bool IsDiskSizeAbove(uint64_t threshold) 
-      ORTHANC_OVERRIDE;
-
-    virtual void ApplyLookupResources(std::list<std::string>& resourcesId,
-                                      std::list<std::string>* instancesId,
-                                      const std::vector<DatabaseConstraint>& lookup,
-                                      ResourceType queryLevel,
-                                      size_t limit)
-      ORTHANC_OVERRIDE;
-
-    virtual bool CreateInstance(CreateInstanceResult& result,
-                                int64_t& instanceId,
-                                const std::string& patient,
-                                const std::string& study,
-                                const std::string& series,
-                                const std::string& instance)
-      ORTHANC_OVERRIDE;
-
-    // From the "ILookupResources" interface
-    virtual void GetAllInternalIds(std::list<int64_t>& target,
-                                   ResourceType resourceType) 
-      ORTHANC_OVERRIDE;
-
-    // From the "ILookupResources" interface
-    virtual void LookupIdentifier(std::list<int64_t>& result,
-                                  ResourceType level,
-                                  const DicomTag& tag,
-                                  Compatibility::IdentifierConstraintType type,
-                                  const std::string& value)
-      ORTHANC_OVERRIDE;
-    
-    // From the "ILookupResources" interface
-    virtual void LookupIdentifierRange(std::list<int64_t>& result,
-                                       ResourceType level,
-                                       const DicomTag& tag,
-                                       const std::string& start,
-                                       const std::string& end)
-      ORTHANC_OVERRIDE;
-
-    virtual void SetResourcesContent(const Orthanc::ResourcesContent& content)
-      ORTHANC_OVERRIDE;
-
-    virtual void GetChildrenMetadata(std::list<std::string>& target,
-                                     int64_t resourceId,
-                                     MetadataType metadata)
-      ORTHANC_OVERRIDE;
-
-    virtual int64_t GetLastChangeIndex() ORTHANC_OVERRIDE;
-  
-    virtual void TagMostRecentPatient(int64_t patient) ORTHANC_OVERRIDE;
-
-    virtual bool LookupResourceAndParent(int64_t& id,
-                                         ResourceType& type,
-                                         std::string& parentPublicId,
-                                         const std::string& publicId)
-      ORTHANC_OVERRIDE;
   };
 }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp	Tue Apr 20 18:11:29 2021 +0200
@@ -0,0 +1,1244 @@
+/**
+ * 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 General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * 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 "OrthancPluginDatabaseV3.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 OrthancPluginDatabaseV3::Transaction : public IDatabaseWrapper::ITransaction
+  {
+  private:
+    OrthancPluginDatabaseV3&           that_;
+    IDatabaseListener&                 listener_;
+    OrthancPluginDatabaseTransaction*  transaction_;
+
+    
+    void CheckSuccess(OrthancPluginErrorCode code) const
+    {
+      that_.CheckSuccess(code);
+    }
+    
+
+    static FileInfo Convert(const OrthancPluginAttachment& attachment)
+    {
+      return FileInfo(attachment.uuid,
+                      static_cast<FileContentType>(attachment.contentType),
+                      attachment.uncompressedSize,
+                      attachment.uncompressedHash,
+                      static_cast<CompressionType>(attachment.compressionType),
+                      attachment.compressedSize,
+                      attachment.compressedHash);
+    }
+
+
+    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++)
+      {
+        OrthancPluginDatabaseEvent event;
+        CheckSuccess(that_.backend_.readEvent(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(OrthancPluginDatabaseV3& 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
+    {
+      OrthancPluginAttachment 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();
+
+      CheckSuccess(that_.backend_.addAttachment(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)
+      {
+        OrthancPluginAttachment tmp;
+        CheckSuccess(that_.backend_.readAnswerAttachment(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 OrthancPluginDatabaseV3::CheckSuccess(OrthancPluginErrorCode code) const
+  {
+    if (code != OrthancPluginErrorCode_Success)
+    {
+      errorDictionary_.LogError(code, true);
+      throw OrthancException(static_cast<ErrorCode>(code));
+    }
+  }
+
+
+  OrthancPluginDatabaseV3::OrthancPluginDatabaseV3(SharedLibrary& library,
+                                                   PluginsErrorDictionary&  errorDictionary,
+                                                   const OrthancPluginDatabaseBackendV3* 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_, readAnswerAttachment);
+    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_, readEvent);
+
+    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_, rollback);
+    CHECK_FUNCTION_EXISTS(backend_, commit);
+    
+    CHECK_FUNCTION_EXISTS(backend_, addAttachment);
+    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);
+  }
+
+  
+  OrthancPluginDatabaseV3::~OrthancPluginDatabaseV3()
+  {
+    if (database_ != NULL)
+    {
+      OrthancPluginErrorCode code = backend_.destructDatabase(database_);
+      if (code != OrthancPluginErrorCode_Success)
+      {
+        // Don't throw exception in destructors
+        errorDictionary_.LogError(code, true);
+      }
+    }
+  }
+
+  
+  void OrthancPluginDatabaseV3::Open()
+  {
+    CheckSuccess(backend_.open(database_));
+  }
+
+
+  void OrthancPluginDatabaseV3::Close()
+  {
+    CheckSuccess(backend_.close(database_));
+  }
+  
+
+  IDatabaseWrapper::ITransaction* OrthancPluginDatabaseV3::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 OrthancPluginDatabaseV3::GetDatabaseVersion()
+  {
+    uint32_t version = 0;
+    CheckSuccess(backend_.getDatabaseVersion(database_, &version));
+    return version;
+  }
+
+  
+  void OrthancPluginDatabaseV3::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 OrthancPluginDatabaseV3::HasRevisionsSupport() const
+  {
+    // WARNING: This method requires "Open()" to have been called
+    uint8_t hasRevisions;
+    CheckSuccess(backend_.hasRevisionsSupport(database_, &hasRevisions));
+    return (hasRevisions != 0);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.h	Tue Apr 20 18:11:29 2021 +0200
@@ -0,0 +1,99 @@
+/**
+ * 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 General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * 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 OrthancPluginDatabaseV3 : public IDatabaseWrapper
+  {
+  private:
+    class Transaction;
+
+    SharedLibrary&                  library_;
+    PluginsErrorDictionary&         errorDictionary_;
+    OrthancPluginDatabaseBackendV3  backend_;
+    void*                           database_;
+    std::string                     serverIdentifier_;
+
+    void CheckSuccess(OrthancPluginErrorCode code) const;
+
+  public:
+    OrthancPluginDatabaseV3(SharedLibrary& library,
+                            PluginsErrorDictionary&  errorDictionary,
+                            const OrthancPluginDatabaseBackendV3* backend,
+                            size_t backendSize,
+                            void* database,
+                            const std::string& serverIdentifier);
+
+    virtual ~OrthancPluginDatabaseV3();
+
+    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;
+  };
+}
+
+#endif
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Tue Apr 20 18:11:29 2021 +0200
@@ -65,11 +65,14 @@
 #include "../../../OrthancFramework/Sources/SerializationToolbox.h"
 #include "../../../OrthancFramework/Sources/StringMemoryBuffer.h"
 #include "../../../OrthancFramework/Sources/Toolbox.h"
+#include "../../Sources/Database/VoidDatabaseListener.h"
 #include "../../Sources/OrthancConfiguration.h"
 #include "../../Sources/OrthancFindRequestHandler.h"
 #include "../../Sources/Search/HierarchicalMatcher.h"
 #include "../../Sources/ServerContext.h"
 #include "../../Sources/ServerToolbox.h"
+#include "OrthancPluginDatabase.h"
+#include "OrthancPluginDatabaseV3.h"
 #include "PluginsEnumerations.h"
 #include "PluginsJob.h"
 
@@ -1206,14 +1209,19 @@
     int argc_;
     char** argv_;
     std::unique_ptr<OrthancPluginDatabase>  database_;
+    std::unique_ptr<OrthancPluginDatabaseV3>  databaseV3_;  // New in Orthanc 1.9.2
     PluginsErrorDictionary  dictionary_;
-
-    PImpl() : 
+    std::string databaseServerIdentifier_;   // New in Orthanc 1.9.2
+    unsigned int maxDatabaseRetries_;   // New in Orthanc 1.9.2
+
+    explicit PImpl(const std::string& databaseServerIdentifier) : 
       context_(NULL), 
       findCallback_(NULL),
       worklistCallback_(NULL),
       argc_(1),
-      argv_(NULL)
+      argv_(NULL),
+      databaseServerIdentifier_(databaseServerIdentifier),
+      maxDatabaseRetries_(0)
     {
       memset(&moveCallbacks_, 0, sizeof(moveCallbacks_));
     }
@@ -1687,7 +1695,7 @@
   };
 
 
-  OrthancPlugins::OrthancPlugins()
+  OrthancPlugins::OrthancPlugins(const std::string& databaseServerIdentifier)
   {
     /* Sanity check of the compiler */
     if (sizeof(int32_t) != sizeof(OrthancPluginErrorCode) ||
@@ -1727,7 +1735,7 @@
       throw OrthancException(ErrorCode_Plugin);
     }
 
-    pimpl_.reset(new PImpl());
+    pimpl_.reset(new PImpl(databaseServerIdentifier));
     pimpl_->manager_.RegisterServiceProvider(*this);
   }
 
@@ -4206,8 +4214,11 @@
         }
         else
         {
+          // TODO - Plugins can only access global properties of their
+          // own Orthanc server (no access to the shared global properties)
           PImpl::ServerContextLock lock(*pimpl_);
-          lock.GetContext().GetIndex().SetGlobalProperty(static_cast<GlobalProperty>(p.property), p.value);
+          lock.GetContext().GetIndex().SetGlobalProperty(static_cast<GlobalProperty>(p.property),
+                                                         false /* not shared */, p.value);
           return true;
         }
       }
@@ -4220,8 +4231,11 @@
         std::string result;
 
         {
+          // TODO - Plugins can only access global properties of their
+          // own Orthanc server (no access to the shared global properties)
           PImpl::ServerContextLock lock(*pimpl_);
-          result = lock.GetContext().GetIndex().GetGlobalProperty(static_cast<GlobalProperty>(p.property), p.value);
+          result = lock.GetContext().GetIndex().GetGlobalProperty(static_cast<GlobalProperty>(p.property),
+                                                                  false /* not shared */, p.value);
         }
 
         *(p.result) = CopyString(result);
@@ -5018,12 +5032,14 @@
 
       case _OrthancPluginService_RegisterDatabaseBackend:
       {
-        CLOG(INFO, PLUGINS) << "Plugin has registered a custom database back-end";
+        LOG(WARNING) << "Performance warning: Plugin has registered a custom database back-end with an old API";
+        LOG(WARNING) << "The database backend has *no* support for revisions of metadata and attachments";
 
         const _OrthancPluginRegisterDatabaseBackend& p =
           *reinterpret_cast<const _OrthancPluginRegisterDatabaseBackend*>(parameters);
 
-        if (pimpl_->database_.get() == NULL)
+        if (pimpl_->database_.get() == NULL &&
+            pimpl_->databaseV3_.get() == NULL)
         {
           pimpl_->database_.reset(new OrthancPluginDatabase(plugin, GetErrorDictionary(), 
                                                             *p.backend, NULL, 0, p.payload));
@@ -5040,12 +5056,14 @@
 
       case _OrthancPluginService_RegisterDatabaseBackendV2:
       {
-        CLOG(INFO, PLUGINS) << "Plugin has registered a custom database back-end";
+        LOG(WARNING) << "Performance warning: Plugin has registered a custom database back-end with an old API";
+        LOG(WARNING) << "The database backend has *no* support for revisions of metadata and attachments";
 
         const _OrthancPluginRegisterDatabaseBackendV2& p =
           *reinterpret_cast<const _OrthancPluginRegisterDatabaseBackendV2*>(parameters);
 
-        if (pimpl_->database_.get() == NULL)
+        if (pimpl_->database_.get() == NULL &&
+            pimpl_->databaseV3_.get() == NULL)
         {
           pimpl_->database_.reset(new OrthancPluginDatabase(plugin, GetErrorDictionary(),
                                                             *p.backend, p.extensions,
@@ -5061,6 +5079,28 @@
         return true;
       }
 
+      case _OrthancPluginService_RegisterDatabaseBackendV3:
+      {
+        CLOG(INFO, PLUGINS) << "Plugin has registered a custom database back-end";
+
+        const _OrthancPluginRegisterDatabaseBackendV3& p =
+          *reinterpret_cast<const _OrthancPluginRegisterDatabaseBackendV3*>(parameters);
+
+        if (pimpl_->database_.get() == NULL &&
+            pimpl_->databaseV3_.get() == NULL)
+        {
+          pimpl_->databaseV3_.reset(new OrthancPluginDatabaseV3(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 (*)
 
@@ -5103,8 +5143,16 @@
                                  "The service ReconstructMainDicomTags can only be invoked by custom database plugins");
         }
 
-        IStorageArea& storage = *reinterpret_cast<IStorageArea*>(p.storageArea);
-        ServerToolbox::ReconstructMainDicomTags(*pimpl_->database_, storage, Plugins::Convert(p.level));
+        VoidDatabaseListener listener;
+        
+        {
+          IStorageArea& storage = *reinterpret_cast<IStorageArea*>(p.storageArea);
+
+          std::unique_ptr<IDatabaseWrapper::ITransaction> transaction(
+            pimpl_->database_->StartTransaction(TransactionType_ReadWrite, listener));
+          ServerToolbox::ReconstructMainDicomTags(*transaction, storage, Plugins::Convert(p.level));
+          transaction->Commit(0);
+        }
 
         return true;
       }
@@ -5172,7 +5220,8 @@
   bool OrthancPlugins::HasDatabaseBackend() const
   {
     boost::recursive_mutex::scoped_lock lock(pimpl_->invokeServiceMutex_);
-    return pimpl_->database_.get() != NULL;
+    return (pimpl_->database_.get() != NULL ||
+            pimpl_->databaseV3_.get() != NULL);
   }
 
 
@@ -5204,27 +5253,35 @@
 
   IDatabaseWrapper& OrthancPlugins::GetDatabaseBackend()
   {
-    if (!HasDatabaseBackend())
+    if (pimpl_->database_.get() != NULL)
+    {
+      return *pimpl_->database_;
+    }
+    else if (pimpl_->databaseV3_.get() != NULL)
+    {
+      return *pimpl_->databaseV3_;
+    }
+    else
     {
       throw OrthancException(ErrorCode_BadSequenceOfCalls);
     }
-    else
-    {
-      return *pimpl_->database_;
-    }
   }
 
 
   const SharedLibrary& OrthancPlugins::GetDatabaseBackendLibrary() const
   {
-    if (!HasDatabaseBackend())
+    if (pimpl_->database_.get() != NULL)
+    {
+      return pimpl_->database_->GetSharedLibrary();
+    }
+    else if (pimpl_->databaseV3_.get() != NULL)
+    {
+      return pimpl_->databaseV3_->GetSharedLibrary();
+    }
+    else
     {
       throw OrthancException(ErrorCode_BadSequenceOfCalls);
     }
-    else
-    {
-      return pimpl_->database_->GetSharedLibrary();
-    }
   }
 
 
@@ -5702,4 +5759,11 @@
     boost::recursive_mutex::scoped_lock lock(pimpl_->invokeServiceMutex_);
     return (pimpl_->authorizationTokens_.find(token) != pimpl_->authorizationTokens_.end());
   }
+
+  
+  unsigned int OrthancPlugins::GetMaxDatabaseRetries() const
+  {
+    boost::recursive_mutex::scoped_lock lock(pimpl_->invokeServiceMutex_);
+    return pimpl_->maxDatabaseRetries_;
+  }
 }
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.h	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPlugins.h	Tue Apr 20 18:11:29 2021 +0200
@@ -61,10 +61,10 @@
 #include "../../../OrthancFramework/Sources/HttpServer/IHttpHandler.h"
 #include "../../../OrthancFramework/Sources/HttpServer/IIncomingHttpRequestFilter.h"
 #include "../../../OrthancFramework/Sources/JobsEngine/IJob.h"
+#include "../../Sources/Database/IDatabaseWrapper.h"
 #include "../../Sources/IDicomImageDecoder.h"
 #include "../../Sources/IServerListener.h"
 #include "../../Sources/ServerJobs/IStorageCommitmentFactory.h"
-#include "OrthancPluginDatabase.h"
 #include "PluginsManager.h"
 
 #include <list>
@@ -247,7 +247,7 @@
                                  bool allowNewSopInstanceUid) ORTHANC_OVERRIDE;
     
   public:
-    OrthancPlugins();
+    explicit OrthancPlugins(const std::string& databaseServerIdentifier);
 
     virtual ~OrthancPlugins();
 
@@ -389,6 +389,8 @@
 
     // New in Orthanc 1.8.1 (cf. "OrthancPluginGenerateRestApiAuthorizationToken()")
     bool IsValidAuthorizationToken(const std::string& token) const;
+
+    unsigned int GetMaxDatabaseRetries() const;
   };
 }
 
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCDatabasePlugin.h	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancCDatabasePlugin.h	Tue Apr 20 18:11:29 2021 +0200
@@ -44,6 +44,14 @@
   typedef struct _OrthancPluginDatabaseContext_t OrthancPluginDatabaseContext;
 
 
+  /**
+   * Opaque structure that represents a transaction of a custom database engine.
+   * New in Orthanc 1.9.2.
+   * @ingroup Callbacks
+   **/
+  typedef struct _OrthancPluginDatabaseTransaction_t OrthancPluginDatabaseTransaction;
+
+
 /*<! @cond Doxygen_Suppress */
   typedef enum
   {
@@ -966,6 +974,392 @@
   }
 
 
+
+  /**
+   * New interface starting with Orthanc 1.9.2
+   **/
+
+/*<! @cond Doxygen_Suppress */
+  typedef enum
+  {
+    OrthancPluginDatabaseTransactionType_ReadOnly = 1,
+    OrthancPluginDatabaseTransactionType_ReadWrite = 2,
+    OrthancPluginDatabaseTransactionType_INTERNAL = 0x7fffffff
+  } OrthancPluginDatabaseTransactionType;
+
+
+  typedef enum
+  {
+    OrthancPluginDatabaseEventType_DeletedAttachment = 1,
+    OrthancPluginDatabaseEventType_DeletedResource = 2,
+    OrthancPluginDatabaseEventType_RemainingAncestor = 3,
+    OrthancPluginDatabaseEventType_INTERNAL = 0x7fffffff
+  } OrthancPluginDatabaseEventType;
+
+
+  typedef struct
+  {
+    OrthancPluginDatabaseEventType type;
+
+    union
+    {
+      struct
+      {
+        /* For ""DeletedResource" and "RemainingAncestor" */
+        OrthancPluginResourceType  level;
+        const char*                publicId;
+      } resource;
+
+      /* For "DeletedAttachment" */
+      OrthancPluginAttachment  attachment;
+      
+    } content;
+    
+  } OrthancPluginDatabaseEvent;
+
+  
+  typedef struct
+  {
+    /**
+     * Functions to read the answers inside a transaction
+     **/
+    
+    OrthancPluginErrorCode (*readAnswersCount) (OrthancPluginDatabaseTransaction* transaction,
+                                                uint32_t* target /* out */);
+
+    OrthancPluginErrorCode (*readAnswerAttachment) (OrthancPluginDatabaseTransaction* transaction,
+                                                    OrthancPluginAttachment* target /* out */,
+                                                    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 (*readEvent) (OrthancPluginDatabaseTransaction* transaction,
+                                         OrthancPluginDatabaseEvent* event /* out */,
+                                         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 (*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 (*addAttachment) (OrthancPluginDatabaseTransaction* transaction,
+                                             int64_t id,
+                                             const OrthancPluginAttachment* attachment,
+                                             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);
+    
+
+  } OrthancPluginDatabaseBackendV3;
+
+/*<! @endcond */
+  
+
+  typedef struct
+  {
+    const OrthancPluginDatabaseBackendV3*  backend;
+    uint32_t                               backendSize;
+    uint32_t                               maxDatabaseRetries;
+    void*                                  database;
+  } _OrthancPluginRegisterDatabaseBackendV3;
+
+
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterDatabaseBackendV3(
+    OrthancPluginContext*                  context,
+    const OrthancPluginDatabaseBackendV3*  backend,
+    uint32_t                               backendSize,
+    uint32_t                               maxDatabaseRetries,  /* To handle "OrthancPluginErrorCode_DatabaseCannotSerialize" */
+    void*                                  database)
+  {
+    _OrthancPluginRegisterDatabaseBackendV3 params;
+
+    if (sizeof(int32_t) != sizeof(_OrthancPluginDatabaseAnswerType))
+    {
+      return OrthancPluginErrorCode_Plugin;
+    }
+
+    memset(&params, 0, sizeof(params));
+    params.backend = backend;
+    params.backendSize = sizeof(OrthancPluginDatabaseBackendV3);
+    params.maxDatabaseRetries = maxDatabaseRetries;
+    params.database = database;
+
+    return context->InvokeService(context, _OrthancPluginService_RegisterDatabaseBackendV3, &params);
+  }
+  
 #ifdef  __cplusplus
 }
 #endif
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Tue Apr 20 18:11:29 2021 +0200
@@ -17,7 +17,7 @@
  *    - Possibly register its callback for received DICOM instances using ::OrthancPluginRegisterOnStoredInstanceCallback().
  *    - Possibly register its callback for changes to the DICOM store using ::OrthancPluginRegisterOnChangeCallback().
  *    - Possibly register a custom storage area using ::OrthancPluginRegisterStorageArea2().
- *    - Possibly register a custom database back-end area using OrthancPluginRegisterDatabaseBackendV2().
+ *    - Possibly register a custom database back-end area using OrthancPluginRegisterDatabaseBackendV3().
  *    - Possibly register a handler for C-Find SCP using OrthancPluginRegisterFindCallback().
  *    - Possibly register a handler for C-Find SCP against DICOM worklists using OrthancPluginRegisterWorklistCallback().
  *    - Possibly register a handler for C-Move SCP using OrthancPluginRegisterMoveCallback().
@@ -239,6 +239,8 @@
     OrthancPluginErrorCode_SslInitialization = 39    /*!< Cannot initialize SSL encryption, check out your certificates */,
     OrthancPluginErrorCode_DiscontinuedAbi = 40    /*!< Calling a function that has been removed from the Orthanc Framework */,
     OrthancPluginErrorCode_BadRange = 41    /*!< Incorrect range request */,
+    OrthancPluginErrorCode_DatabaseCannotSerialize = 42    /*!< Database could not serialize access due to concurrent update, the transaction should be retried */,
+    OrthancPluginErrorCode_Revision = 43    /*!< A bad revision number was provided, which might indicate conflict between multiple writers */,
     OrthancPluginErrorCode_SQLiteNotOpened = 1000    /*!< SQLite: The database is not opened */,
     OrthancPluginErrorCode_SQLiteAlreadyOpened = 1001    /*!< SQLite: Connection is already open */,
     OrthancPluginErrorCode_SQLiteCannotOpen = 1002    /*!< SQLite: Unable to open the database */,
@@ -518,12 +520,13 @@
     _OrthancPluginService_GetInstanceDicomWebXml = 4019,   /* New in Orthanc 1.7.0 */
     
     /* Services for plugins implementing a database back-end */
-    _OrthancPluginService_RegisterDatabaseBackend = 5000,
+    _OrthancPluginService_RegisterDatabaseBackend = 5000,    /* New in Orthanc 0.8.6 */
     _OrthancPluginService_DatabaseAnswer = 5001,
-    _OrthancPluginService_RegisterDatabaseBackendV2 = 5002,
+    _OrthancPluginService_RegisterDatabaseBackendV2 = 5002,  /* New in Orthanc 0.9.4 */
     _OrthancPluginService_StorageAreaCreate = 5003,
     _OrthancPluginService_StorageAreaRead = 5004,
     _OrthancPluginService_StorageAreaRemove = 5005,
+    _OrthancPluginService_RegisterDatabaseBackendV3 = 5006,  /* New in Orthanc 1.9.2 */
 
     /* Primitives for handling images */
     _OrthancPluginService_GetImagePixelFormat = 6000,
--- a/OrthancServer/Resources/Configuration.json	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancServer/Resources/Configuration.json	Tue Apr 20 18:11:29 2021 +0200
@@ -768,5 +768,27 @@
   // network protocol, expressed in bytes. This value affects both
   // Orthanc SCU and Orthanc SCP. It defaults to 16KB. The allowed
   // range is [4096,131072]. (new in Orthanc 1.9.0)
-  "MaximumPduLength" : 16384
+  "MaximumPduLength" : 16384,
+
+  // Arbitrary identifier of this Orthanc server when storing its
+  // global properties if a custom index plugin is used. This
+  // identifier is only useful in the case of multiple
+  // readers/writers, in order to avoid collisions between multiple
+  // Orthanc servers. If unset, this identifier is taken as a SHA-1
+  // hash derived from the MAC adddresses of the network interfaces,
+  // and from the AET and TCP ports used by Orthanc. Manually setting
+  // this option is needed in Docker/Kubernetes environments. (new in
+  // Orthanc 1.9.2)
+  /**
+     "DatabaseServerIdentifier" : "Orthanc1",
+  **/
+
+  // Whether Orthanc protects the modification of metadata and
+  // attachments using revisions, which is done using the HTTP headers
+  // "ETag", "If-Match" and "If-None-Match" in the calls to the REST
+  // API. This is needed to handle collisions between concurrent
+  // modifications in the case of multiple writers. The database
+  // back-end must support this option, which is notably *not* yet the
+  // case of the built-in SQLite index. (new in Orthanc 1.9.2)
+  "CheckRevisions" : false
 }
--- a/OrthancServer/Resources/RunCppCheck.sh	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancServer/Resources/RunCppCheck.sh	Tue Apr 20 18:11:29 2021 +0200
@@ -17,8 +17,8 @@
 stlFindInsert:../../OrthancFramework/Sources/DicomFormat/DicomMap.cpp:1194
 stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:164
 stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:72
-stlFindInsert:../../OrthancServer/Sources/OrthancWebDav.cpp:385
-stlFindInsert:../../OrthancServer/Sources/ServerIndex.cpp:400
+stlFindInsert:../../OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp:383
+stlFindInsert:../../OrthancServer/Sources/OrthancWebDav.cpp:386
 syntaxError:../../OrthancFramework/Sources/SQLite/FunctionContext.h:50
 syntaxError:../../OrthancFramework/UnitTestsSources/ZipTests.cpp:130
 syntaxError:../../OrthancServer/UnitTestsSources/UnitTestsMain.cpp:321
--- a/OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp	Tue Apr 20 18:11:29 2021 +0200
@@ -161,7 +161,7 @@
 
     
     static void ApplyLevel(SetOfResources& candidates,
-                           IDatabaseWrapper& database,
+                           IDatabaseWrapper::ITransaction& transaction,
                            ILookupResources& compatibility,
                            const std::vector<DatabaseConstraint>& lookup,
                            ResourceType level)
@@ -267,7 +267,7 @@
              candidate != source.end(); ++candidate)
         {
           DicomMap tags;
-          database.GetMainDicomTags(tags, *candidate);
+          transaction.GetMainDicomTags(tags, *candidate);
 
           bool match = true;
 
@@ -291,7 +291,7 @@
     }
 
 
-    static std::string GetOneInstance(IDatabaseWrapper& compatibility,
+    static std::string GetOneInstance(IDatabaseWrapper::ITransaction& compatibility,
                                       int64_t resource,
                                       ResourceType level)
     {
@@ -348,11 +348,11 @@
       assert(upperLevel <= queryLevel &&
              queryLevel <= lowerLevel);
 
-      SetOfResources candidates(database_, upperLevel);
+      SetOfResources candidates(transaction_, upperLevel);
 
       for (int level = upperLevel; level <= lowerLevel; level++)
       {
-        ApplyLevel(candidates, database_, compatibility_, lookup, static_cast<ResourceType>(level));
+        ApplyLevel(candidates, transaction_, compatibility_, lookup, static_cast<ResourceType>(level));
 
         if (level != lowerLevel)
         {
@@ -372,7 +372,7 @@
                it = resources.begin(); it != resources.end(); ++it)
         {
           int64_t parent;
-          if (database_.LookupParent(parent, *it))
+          if (transaction_.LookupParent(parent, *it))
           {
             parents.push_back(parent);
           }
@@ -396,9 +396,9 @@
       for (std::list<int64_t>::const_iterator
              it = resources.begin(); it != resources.end(); ++it, pos++)
       {
-        assert(database_.GetResourceType(*it) == queryLevel);
+        assert(transaction_.GetResourceType(*it) == queryLevel);
 
-        const std::string resource = database_.GetPublicId(*it);
+        const std::string resource = transaction_.GetPublicId(*it);
         resourcesId.push_back(resource);
 
         if (instancesId != NULL)
@@ -411,7 +411,7 @@
           else
           {
             // Collect one child instance for each of the selected resources
-            instancesId->push_back(GetOneInstance(database_, *it, queryLevel));
+            instancesId->push_back(GetOneInstance(transaction_, *it, queryLevel));
           }
         }
       }
--- a/OrthancServer/Sources/Database/Compatibility/DatabaseLookup.h	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancServer/Sources/Database/Compatibility/DatabaseLookup.h	Tue Apr 20 18:11:29 2021 +0200
@@ -43,13 +43,13 @@
     class DatabaseLookup : public boost::noncopyable
     {
     private:
-      IDatabaseWrapper&  database_;
+      IDatabaseWrapper::ITransaction&  transaction_;
       ILookupResources&  compatibility_;
 
     public:
-      DatabaseLookup(IDatabaseWrapper& database,
+      DatabaseLookup(IDatabaseWrapper::ITransaction& transaction,
                      ILookupResources& compatibility) :
-        database_(database),
+        transaction_(transaction),
         compatibility_(compatibility)
       {
       }
--- a/OrthancServer/Sources/Database/Compatibility/IGetChildrenMetadata.cpp	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancServer/Sources/Database/Compatibility/IGetChildrenMetadata.cpp	Tue Apr 20 18:11:29 2021 +0200
@@ -56,7 +56,8 @@
              it = children.begin(); it != children.end(); ++it)
       {
         std::string value;
-        if (database.LookupMetadata(value, *it, metadata))
+        int64_t revision;  // Ignored
+        if (database.LookupMetadata(value, revision, *it, metadata))
         {
           target.push_back(value);
         }
--- a/OrthancServer/Sources/Database/Compatibility/IGetChildrenMetadata.h	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancServer/Sources/Database/Compatibility/IGetChildrenMetadata.h	Tue Apr 20 18:11:29 2021 +0200
@@ -49,6 +49,7 @@
                                          int64_t id) = 0;
 
       virtual bool LookupMetadata(std::string& target,
+                                  int64_t& revision,
                                   int64_t id,
                                   MetadataType type) = 0;
 
--- a/OrthancServer/Sources/Database/Compatibility/ILookupResources.cpp	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancServer/Sources/Database/Compatibility/ILookupResources.cpp	Tue Apr 20 18:11:29 2021 +0200
@@ -41,7 +41,7 @@
   namespace Compatibility
   {
     void ILookupResources::Apply(
-      IDatabaseWrapper& database,
+      IDatabaseWrapper::ITransaction& transaction,
       ILookupResources& compatibility,
       std::list<std::string>& resourcesId,
       std::list<std::string>* instancesId,
@@ -49,7 +49,7 @@
       ResourceType queryLevel,
       size_t limit)
     {
-      Compatibility::DatabaseLookup compat(database, compatibility);
+      Compatibility::DatabaseLookup compat(transaction, compatibility);
       compat.ApplyLookupResources(resourcesId, instancesId, lookup, queryLevel, limit);
     }
   }
--- a/OrthancServer/Sources/Database/Compatibility/ILookupResources.h	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancServer/Sources/Database/Compatibility/ILookupResources.h	Tue Apr 20 18:11:29 2021 +0200
@@ -66,7 +66,7 @@
                                          const std::string& start,
                                          const std::string& end) = 0;
 
-      static void Apply(IDatabaseWrapper& database,
+      static void Apply(IDatabaseWrapper::ITransaction& transaction,
                         ILookupResources& compatibility,
                         std::list<std::string>& resourcesId,
                         std::list<std::string>* instancesId,
--- a/OrthancServer/Sources/Database/Compatibility/ISetResourcesContent.h	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancServer/Sources/Database/Compatibility/ISetResourcesContent.h	Tue Apr 20 18:11:29 2021 +0200
@@ -56,7 +56,8 @@
 
       virtual void SetMetadata(int64_t id,
                                MetadataType type,
-                               const std::string& value) = 0;
+                               const std::string& value,
+                               int64_t revision) = 0;
 
       static void Apply(ISetResourcesContent& that,
                         const ResourcesContent& content)
--- a/OrthancServer/Sources/Database/Compatibility/SetOfResources.cpp	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancServer/Sources/Database/Compatibility/SetOfResources.cpp	Tue Apr 20 18:11:29 2021 +0200
@@ -90,7 +90,7 @@
              it != resources_->end(); ++it)
         {
           std::list<int64_t> tmp;
-          database_.GetChildrenInternalId(tmp, *it);
+          transaction_.GetChildrenInternalId(tmp, *it);
 
           for (std::list<int64_t>::const_iterator
                  child = tmp.begin(); child != tmp.end(); ++child)
@@ -133,14 +133,14 @@
       if (resources_.get() == NULL)
       {
         // All the resources of this level are part of the filter
-        database_.GetAllPublicIds(result, level_);
+        transaction_.GetAllPublicIds(result, level_);
       }
       else
       {
         for (Resources::const_iterator it = resources_->begin(); 
              it != resources_->end(); ++it)
         {
-          result.push_back(database_.GetPublicId(*it));
+          result.push_back(transaction_.GetPublicId(*it));
         }
       }
     }
--- a/OrthancServer/Sources/Database/Compatibility/SetOfResources.h	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancServer/Sources/Database/Compatibility/SetOfResources.h	Tue Apr 20 18:11:29 2021 +0200
@@ -49,14 +49,14 @@
     private:
       typedef std::set<int64_t>  Resources;
 
-      IDatabaseWrapper&           database_;
+      IDatabaseWrapper::ITransaction&  transaction_;
       ResourceType                level_;
       std::unique_ptr<Resources>  resources_;
     
     public:
-      SetOfResources(IDatabaseWrapper& database,
+      SetOfResources(IDatabaseWrapper::ITransaction& transaction,
                      ResourceType level) : 
-        database_(database),
+        transaction_(transaction),
         level_(level)
       {
       }
--- a/OrthancServer/Sources/Database/IDatabaseListener.h	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancServer/Sources/Database/IDatabaseListener.h	Tue Apr 20 18:11:29 2021 +0200
@@ -33,9 +33,10 @@
 
 #pragma once
 
+#include "../../../OrthancFramework/Sources/FileStorage/FileInfo.h"
 #include "../ServerEnumerations.h"
-#include "../ServerIndexChange.h"
 
+#include <boost/noncopyable.hpp>
 #include <string>
 
 namespace Orthanc
@@ -50,8 +51,9 @@
     virtual void SignalRemainingAncestor(ResourceType parentType,
                                          const std::string& publicId) = 0;
 
-    virtual void SignalFileDeleted(const FileInfo& info) = 0;
+    virtual void SignalAttachmentDeleted(const FileInfo& info) = 0;
 
-    virtual void SignalChange(const ServerIndexChange& change) = 0;
+    virtual void SignalResourceDeleted(ResourceType type,
+                                       const std::string& publicId) = 0;
   };
 }
--- a/OrthancServer/Sources/Database/IDatabaseWrapper.h	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancServer/Sources/Database/IDatabaseWrapper.h	Tue Apr 20 18:11:29 2021 +0200
@@ -36,9 +36,8 @@
 #include "../../../OrthancFramework/Sources/DicomFormat/DicomMap.h"
 #include "../../../OrthancFramework/Sources/FileStorage/FileInfo.h"
 #include "../../../OrthancFramework/Sources/FileStorage/IStorageArea.h"
-#include "../../../OrthancFramework/Sources/SQLite/ITransaction.h"
-
 #include "../ExportedResource.h"
+#include "../ServerIndexChange.h"
 #include "IDatabaseListener.h"
 
 #include <list>
@@ -54,21 +53,6 @@
   class IDatabaseWrapper : public boost::noncopyable
   {
   public:
-    class ITransaction : public boost::noncopyable
-    {
-    public:
-      virtual ~ITransaction()
-      {
-      }
-
-      virtual void Begin() = 0;
-
-      virtual void Rollback() = 0;
-
-      virtual void Commit(int64_t fileSizeDelta) = 0;
-    };
-
-
     struct CreateInstanceResult
     {
       bool     isNewPatient_;
@@ -79,6 +63,192 @@
       int64_t  seriesId_;
     };
 
+
+    class ITransaction : public boost::noncopyable
+    {
+    public:
+      virtual ~ITransaction()
+      {
+      }
+
+      virtual void Rollback() = 0;
+
+      // The "fileSizeDelta" is used for older database plugins that
+      // have no fast way to compute the size of all the stored
+      // attachments (cf. "fastGetTotalSize_")
+      virtual void Commit(int64_t fileSizeDelta) = 0;
+
+      // A call to "AddAttachment()" guarantees that this attachment
+      // is not already existing. This is different from
+      // "SetMetadata()" that might have to replace an older value.
+      virtual void AddAttachment(int64_t id,
+                                 const FileInfo& attachment,
+                                 int64_t revision) = 0;
+
+      virtual void ClearChanges() = 0;
+
+      virtual void ClearExportedResources() = 0;
+
+      virtual void DeleteAttachment(int64_t id,
+                                    FileContentType attachment) = 0;
+
+      virtual void DeleteMetadata(int64_t id,
+                                  MetadataType type) = 0;
+
+      virtual void DeleteResource(int64_t id) = 0;
+
+      virtual void GetAllMetadata(std::map<MetadataType, std::string>& target,
+                                  int64_t id) = 0;
+
+      virtual void GetAllPublicIds(std::list<std::string>& target,
+                                   ResourceType resourceType) = 0;
+
+      virtual void GetAllPublicIds(std::list<std::string>& target,
+                                   ResourceType resourceType,
+                                   size_t since,
+                                   size_t limit) = 0;
+
+      virtual void GetChanges(std::list<ServerIndexChange>& target /*out*/,
+                              bool& done /*out*/,
+                              int64_t since,
+                              uint32_t maxResults) = 0;
+
+      virtual void GetChildrenInternalId(std::list<int64_t>& target,
+                                         int64_t id) = 0;
+
+      virtual void GetChildrenPublicId(std::list<std::string>& target,
+                                       int64_t id) = 0;
+
+      virtual void GetExportedResources(std::list<ExportedResource>& target /*out*/,
+                                        bool& done /*out*/,
+                                        int64_t since,
+                                        uint32_t maxResults) = 0;
+
+      virtual void GetLastChange(std::list<ServerIndexChange>& target /*out*/) = 0;
+
+      virtual void GetLastExportedResource(std::list<ExportedResource>& target /*out*/) = 0;
+
+      virtual void GetMainDicomTags(DicomMap& map,
+                                    int64_t id) = 0;
+
+      virtual std::string GetPublicId(int64_t resourceId) = 0;
+
+      virtual uint64_t GetResourcesCount(ResourceType resourceType) = 0;
+
+      virtual ResourceType GetResourceType(int64_t resourceId) = 0;
+
+      virtual uint64_t GetTotalCompressedSize() = 0;
+    
+      virtual uint64_t GetTotalUncompressedSize() = 0;
+
+      virtual bool IsExistingResource(int64_t internalId) = 0;
+
+      virtual bool IsProtectedPatient(int64_t internalId) = 0;
+
+      virtual void ListAvailableAttachments(std::set<FileContentType>& target,
+                                            int64_t id) = 0;
+
+      virtual void LogChange(int64_t internalId,
+                             const ServerIndexChange& change) = 0;
+
+      virtual void LogExportedResource(const ExportedResource& resource) = 0;
+    
+      virtual bool LookupAttachment(FileInfo& attachment,
+                                    int64_t& revision,
+                                    int64_t id,
+                                    FileContentType contentType) = 0;
+
+      /**
+       * If "shared" is "true", the property is shared by all the
+       * Orthanc servers that access the same database. If "shared" is
+       * "false", the property is private to the server (cf. the
+       * "DatabaseServerIdentifier" configuration option).
+       **/
+      virtual bool LookupGlobalProperty(std::string& target,
+                                        GlobalProperty property,
+                                        bool shared) = 0;
+
+      virtual bool LookupMetadata(std::string& target,
+                                  int64_t& revision,
+                                  int64_t id,
+                                  MetadataType type) = 0;
+
+      virtual bool LookupParent(int64_t& parentId,
+                                int64_t resourceId) = 0;
+
+      virtual bool LookupResource(int64_t& id,
+                                  ResourceType& type,
+                                  const std::string& publicId) = 0;
+
+      virtual bool SelectPatientToRecycle(int64_t& internalId) = 0;
+
+      virtual bool SelectPatientToRecycle(int64_t& internalId,
+                                          int64_t patientIdToAvoid) = 0;
+
+      virtual void SetGlobalProperty(GlobalProperty property,
+                                     bool shared,
+                                     const std::string& value) = 0;
+
+      virtual void ClearMainDicomTags(int64_t id) = 0;
+
+      virtual void SetMetadata(int64_t id,
+                               MetadataType type,
+                               const std::string& value,
+                               int64_t revision) = 0;
+
+      virtual void SetProtectedPatient(int64_t internalId, 
+                                       bool isProtected) = 0;
+
+
+      /**
+       * Primitives introduced in Orthanc 1.5.2
+       **/
+    
+      virtual bool IsDiskSizeAbove(uint64_t threshold) = 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) = 0;
+
+      // Returns "true" iff. the instance is new and has been inserted
+      // into the database. If "false" is returned, the content of
+      // "result" is undefined, but "instanceId" must be properly
+      // set. This method must also tag the parent patient as the most
+      // recent in the patient recycling order if it is not protected
+      // (so as to fix issue #58).
+      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) = 0;
+
+      // It is guaranteed that the resources to be modified have no main
+      // DICOM tags, and no DICOM identifiers associated with
+      // them. However, some metadata might be already existing, and
+      // have to be overwritten.
+      virtual void SetResourcesContent(const ResourcesContent& content) = 0;
+
+      virtual void GetChildrenMetadata(std::list<std::string>& target,
+                                       int64_t resourceId,
+                                       MetadataType metadata) = 0;
+
+      virtual int64_t GetLastChangeIndex() = 0;
+
+
+      /**
+       * Primitives introduced in Orthanc 1.5.4
+       **/
+
+      virtual bool LookupResourceAndParent(int64_t& id,
+                                           ResourceType& type,
+                                           std::string& parentPublicId,
+                                           const std::string& publicId) = 0;
+    };
+
+
     virtual ~IDatabaseWrapper()
     {
     }
@@ -87,171 +257,18 @@
 
     virtual void Close() = 0;
 
-    virtual void AddAttachment(int64_t id,
-                               const FileInfo& attachment) = 0;
-
-    virtual void ClearChanges() = 0;
-
-    virtual void ClearExportedResources() = 0;
-
-    virtual void DeleteAttachment(int64_t id,
-                                  FileContentType attachment) = 0;
-
-    virtual void DeleteMetadata(int64_t id,
-                                MetadataType type) = 0;
-
-    virtual void DeleteResource(int64_t id) = 0;
-
     virtual void FlushToDisk() = 0;
 
     virtual bool HasFlushToDisk() const = 0;
 
-    virtual void GetAllMetadata(std::map<MetadataType, std::string>& target,
-                                int64_t id) = 0;
-
-    virtual void GetAllPublicIds(std::list<std::string>& target,
-                                 ResourceType resourceType) = 0;
-
-    virtual void GetAllPublicIds(std::list<std::string>& target,
-                                 ResourceType resourceType,
-                                 size_t since,
-                                 size_t limit) = 0;
-
-    virtual void GetChanges(std::list<ServerIndexChange>& target /*out*/,
-                            bool& done /*out*/,
-                            int64_t since,
-                            uint32_t maxResults) = 0;
-
-    virtual void GetChildrenInternalId(std::list<int64_t>& target,
-                                       int64_t id) = 0;
-
-    virtual void GetChildrenPublicId(std::list<std::string>& target,
-                                     int64_t id) = 0;
-
-    virtual void GetExportedResources(std::list<ExportedResource>& target /*out*/,
-                                      bool& done /*out*/,
-                                      int64_t since,
-                                      uint32_t maxResults) = 0;
-
-    virtual void GetLastChange(std::list<ServerIndexChange>& target /*out*/) = 0;
-
-    virtual void GetLastExportedResource(std::list<ExportedResource>& target /*out*/) = 0;
-
-    virtual void GetMainDicomTags(DicomMap& map,
-                                  int64_t id) = 0;
-
-    virtual std::string GetPublicId(int64_t resourceId) = 0;
-
-    virtual uint64_t GetResourceCount(ResourceType resourceType) = 0;
-
-    virtual ResourceType GetResourceType(int64_t resourceId) = 0;
-
-    virtual uint64_t GetTotalCompressedSize() = 0;
-    
-    virtual uint64_t GetTotalUncompressedSize() = 0;
-
-    virtual bool IsExistingResource(int64_t internalId) = 0;
-
-    virtual bool IsProtectedPatient(int64_t internalId) = 0;
-
-    virtual void ListAvailableAttachments(std::set<FileContentType>& target,
-                                          int64_t id) = 0;
-
-    virtual void LogChange(int64_t internalId,
-                           const ServerIndexChange& change) = 0;
-
-    virtual void LogExportedResource(const ExportedResource& resource) = 0;
-    
-    virtual bool LookupAttachment(FileInfo& attachment,
-                                  int64_t id,
-                                  FileContentType contentType) = 0;
-
-    virtual bool LookupGlobalProperty(std::string& target,
-                                      GlobalProperty property) = 0;
-
-    virtual bool LookupMetadata(std::string& target,
-                                int64_t id,
-                                MetadataType type) = 0;
-
-    virtual bool LookupParent(int64_t& parentId,
-                              int64_t resourceId) = 0;
-
-    virtual bool LookupResource(int64_t& id,
-                                ResourceType& type,
-                                const std::string& publicId) = 0;
-
-    virtual bool SelectPatientToRecycle(int64_t& internalId) = 0;
-
-    virtual bool SelectPatientToRecycle(int64_t& internalId,
-                                        int64_t patientIdToAvoid) = 0;
-
-    virtual void SetGlobalProperty(GlobalProperty property,
-                                   const std::string& value) = 0;
-
-    virtual void ClearMainDicomTags(int64_t id) = 0;
-
-    virtual void SetMetadata(int64_t id,
-                             MetadataType type,
-                             const std::string& value) = 0;
-
-    virtual void SetProtectedPatient(int64_t internalId, 
-                                     bool isProtected) = 0;
-
-    virtual ITransaction* StartTransaction() = 0;
-
-    virtual void SetListener(IDatabaseListener& listener) = 0;
+    virtual ITransaction* StartTransaction(TransactionType type,
+                                           IDatabaseListener& listener) = 0;
 
     virtual unsigned int GetDatabaseVersion() = 0;
 
     virtual void Upgrade(unsigned int targetVersion,
                          IStorageArea& storageArea) = 0;
 
-
-    /**
-     * Primitives introduced in Orthanc 1.5.2
-     **/
-    
-    virtual bool IsDiskSizeAbove(uint64_t threshold) = 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) = 0;
-
-    // Returns "true" iff. the instance is new and has been inserted
-    // into the database. If "false" is returned, the content of
-    // "result" is undefined, but "instanceId" must be properly
-    // set. This method must also tag the parent patient as the most
-    // recent in the patient recycling order if it is not protected
-    // (so as to fix issue #58).
-    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) = 0;
-
-    // It is guaranteed that the resources to be modified have no main
-    // DICOM tags, and no DICOM identifiers associated with
-    // them. However, some metadata might be already existing, and
-    // have to be overwritten.
-    virtual void SetResourcesContent(const ResourcesContent& content) = 0;
-
-    virtual void GetChildrenMetadata(std::list<std::string>& target,
-                                     int64_t resourceId,
-                                     MetadataType metadata) = 0;
-
-    virtual int64_t GetLastChangeIndex() = 0;
-
-
-    /**
-     * Primitives introduced in Orthanc 1.5.4
-     **/
-
-    virtual bool LookupResourceAndParent(int64_t& id,
-                                         ResourceType& type,
-                                         std::string& parentPublicId,
-                                         const std::string& publicId) = 0;
+    virtual bool HasRevisionsSupport() const = 0;
   };
 }
--- a/OrthancServer/Sources/Database/ResourcesContent.cpp	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancServer/Sources/Database/ResourcesContent.cpp	Tue Apr 20 18:11:29 2021 +0200
@@ -35,12 +35,119 @@
 #include "ResourcesContent.h"
 
 #include "Compatibility/ISetResourcesContent.h"
+#include "../ServerToolbox.h"
+
+#include "../../../OrthancFramework/Sources/DicomFormat/DicomArray.h"
+#include "../../../OrthancFramework/Sources/OrthancException.h"
 
 #include <cassert>
 
 
 namespace Orthanc
 {
+  static void StoreMainDicomTagsInternal(ResourcesContent& target,
+                                         int64_t resource,
+                                         const DicomMap& tags)
+  {
+    DicomArray flattened(tags);
+
+    for (size_t i = 0; i < flattened.GetSize(); i++)
+    {
+      const DicomElement& element = flattened.GetElement(i);
+      const DicomTag& tag = element.GetTag();
+      const DicomValue& value = element.GetValue();
+      if (!value.IsNull() && 
+          !value.IsBinary())
+      {
+        target.AddMainDicomTag(resource, tag, element.GetValue().GetContent());
+      }
+    }
+  }
+
+
+  static void StoreIdentifiers(ResourcesContent& target,
+                               int64_t resource,
+                               ResourceType level,
+                               const DicomMap& map)
+  {
+    const DicomTag* tags;
+    size_t size;
+
+    ServerToolbox::LoadIdentifiers(tags, size, level);
+
+    for (size_t i = 0; i < size; i++)
+    {
+      // The identifiers tags are a subset of the main DICOM tags
+      assert(DicomMap::IsMainDicomTag(tags[i]));
+        
+      const DicomValue* value = map.TestAndGetValue(tags[i]);
+      if (value != NULL &&
+          !value->IsNull() &&
+          !value->IsBinary())
+      {
+        std::string s = ServerToolbox::NormalizeIdentifier(value->GetContent());
+        target.AddIdentifierTag(resource, tags[i], s);
+      }
+    }
+  }
+
+
+  void ResourcesContent::AddMetadata(int64_t resourceId,
+                                     MetadataType metadata,
+                                     const std::string& value)
+  {
+    if (isNewResource_)
+    {
+      metadata_.push_back(Metadata(resourceId, metadata, value));
+    }
+    else
+    {
+      // This would require to handle the incrementation of revision
+      // numbers in the database backend => only allow setting
+      // metadata on new resources
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
+
+
+  void ResourcesContent::AddResource(int64_t resource,
+                                     ResourceType level,
+                                     const DicomMap& dicomSummary)
+  {
+    StoreIdentifiers(*this, resource, level, dicomSummary);
+
+    DicomMap tags;
+
+    switch (level)
+    {
+      case ResourceType_Patient:
+        dicomSummary.ExtractPatientInformation(tags);
+        break;
+
+      case ResourceType_Study:
+        // Duplicate the patient tags at the study level (new in Orthanc 0.9.5 - db v6)
+        dicomSummary.ExtractPatientInformation(tags);
+        StoreMainDicomTagsInternal(*this, resource, tags);
+
+        dicomSummary.ExtractStudyInformation(tags);
+        break;
+
+      case ResourceType_Series:
+        dicomSummary.ExtractSeriesInformation(tags);
+        break;
+
+      case ResourceType_Instance:
+        dicomSummary.ExtractInstanceInformation(tags);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+    StoreMainDicomTagsInternal(*this, resource, tags);
+  }
+
+
   void ResourcesContent::Store(Compatibility::ISetResourcesContent& compatibility) const
   {
     for (std::list<TagValue>::const_iterator
@@ -59,7 +166,8 @@
     for (std::list<Metadata>::const_iterator
            it = metadata_.begin(); it != metadata_.end(); ++it)
     {
-      compatibility.SetMetadata(it->resourceId_, it->metadata_,  it->value_);
+      assert(isNewResource_);
+      compatibility.SetMetadata(it->resourceId_, it->metadata_,  it->value_, 0 /* initial revision number */);
     }
   }
 }
--- a/OrthancServer/Sources/Database/ResourcesContent.h	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancServer/Sources/Database/ResourcesContent.h	Tue Apr 20 18:11:29 2021 +0200
@@ -89,10 +89,16 @@
     typedef std::list<Metadata>  ListMetadata;
     
   private:
+    bool           isNewResource_;
     ListTags       tags_;
     ListMetadata   metadata_;
 
   public:
+    explicit ResourcesContent(bool isNewResource) :
+      isNewResource_(isNewResource)
+    {
+    }
+    
     void AddMainDicomTag(int64_t resourceId,
                          const DicomTag& tag,
                          const std::string& value)
@@ -109,10 +115,7 @@
 
     void AddMetadata(int64_t resourceId,
                      MetadataType metadata,
-                     const std::string& value)
-    {
-      metadata_.push_back(Metadata(resourceId, metadata, value));
-    }
+                     const std::string& value);
 
     void AddResource(int64_t resource,
                      ResourceType level,
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp	Tue Apr 20 18:11:29 2021 +0200
@@ -39,6 +39,11 @@
 #include "../../../OrthancFramework/Sources/SQLite/Transaction.h"
 #include "../Search/ISqlLookupFormatter.h"
 #include "../ServerToolbox.h"
+#include "Compatibility/ICreateInstance.h"
+#include "Compatibility/IGetChildrenMetadata.h"
+#include "Compatibility/ILookupResourceAndParent.h"
+#include "Compatibility/ISetResourcesContent.h"
+#include "VoidDatabaseListener.h"
 
 #include <OrthancServerResources.h>
 
@@ -46,31 +51,1065 @@
 #include <boost/lexical_cast.hpp>
 
 namespace Orthanc
-{
-  namespace Internals
+{  
+  class SQLiteDatabaseWrapper::LookupFormatter : public ISqlLookupFormatter
+  {
+  private:
+    std::list<std::string>  values_;
+
+  public:
+    virtual std::string GenerateParameter(const std::string& value) ORTHANC_OVERRIDE
+    {
+      values_.push_back(value);
+      return "?";
+    }
+    
+    virtual std::string FormatResourceType(ResourceType level) ORTHANC_OVERRIDE
+    {
+      return boost::lexical_cast<std::string>(level);
+    }
+
+    virtual std::string FormatWildcardEscape() ORTHANC_OVERRIDE
+    {
+      return "ESCAPE '\\'";
+    }
+
+    void Bind(SQLite::Statement& statement) const
+    {
+      size_t pos = 0;
+      
+      for (std::list<std::string>::const_iterator
+             it = values_.begin(); it != values_.end(); ++it, pos++)
+      {
+        statement.BindString(pos, *it);
+      }
+    }
+  };
+
+  
+  class SQLiteDatabaseWrapper::SignalRemainingAncestor : public SQLite::IScalarFunction
+  {
+  private:
+    bool hasRemainingAncestor_;
+    std::string remainingPublicId_;
+    ResourceType remainingType_;
+
+  public:
+    SignalRemainingAncestor() : 
+      hasRemainingAncestor_(false)
+    {
+    }
+
+    void Reset()
+    {
+      hasRemainingAncestor_ = false;
+    }
+
+    virtual const char* GetName() const ORTHANC_OVERRIDE
+    {
+      return "SignalRemainingAncestor";
+    }
+
+    virtual unsigned int GetCardinality() const ORTHANC_OVERRIDE
+    {
+      return 2;
+    }
+
+    virtual void Compute(SQLite::FunctionContext& context) ORTHANC_OVERRIDE
+    {
+      CLOG(TRACE, SQLITE) << "There exists a remaining ancestor with public ID \""
+                          << context.GetStringValue(0) << "\" of type "
+                          << context.GetIntValue(1);
+
+      if (!hasRemainingAncestor_ ||
+          remainingType_ >= context.GetIntValue(1))
+      {
+        hasRemainingAncestor_ = true;
+        remainingPublicId_ = context.GetStringValue(0);
+        remainingType_ = static_cast<ResourceType>(context.GetIntValue(1));
+      }
+    }
+
+    bool HasRemainingAncestor() const
+    {
+      return hasRemainingAncestor_;
+    }
+
+    const std::string& GetRemainingAncestorId() const
+    {
+      assert(hasRemainingAncestor_);
+      return remainingPublicId_;
+    }
+
+    ResourceType GetRemainingAncestorType() const
+    {
+      assert(hasRemainingAncestor_);
+      return remainingType_;
+    }
+  };
+
+
+  class SQLiteDatabaseWrapper::TransactionBase :
+    public SQLiteDatabaseWrapper::UnitTestsTransaction,
+    public Compatibility::ICreateInstance,
+    public Compatibility::IGetChildrenMetadata,
+    public Compatibility::ILookupResourceAndParent,
+    public Compatibility::ISetResourcesContent
   {
-    class SignalFileDeleted : public SQLite::IScalarFunction
+  private:
+    void AnswerLookup(std::list<std::string>& resourcesId,
+                      std::list<std::string>& instancesId,
+                      ResourceType level)
+    {
+      resourcesId.clear();
+      instancesId.clear();
+    
+      std::unique_ptr<SQLite::Statement> statement;
+    
+      switch (level)
+      {
+        case ResourceType_Patient:
+        {
+          statement.reset(
+            new SQLite::Statement(
+              db_, SQLITE_FROM_HERE,
+              "SELECT patients.publicId, instances.publicID FROM Lookup AS patients "
+              "INNER JOIN Resources studies ON patients.internalId=studies.parentId "
+              "INNER JOIN Resources series ON studies.internalId=series.parentId "
+              "INNER JOIN Resources instances ON series.internalId=instances.parentId "
+              "GROUP BY patients.publicId"));
+      
+          break;
+        }
+
+        case ResourceType_Study:
+        {
+          statement.reset(
+            new SQLite::Statement(
+              db_, SQLITE_FROM_HERE,
+              "SELECT studies.publicId, instances.publicID FROM Lookup AS studies "
+              "INNER JOIN Resources series ON studies.internalId=series.parentId "
+              "INNER JOIN Resources instances ON series.internalId=instances.parentId "
+              "GROUP BY studies.publicId"));
+      
+          break;
+        }
+
+        case ResourceType_Series:
+        {
+          statement.reset(
+            new SQLite::Statement(
+              db_, SQLITE_FROM_HERE,
+              "SELECT series.publicId, instances.publicID FROM Lookup AS series "
+              "INNER JOIN Resources instances ON series.internalId=instances.parentId "
+              "GROUP BY series.publicId"));
+      
+          break;
+        }
+
+        case ResourceType_Instance:
+        {
+          statement.reset(
+            new SQLite::Statement(
+              db_, SQLITE_FROM_HERE, "SELECT publicId, publicId FROM Lookup"));
+        
+          break;
+        }
+      
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+
+      assert(statement.get() != NULL);
+      
+      while (statement->Step())
+      {
+        resourcesId.push_back(statement->ColumnString(0));
+        instancesId.push_back(statement->ColumnString(1));
+      }
+    }
+
+
+    void ClearTable(const std::string& tableName)
+    {
+      db_.Execute("DELETE FROM " + tableName);    
+    }
+
+
+    void GetChangesInternal(std::list<ServerIndexChange>& target,
+                            bool& done,
+                            SQLite::Statement& s,
+                            uint32_t maxResults)
+    {
+      target.clear();
+
+      while (target.size() < maxResults && s.Step())
+      {
+        int64_t seq = s.ColumnInt64(0);
+        ChangeType changeType = static_cast<ChangeType>(s.ColumnInt(1));
+        ResourceType resourceType = static_cast<ResourceType>(s.ColumnInt(3));
+        const std::string& date = s.ColumnString(4);
+
+        int64_t internalId = s.ColumnInt64(2);
+        std::string publicId = GetPublicId(internalId);
+
+        target.push_back(ServerIndexChange(seq, changeType, resourceType, publicId, date));
+      }
+
+      done = !(target.size() == maxResults && s.Step());
+    }
+
+
+    void GetExportedResourcesInternal(std::list<ExportedResource>& target,
+                                      bool& done,
+                                      SQLite::Statement& s,
+                                      uint32_t maxResults)
     {
-    private:
-      IDatabaseListener& listener_;
+      target.clear();
+
+      while (target.size() < maxResults && s.Step())
+      {
+        int64_t seq = s.ColumnInt64(0);
+        ResourceType resourceType = static_cast<ResourceType>(s.ColumnInt(1));
+        std::string publicId = s.ColumnString(2);
+
+        ExportedResource resource(seq, 
+                                  resourceType,
+                                  publicId,
+                                  s.ColumnString(3),  // modality
+                                  s.ColumnString(8),  // date
+                                  s.ColumnString(4),  // patient ID
+                                  s.ColumnString(5),  // study instance UID
+                                  s.ColumnString(6),  // series instance UID
+                                  s.ColumnString(7)); // sop instance UID
+
+        target.push_back(resource);
+      }
+
+      done = !(target.size() == maxResults && s.Step());
+    }
+
+
+    void GetChildren(std::list<std::string>& childrenPublicIds,
+                     int64_t id)
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Resources WHERE parentId=?");
+      s.BindInt64(0, id);
+
+      childrenPublicIds.clear();
+      while (s.Step())
+      {
+        childrenPublicIds.push_back(s.ColumnString(0));
+      }
+    }
+
+    boost::mutex::scoped_lock  lock_;
+    IDatabaseListener&         listener_;
+    SignalRemainingAncestor&   signalRemainingAncestor_;
+
+  public:
+    TransactionBase(boost::mutex& mutex,
+                    SQLite::Connection& db,
+                    IDatabaseListener& listener,
+                    SignalRemainingAncestor& signalRemainingAncestor) :
+      UnitTestsTransaction(db),
+      lock_(mutex),
+      listener_(listener),
+      signalRemainingAncestor_(signalRemainingAncestor)
+    {
+    }
+
+    IDatabaseListener& GetListener() const
+    {
+      return listener_;
+    }
+
+    
+    virtual void AddAttachment(int64_t id,
+                               const FileInfo& attachment,
+                               int64_t revision) ORTHANC_OVERRIDE
+    {
+      // TODO - REVISIONS
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO AttachedFiles VALUES(?, ?, ?, ?, ?, ?, ?, ?)");
+      s.BindInt64(0, id);
+      s.BindInt(1, attachment.GetContentType());
+      s.BindString(2, attachment.GetUuid());
+      s.BindInt64(3, attachment.GetCompressedSize());
+      s.BindInt64(4, attachment.GetUncompressedSize());
+      s.BindInt(5, attachment.GetCompressionType());
+      s.BindString(6, attachment.GetUncompressedMD5());
+      s.BindString(7, attachment.GetCompressedMD5());
+      s.Run();
+    }
+
+
+    virtual void ApplyLookupResources(std::list<std::string>& resourcesId,
+                                      std::list<std::string>* instancesId,
+                                      const std::vector<DatabaseConstraint>& lookup,
+                                      ResourceType queryLevel,
+                                      size_t limit) ORTHANC_OVERRIDE
+    {
+      LookupFormatter formatter;
+
+      std::string sql;
+      LookupFormatter::Apply(sql, formatter, lookup, queryLevel, limit);
+
+      sql = "CREATE TEMPORARY TABLE Lookup AS " + sql;
+    
+      {
+        SQLite::Statement s(db_, SQLITE_FROM_HERE, "DROP TABLE IF EXISTS Lookup");
+        s.Run();
+      }
+
+      {
+        SQLite::Statement statement(db_, sql);
+        formatter.Bind(statement);
+        statement.Run();
+      }
+
+      if (instancesId != NULL)
+      {
+        AnswerLookup(resourcesId, *instancesId, queryLevel);
+      }
+      else
+      {
+        resourcesId.clear();
+    
+        SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Lookup");
+        
+        while (s.Step())
+        {
+          resourcesId.push_back(s.ColumnString(0));
+        }
+      }
+    }
+
 
-    public:
-      SignalFileDeleted(IDatabaseListener& listener) :
-        listener_(listener)
+    // From the "ICreateInstance" interface
+    virtual void AttachChild(int64_t parent,
+                             int64_t child) ORTHANC_OVERRIDE
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "UPDATE Resources SET parentId = ? WHERE internalId = ?");
+      s.BindInt64(0, parent);
+      s.BindInt64(1, child);
+      s.Run();
+    }
+
+
+    virtual void ClearChanges() ORTHANC_OVERRIDE
+    {
+      ClearTable("Changes");
+    }
+
+    virtual void ClearExportedResources() ORTHANC_OVERRIDE
+    {
+      ClearTable("ExportedResources");
+    }
+
+
+    virtual void ClearMainDicomTags(int64_t id) ORTHANC_OVERRIDE
+    {
+      {
+        SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM DicomIdentifiers WHERE id=?");
+        s.BindInt64(0, id);
+        s.Run();
+      }
+
       {
+        SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM MainDicomTags WHERE id=?");
+        s.BindInt64(0, id);
+        s.Run();
+      }
+    }
+
+
+    virtual bool CreateInstance(CreateInstanceResult& result,
+                                int64_t& instanceId,
+                                const std::string& patient,
+                                const std::string& study,
+                                const std::string& series,
+                                const std::string& instance) ORTHANC_OVERRIDE
+    {
+      return ICreateInstance::Apply
+        (*this, result, instanceId, patient, study, series, instance);
+    }
+
+
+    // From the "ICreateInstance" interface
+    virtual int64_t CreateResource(const std::string& publicId,
+                                   ResourceType type) ORTHANC_OVERRIDE
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Resources VALUES(NULL, ?, ?, NULL)");
+      s.BindInt(0, type);
+      s.BindString(1, publicId);
+      s.Run();
+      return db_.GetLastInsertRowId();
+    }
+
+
+    virtual void DeleteAttachment(int64_t id,
+                                  FileContentType attachment) ORTHANC_OVERRIDE
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM AttachedFiles WHERE id=? AND fileType=?");
+      s.BindInt64(0, id);
+      s.BindInt(1, attachment);
+      s.Run();
+    }
+
+
+    virtual void DeleteMetadata(int64_t id,
+                                MetadataType type) ORTHANC_OVERRIDE
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM Metadata WHERE id=? and type=?");
+      s.BindInt64(0, id);
+      s.BindInt(1, type);
+      s.Run();
+    }
+
+
+    virtual void DeleteResource(int64_t id) ORTHANC_OVERRIDE
+    {
+      signalRemainingAncestor_.Reset();
+
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM Resources WHERE internalId=?");
+      s.BindInt64(0, id);
+      s.Run();
+
+      if (signalRemainingAncestor_.HasRemainingAncestor())
+      {
+        listener_.SignalRemainingAncestor(signalRemainingAncestor_.GetRemainingAncestorType(),
+                                          signalRemainingAncestor_.GetRemainingAncestorId());
+      }
+    }
+
+
+    virtual void GetAllMetadata(std::map<MetadataType, std::string>& target,
+                                int64_t id) ORTHANC_OVERRIDE
+    {
+      target.clear();
+
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT type, value FROM Metadata WHERE id=?");
+      s.BindInt64(0, id);
+
+      while (s.Step())
+      {
+        MetadataType key = static_cast<MetadataType>(s.ColumnInt(0));
+        target[key] = s.ColumnString(1);
+      }
+    }
+
+
+    virtual void GetAllPublicIds(std::list<std::string>& target,
+                                 ResourceType resourceType) ORTHANC_OVERRIDE
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Resources WHERE resourceType=?");
+      s.BindInt(0, resourceType);
+
+      target.clear();
+      while (s.Step())
+      {
+        target.push_back(s.ColumnString(0));
+      }
+    }
+
+
+    virtual void GetAllPublicIds(std::list<std::string>& target,
+                                 ResourceType resourceType,
+                                 size_t since,
+                                 size_t limit) ORTHANC_OVERRIDE
+    {
+      if (limit == 0)
+      {
+        target.clear();
+        return;
       }
 
-      virtual const char* GetName() const
+      SQLite::Statement s(db_, SQLITE_FROM_HERE,
+                          "SELECT publicId FROM Resources WHERE "
+                          "resourceType=? LIMIT ? OFFSET ?");
+      s.BindInt(0, resourceType);
+      s.BindInt64(1, limit);
+      s.BindInt64(2, since);
+
+      target.clear();
+      while (s.Step())
+      {
+        target.push_back(s.ColumnString(0));
+      }
+    }
+
+
+    virtual void GetChanges(std::list<ServerIndexChange>& target /*out*/,
+                            bool& done /*out*/,
+                            int64_t since,
+                            uint32_t maxResults) ORTHANC_OVERRIDE
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes WHERE seq>? ORDER BY seq LIMIT ?");
+      s.BindInt64(0, since);
+      s.BindInt(1, maxResults + 1);
+      GetChangesInternal(target, done, s, maxResults);
+    }
+
+
+    virtual void GetChildrenMetadata(std::list<std::string>& target,
+                                     int64_t resourceId,
+                                     MetadataType metadata) ORTHANC_OVERRIDE
+    {
+      IGetChildrenMetadata::Apply(*this, target, resourceId, metadata);
+    }
+
+
+    virtual void GetChildrenInternalId(std::list<int64_t>& target,
+                                       int64_t id) ORTHANC_OVERRIDE
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.internalId FROM Resources AS a, Resources AS b  "
+                          "WHERE a.parentId = b.internalId AND b.internalId = ?");     
+      s.BindInt64(0, id);
+
+      target.clear();
+
+      while (s.Step())
+      {
+        target.push_back(s.ColumnInt64(0));
+      }
+    }
+
+
+    virtual void GetChildrenPublicId(std::list<std::string>& target,
+                                     int64_t id) ORTHANC_OVERRIDE
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.publicId FROM Resources AS a, Resources AS b  "
+                          "WHERE a.parentId = b.internalId AND b.internalId = ?");     
+      s.BindInt64(0, id);
+
+      target.clear();
+
+      while (s.Step())
+      {
+        target.push_back(s.ColumnString(0));
+      }
+    }
+
+
+    virtual void GetExportedResources(std::list<ExportedResource>& target,
+                                      bool& done,
+                                      int64_t since,
+                                      uint32_t maxResults) ORTHANC_OVERRIDE
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                          "SELECT * FROM ExportedResources WHERE seq>? ORDER BY seq LIMIT ?");
+      s.BindInt64(0, since);
+      s.BindInt(1, maxResults + 1);
+      GetExportedResourcesInternal(target, done, s, maxResults);
+    }
+
+
+    virtual void GetLastChange(std::list<ServerIndexChange>& target /*out*/) ORTHANC_OVERRIDE
+    {
+      bool done;  // Ignored
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes ORDER BY seq DESC LIMIT 1");
+      GetChangesInternal(target, done, s, 1);
+    }
+
+
+    int64_t GetLastChangeIndex() ORTHANC_OVERRIDE
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                          "SELECT seq FROM sqlite_sequence WHERE name='Changes'");
+
+      if (s.Step())
+      {
+        int64_t c = s.ColumnInt(0);
+        assert(!s.Step());
+        return c;
+      }
+      else
+      {
+        // No change has been recorded so far in the database
+        return 0;
+      }
+    }
+
+    
+    virtual void GetLastExportedResource(std::list<ExportedResource>& target) ORTHANC_OVERRIDE
+    {
+      bool done;  // Ignored
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                          "SELECT * FROM ExportedResources ORDER BY seq DESC LIMIT 1");
+      GetExportedResourcesInternal(target, done, s, 1);
+    }
+
+
+    virtual void GetMainDicomTags(DicomMap& map,
+                                  int64_t id) ORTHANC_OVERRIDE
+    {
+      map.Clear();
+
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM MainDicomTags WHERE id=?");
+      s.BindInt64(0, id);
+      while (s.Step())
+      {
+        map.SetValue(s.ColumnInt(1),
+                     s.ColumnInt(2),
+                     s.ColumnString(3), false);
+      }
+    }
+
+
+    virtual std::string GetPublicId(int64_t resourceId) ORTHANC_OVERRIDE
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                          "SELECT publicId FROM Resources WHERE internalId=?");
+      s.BindInt64(0, resourceId);
+    
+      if (s.Step())
+      { 
+        return s.ColumnString(0);
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_UnknownResource);
+      }
+    }
+
+
+    virtual uint64_t GetResourcesCount(ResourceType resourceType) ORTHANC_OVERRIDE
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                          "SELECT COUNT(*) FROM Resources WHERE resourceType=?");
+      s.BindInt(0, resourceType);
+    
+      if (!s.Step())
+      {
+        return 0;
+      }
+      else
+      {
+        int64_t c = s.ColumnInt(0);
+        assert(!s.Step());
+        return c;
+      }
+    }
+
+
+    virtual ResourceType GetResourceType(int64_t resourceId) ORTHANC_OVERRIDE
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                          "SELECT resourceType FROM Resources WHERE internalId=?");
+      s.BindInt64(0, resourceId);
+    
+      if (s.Step())
       {
-        return "SignalFileDeleted";
+        return static_cast<ResourceType>(s.ColumnInt(0));
+      }
+      else
+      { 
+        throw OrthancException(ErrorCode_UnknownResource);
+      }
+    }
+
+
+    virtual uint64_t GetTotalCompressedSize() ORTHANC_OVERRIDE
+    {
+      // Old SQL query that was used in Orthanc <= 1.5.0:
+      // SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(compressedSize) FROM AttachedFiles");
+
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT value FROM GlobalIntegers WHERE key=0");
+      s.Run();
+      return static_cast<uint64_t>(s.ColumnInt64(0));
+    }
+
+    
+    virtual uint64_t GetTotalUncompressedSize() ORTHANC_OVERRIDE
+    {
+      // Old SQL query that was used in Orthanc <= 1.5.0:
+      // SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(uncompressedSize) FROM AttachedFiles");
+
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT value FROM GlobalIntegers WHERE key=1");
+      s.Run();
+      return static_cast<uint64_t>(s.ColumnInt64(0));
+    }
+
+
+    virtual bool IsDiskSizeAbove(uint64_t threshold) ORTHANC_OVERRIDE
+    {
+      return GetTotalCompressedSize() > threshold;
+    }
+
+
+    virtual bool IsExistingResource(int64_t internalId) ORTHANC_OVERRIDE
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                          "SELECT * FROM Resources WHERE internalId=?");
+      s.BindInt64(0, internalId);
+      return s.Step();
+    }
+
+
+    virtual bool IsProtectedPatient(int64_t internalId) ORTHANC_OVERRIDE
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE,
+                          "SELECT * FROM PatientRecyclingOrder WHERE patientId = ?");
+      s.BindInt64(0, internalId);
+      return !s.Step();
+    }
+
+
+    virtual void ListAvailableAttachments(std::set<FileContentType>& target,
+                                          int64_t id) ORTHANC_OVERRIDE
+    {
+      target.clear();
+
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                          "SELECT fileType FROM AttachedFiles WHERE id=?");
+      s.BindInt64(0, id);
+
+      while (s.Step())
+      {
+        target.insert(static_cast<FileContentType>(s.ColumnInt(0)));
+      }
+    }
+
+
+    virtual void LogChange(int64_t internalId,
+                           const ServerIndexChange& change) ORTHANC_OVERRIDE
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Changes VALUES(NULL, ?, ?, ?, ?)");
+      s.BindInt(0, change.GetChangeType());
+      s.BindInt64(1, internalId);
+      s.BindInt(2, change.GetResourceType());
+      s.BindString(3, change.GetDate());
+      s.Run();
+    }
+
+
+    virtual void LogExportedResource(const ExportedResource& resource) ORTHANC_OVERRIDE
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                          "INSERT INTO ExportedResources VALUES(NULL, ?, ?, ?, ?, ?, ?, ?, ?)");
+
+      s.BindInt(0, resource.GetResourceType());
+      s.BindString(1, resource.GetPublicId());
+      s.BindString(2, resource.GetModality());
+      s.BindString(3, resource.GetPatientId());
+      s.BindString(4, resource.GetStudyInstanceUid());
+      s.BindString(5, resource.GetSeriesInstanceUid());
+      s.BindString(6, resource.GetSopInstanceUid());
+      s.BindString(7, resource.GetDate());
+      s.Run();      
+    }
+
+
+    virtual bool LookupAttachment(FileInfo& attachment,
+                                  int64_t& revision,
+                                  int64_t id,
+                                  FileContentType contentType) ORTHANC_OVERRIDE
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                          "SELECT uuid, uncompressedSize, compressionType, compressedSize, "
+                          "uncompressedMD5, compressedMD5 FROM AttachedFiles WHERE id=? AND fileType=?");
+      s.BindInt64(0, id);
+      s.BindInt(1, contentType);
+
+      if (!s.Step())
+      {
+        return false;
+      }
+      else
+      {
+        attachment = FileInfo(s.ColumnString(0),
+                              contentType,
+                              s.ColumnInt64(1),
+                              s.ColumnString(4),
+                              static_cast<CompressionType>(s.ColumnInt(2)),
+                              s.ColumnInt64(3),
+                              s.ColumnString(5));
+        revision = 0;   // TODO - REVISIONS
+        return true;
+      }
+    }
+
+
+    virtual bool LookupGlobalProperty(std::string& target,
+                                      GlobalProperty property,
+                                      bool shared) ORTHANC_OVERRIDE
+    {
+      // The "shared" info is not used by the SQLite database, as it
+      // can only be used by one Orthanc server.
+      
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                          "SELECT value FROM GlobalProperties WHERE property=?");
+      s.BindInt(0, property);
+
+      if (!s.Step())
+      {
+        return false;
+      }
+      else
+      {
+        target = s.ColumnString(0);
+        return true;
+      }
+    }
+
+
+    virtual bool LookupMetadata(std::string& target,
+                                int64_t& revision,
+                                int64_t id,
+                                MetadataType type) ORTHANC_OVERRIDE
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                          "SELECT value FROM Metadata WHERE id=? AND type=?");
+      s.BindInt64(0, id);
+      s.BindInt(1, type);
+
+      if (!s.Step())
+      {
+        return false;
+      }
+      else
+      {
+        target = s.ColumnString(0);
+        revision = 0;   // TODO - REVISIONS
+        return true;
+      }
+    }
+
+
+    virtual bool LookupParent(int64_t& parentId,
+                              int64_t resourceId) ORTHANC_OVERRIDE
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                          "SELECT parentId FROM Resources WHERE internalId=?");
+      s.BindInt64(0, resourceId);
+
+      if (!s.Step())
+      {
+        throw OrthancException(ErrorCode_UnknownResource);
       }
 
-      virtual unsigned int GetCardinality() const
+      if (s.ColumnIsNull(0))
+      {
+        return false;
+      }
+      else
+      {
+        parentId = s.ColumnInt(0);
+        return true;
+      }
+    }
+
+
+    virtual bool LookupResourceAndParent(int64_t& id,
+                                         ResourceType& type,
+                                         std::string& parentPublicId,
+                                         const std::string& publicId) ORTHANC_OVERRIDE
+    {
+      return ILookupResourceAndParent::Apply(*this, id, type, parentPublicId, publicId);
+    }
+
+
+    virtual bool LookupResource(int64_t& id,
+                                ResourceType& type,
+                                const std::string& publicId) ORTHANC_OVERRIDE
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                          "SELECT internalId, resourceType FROM Resources WHERE publicId=?");
+      s.BindString(0, publicId);
+
+      if (!s.Step())
+      {
+        return false;
+      }
+      else
+      {
+        id = s.ColumnInt(0);
+        type = static_cast<ResourceType>(s.ColumnInt(1));
+
+        // Check whether there is a single resource with this public id
+        assert(!s.Step());
+
+        return true;
+      }
+    }
+
+
+    virtual bool SelectPatientToRecycle(int64_t& internalId) ORTHANC_OVERRIDE
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE,
+                          "SELECT patientId FROM PatientRecyclingOrder ORDER BY seq ASC LIMIT 1");
+   
+      if (!s.Step())
+      {
+        // No patient remaining or all the patients are protected
+        return false;
+      }
+      else
+      {
+        internalId = s.ColumnInt(0);
+        return true;
+      }    
+    }
+
+
+    virtual bool SelectPatientToRecycle(int64_t& internalId,
+                                        int64_t patientIdToAvoid) ORTHANC_OVERRIDE
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE,
+                          "SELECT patientId FROM PatientRecyclingOrder "
+                          "WHERE patientId != ? ORDER BY seq ASC LIMIT 1");
+      s.BindInt64(0, patientIdToAvoid);
+
+      if (!s.Step())
+      {
+        // No patient remaining or all the patients are protected
+        return false;
+      }
+      else
       {
-        return 7;
+        internalId = s.ColumnInt(0);
+        return true;
+      }   
+    }
+
+
+    virtual void SetGlobalProperty(GlobalProperty property,
+                                   bool shared,
+                                   const std::string& value) ORTHANC_OVERRIDE
+    {
+      // The "shared" info is not used by the SQLite database, as it
+      // can only be used by one Orthanc server.
+      
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO GlobalProperties VALUES(?, ?)");
+      s.BindInt(0, property);
+      s.BindString(1, value);
+      s.Run();
+    }
+
+
+    // From the "ISetResourcesContent" interface
+    virtual void SetIdentifierTag(int64_t id,
+                                  const DicomTag& tag,
+                                  const std::string& value) ORTHANC_OVERRIDE
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO DicomIdentifiers VALUES(?, ?, ?, ?)");
+      s.BindInt64(0, id);
+      s.BindInt(1, tag.GetGroup());
+      s.BindInt(2, tag.GetElement());
+      s.BindString(3, value);
+      s.Run();
+    }
+
+
+    virtual void SetProtectedPatient(int64_t internalId, 
+                                     bool isProtected) ORTHANC_OVERRIDE
+    {
+      if (isProtected)
+      {
+        SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM PatientRecyclingOrder WHERE patientId=?");
+        s.BindInt64(0, internalId);
+        s.Run();
+      }
+      else if (IsProtectedPatient(internalId))
+      {
+        SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO PatientRecyclingOrder VALUES(NULL, ?)");
+        s.BindInt64(0, internalId);
+        s.Run();
+      }
+      else
+      {
+        // Nothing to do: The patient is already unprotected
+      }
+    }
+
+
+    // From the "ISetResourcesContent" interface
+    virtual void SetMainDicomTag(int64_t id,
+                                 const DicomTag& tag,
+                                 const std::string& value) ORTHANC_OVERRIDE
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO MainDicomTags VALUES(?, ?, ?, ?)");
+      s.BindInt64(0, id);
+      s.BindInt(1, tag.GetGroup());
+      s.BindInt(2, tag.GetElement());
+      s.BindString(3, value);
+      s.Run();
+    }
+
+
+    virtual void SetMetadata(int64_t id,
+                             MetadataType type,
+                             const std::string& value,
+                             int64_t revision) ORTHANC_OVERRIDE
+    {
+      // TODO - REVISIONS
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO Metadata VALUES(?, ?, ?)");
+      s.BindInt64(0, id);
+      s.BindInt(1, type);
+      s.BindString(2, value);
+      s.Run();
+    }
+
+
+    virtual void SetResourcesContent(const Orthanc::ResourcesContent& content) ORTHANC_OVERRIDE
+    {
+      ISetResourcesContent::Apply(*this, content);
+    }
+
+
+    // From the "ICreateInstance" interface
+    virtual void TagMostRecentPatient(int64_t patient) ORTHANC_OVERRIDE
+    {
+      {
+        SQLite::Statement s(db_, SQLITE_FROM_HERE,
+                            "DELETE FROM PatientRecyclingOrder WHERE patientId=?");
+        s.BindInt64(0, patient);
+        s.Run();
+
+        assert(db_.GetLastChangeCount() == 0 ||
+               db_.GetLastChangeCount() == 1);
+      
+        if (db_.GetLastChangeCount() == 0)
+        {
+          // The patient was protected, there was nothing to delete from the recycling order
+          return;
+        }
       }
 
-      virtual void Compute(SQLite::FunctionContext& context)
+      {
+        SQLite::Statement s(db_, SQLITE_FROM_HERE,
+                            "INSERT INTO PatientRecyclingOrder VALUES(NULL, ?)");
+        s.BindInt64(0, patient);
+        s.Run();
+      }
+    }
+  };
+
+
+  class SQLiteDatabaseWrapper::SignalFileDeleted : public SQLite::IScalarFunction
+  {
+  private:
+    SQLiteDatabaseWrapper& sqlite_;
+
+  public:
+    SignalFileDeleted(SQLiteDatabaseWrapper& sqlite) :
+      sqlite_(sqlite)
+    {
+    }
+
+    virtual const char* GetName() const ORTHANC_OVERRIDE
+    {
+      return "SignalFileDeleted";
+    }
+
+    virtual unsigned int GetCardinality() const ORTHANC_OVERRIDE
+    {
+      return 7;
+    }
+
+    virtual void Compute(SQLite::FunctionContext& context) ORTHANC_OVERRIDE
+    {
+      if (sqlite_.activeTransaction_ != NULL)
       {
         std::string uncompressedMD5, compressedMD5;
 
@@ -91,244 +1130,143 @@
                       static_cast<CompressionType>(context.GetIntValue(3)),
                       static_cast<uint64_t>(context.GetInt64Value(4)),
                       compressedMD5);
-        
-        listener_.SignalFileDeleted(info);
-      }
-    };
-
-    class SignalResourceDeleted : public SQLite::IScalarFunction
-    {
-    private:
-      IDatabaseListener& listener_;
-
-    public:
-      SignalResourceDeleted(IDatabaseListener& listener) :
-        listener_(listener)
-      {
-      }
-
-      virtual const char* GetName() const
-      {
-        return "SignalResourceDeleted";
-      }
-
-      virtual unsigned int GetCardinality() const
-      {
-        return 2;
-      }
 
-      virtual void Compute(SQLite::FunctionContext& context)
-      {
-        ResourceType type = static_cast<ResourceType>(context.GetIntValue(1));
-        ServerIndexChange change(ChangeType_Deleted, type, context.GetStringValue(0));
-        listener_.SignalChange(change);
+        sqlite_.activeTransaction_->GetListener().SignalAttachmentDeleted(info);
       }
-    };
-
-    class SignalRemainingAncestor : public SQLite::IScalarFunction
-    {
-    private:
-      bool hasRemainingAncestor_;
-      std::string remainingPublicId_;
-      ResourceType remainingType_;
+    }
+  };
+    
 
-    public:
-      SignalRemainingAncestor() : 
-        hasRemainingAncestor_(false)
-      {
-      }
-
-      void Reset()
-      {
-        hasRemainingAncestor_ = false;
-      }
-
-      virtual const char* GetName() const
-      {
-        return "SignalRemainingAncestor";
-      }
+  class SQLiteDatabaseWrapper::SignalResourceDeleted : public SQLite::IScalarFunction
+  {
+  private:
+    SQLiteDatabaseWrapper& sqlite_;
 
-      virtual unsigned int GetCardinality() const
-      {
-        return 2;
-      }
-
-      virtual void Compute(SQLite::FunctionContext& context)
-      {
-        CLOG(TRACE, SQLITE) << "There exists a remaining ancestor with public ID \""
-                            << context.GetStringValue(0) << "\" of type "
-                            << context.GetIntValue(1);
-
-        if (!hasRemainingAncestor_ ||
-            remainingType_ >= context.GetIntValue(1))
-        {
-          hasRemainingAncestor_ = true;
-          remainingPublicId_ = context.GetStringValue(0);
-          remainingType_ = static_cast<ResourceType>(context.GetIntValue(1));
-        }
-      }
-
-      bool HasRemainingAncestor() const
-      {
-        return hasRemainingAncestor_;
-      }
+  public:
+    SignalResourceDeleted(SQLiteDatabaseWrapper& sqlite) :
+      sqlite_(sqlite)
+    {
+    }
 
-      const std::string& GetRemainingAncestorId() const
-      {
-        assert(hasRemainingAncestor_);
-        return remainingPublicId_;
-      }
-
-      ResourceType GetRemainingAncestorType() const
-      {
-        assert(hasRemainingAncestor_);
-        return remainingType_;
-      }
-    };
-  }
-
+    virtual const char* GetName() const ORTHANC_OVERRIDE
+    {
+      return "SignalResourceDeleted";
+    }
 
-  void SQLiteDatabaseWrapper::GetChangesInternal(std::list<ServerIndexChange>& target,
-                                                 bool& done,
-                                                 SQLite::Statement& s,
-                                                 uint32_t maxResults)
-  {
-    target.clear();
-
-    while (target.size() < maxResults && s.Step())
+    virtual unsigned int GetCardinality() const ORTHANC_OVERRIDE
     {
-      int64_t seq = s.ColumnInt64(0);
-      ChangeType changeType = static_cast<ChangeType>(s.ColumnInt(1));
-      ResourceType resourceType = static_cast<ResourceType>(s.ColumnInt(3));
-      const std::string& date = s.ColumnString(4);
-
-      int64_t internalId = s.ColumnInt64(2);
-      std::string publicId = GetPublicId(internalId);
-
-      target.push_back(ServerIndexChange(seq, changeType, resourceType, publicId, date));
+      return 2;
     }
 
-    done = !(target.size() == maxResults && s.Step());
-  }
-
-
-  void SQLiteDatabaseWrapper::GetExportedResourcesInternal(std::list<ExportedResource>& target,
-                                                           bool& done,
-                                                           SQLite::Statement& s,
-                                                           uint32_t maxResults)
-  {
-    target.clear();
-
-    while (target.size() < maxResults && s.Step())
+    virtual void Compute(SQLite::FunctionContext& context) ORTHANC_OVERRIDE
     {
-      int64_t seq = s.ColumnInt64(0);
-      ResourceType resourceType = static_cast<ResourceType>(s.ColumnInt(1));
-      std::string publicId = s.ColumnString(2);
+      if (sqlite_.activeTransaction_ != NULL)
+      {
+        sqlite_.activeTransaction_->GetListener().
+          SignalResourceDeleted(static_cast<ResourceType>(context.GetIntValue(1)),
+                                context.GetStringValue(0));
+      }
+    }
+  };
+
+  
+  class SQLiteDatabaseWrapper::ReadWriteTransaction : public SQLiteDatabaseWrapper::TransactionBase
+  {
+  private:
+    SQLiteDatabaseWrapper&                that_;
+    std::unique_ptr<SQLite::Transaction>  transaction_;
+    int64_t                               initialDiskSize_;
 
-      ExportedResource resource(seq, 
-                                resourceType,
-                                publicId,
-                                s.ColumnString(3),  // modality
-                                s.ColumnString(8),  // date
-                                s.ColumnString(4),  // patient ID
-                                s.ColumnString(5),  // study instance UID
-                                s.ColumnString(6),  // series instance UID
-                                s.ColumnString(7)); // sop instance UID
+  public:
+    ReadWriteTransaction(SQLiteDatabaseWrapper& that,
+                         IDatabaseListener& listener) :
+      TransactionBase(that.mutex_, that.db_, listener, *that.signalRemainingAncestor_),
+      that_(that),
+      transaction_(new SQLite::Transaction(that_.db_))
+    {
+      if (that_.activeTransaction_ != NULL)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+      
+      that_.activeTransaction_ = this;
 
-      target.push_back(resource);
+#if defined(NDEBUG)
+      // Release mode
+      initialDiskSize_ = 0;
+#else
+      // Debug mode
+      initialDiskSize_ = static_cast<int64_t>(GetTotalCompressedSize());
+#endif
     }
 
-    done = !(target.size() == maxResults && s.Step());
-  }
+    virtual ~ReadWriteTransaction()
+    {
+      assert(that_.activeTransaction_ != NULL);    
+      that_.activeTransaction_ = NULL;
+    }
 
+    void Begin()
+    {
+      transaction_->Begin();
+    }
 
-  void SQLiteDatabaseWrapper::GetChildren(std::list<std::string>& childrenPublicIds,
-                                          int64_t id)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Resources WHERE parentId=?");
-    s.BindInt64(0, id);
+    virtual void Rollback() ORTHANC_OVERRIDE
+    {
+      transaction_->Rollback();
+    }
 
-    childrenPublicIds.clear();
-    while (s.Step())
+    virtual void Commit(int64_t fileSizeDelta /* only used in debug */) ORTHANC_OVERRIDE
     {
-      childrenPublicIds.push_back(s.ColumnString(0));
+      transaction_->Commit();
+
+      assert(initialDiskSize_ + fileSizeDelta >= 0 &&
+             initialDiskSize_ + fileSizeDelta == static_cast<int64_t>(GetTotalCompressedSize()));
     }
-  }
+  };
 
 
-  void SQLiteDatabaseWrapper::DeleteResource(int64_t id)
+  class SQLiteDatabaseWrapper::ReadOnlyTransaction : public SQLiteDatabaseWrapper::TransactionBase
   {
-    signalRemainingAncestor_->Reset();
-
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM Resources WHERE internalId=?");
-    s.BindInt64(0, id);
-    s.Run();
-
-    if (signalRemainingAncestor_->HasRemainingAncestor() &&
-        listener_ != NULL)
-    {
-      listener_->SignalRemainingAncestor(signalRemainingAncestor_->GetRemainingAncestorType(),
-                                         signalRemainingAncestor_->GetRemainingAncestorId());
-    }
-  }
-
-
-  bool SQLiteDatabaseWrapper::GetParentPublicId(std::string& target,
-                                                int64_t id)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.publicId FROM Resources AS a, Resources AS b "
-                        "WHERE a.internalId = b.parentId AND b.internalId = ?");     
-    s.BindInt64(0, id);
-
-    if (s.Step())
+  private:
+    SQLiteDatabaseWrapper&  that_;
+    
+  public:
+    ReadOnlyTransaction(SQLiteDatabaseWrapper& that,
+                        IDatabaseListener& listener) :
+      TransactionBase(that.mutex_, that.db_, listener, *that.signalRemainingAncestor_),
+      that_(that)
     {
-      target = s.ColumnString(0);
-      return true;
+      if (that_.activeTransaction_ != NULL)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+      
+      that_.activeTransaction_ = this;
     }
-    else
-    {
-      return false;
-    }
-  }
-
 
-  int64_t SQLiteDatabaseWrapper::GetTableRecordCount(const std::string& table)
-  {
-    /**
-     * "Generally one cannot use SQL parameters/placeholders for
-     * database identifiers (tables, columns, views, schemas, etc.) or
-     * database functions (e.g., CURRENT_DATE), but instead only for
-     * binding literal values." => To avoid any SQL injection, we
-     * check that the "table" parameter has only alphabetic
-     * characters.
-     * https://stackoverflow.com/a/1274764/881731
-     **/
-    for (size_t i = 0; i < table.size(); i++)
+    virtual ~ReadOnlyTransaction()
+    {
+      assert(that_.activeTransaction_ != NULL);    
+      that_.activeTransaction_ = NULL;
+    }
+
+    virtual void Rollback() ORTHANC_OVERRIDE
     {
-      if (!isalpha(table[i]))
+    }
+
+    virtual void Commit(int64_t fileSizeDelta /* only used in debug */) ORTHANC_OVERRIDE
+    {
+      if (fileSizeDelta != 0)
       {
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
+        throw OrthancException(ErrorCode_InternalError);
       }
     }
-
-    // Don't use "SQLITE_FROM_HERE", otherwise "table" would be cached
-    SQLite::Statement s(db_, "SELECT COUNT(*) FROM " + table);
+  };
+  
 
-    if (s.Step())
-    {
-      int64_t c = s.ColumnInt(0);
-      assert(!s.Step());
-      return c;
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-  }
-
-    
   SQLiteDatabaseWrapper::SQLiteDatabaseWrapper(const std::string& path) : 
-    listener_(NULL), 
+    activeTransaction_(NULL), 
     signalRemainingAncestor_(NULL),
     version_(0)
   {
@@ -337,57 +1275,54 @@
 
 
   SQLiteDatabaseWrapper::SQLiteDatabaseWrapper() : 
-    listener_(NULL), 
+    activeTransaction_(NULL), 
     signalRemainingAncestor_(NULL),
     version_(0)
   {
     db_.OpenInMemory();
   }
 
-
-  int SQLiteDatabaseWrapper::GetGlobalIntegerProperty(GlobalProperty property,
-                                                      int defaultValue)
+  SQLiteDatabaseWrapper::~SQLiteDatabaseWrapper()
   {
-    std::string tmp;
-
-    if (!LookupGlobalProperty(tmp, GlobalProperty_DatabasePatchLevel))
+    if (activeTransaction_ != NULL)
     {
-      return defaultValue;
-    }
-    else
-    {
-      try
-      {
-        return boost::lexical_cast<int>(tmp);
-      }
-      catch (boost::bad_lexical_cast&)
-      {
-        throw OrthancException(ErrorCode_ParameterOutOfRange,
-                               "Global property " + boost::lexical_cast<std::string>(property) +
-                               " should be an integer, but found: " + tmp);
-      }
+      LOG(ERROR) << "A SQLite transaction is still active in the SQLiteDatabaseWrapper destructor: Expect a crash";
     }
   }
 
 
   void SQLiteDatabaseWrapper::Open()
   {
-    db_.Execute("PRAGMA ENCODING=\"UTF-8\";");
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+
+      if (signalRemainingAncestor_ != NULL)
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);  // Cannot open twice
+      }
+    
+      signalRemainingAncestor_ = dynamic_cast<SignalRemainingAncestor*>(db_.Register(new SignalRemainingAncestor));
+      db_.Register(new SignalFileDeleted(*this));
+      db_.Register(new SignalResourceDeleted(*this));
+    
+      db_.Execute("PRAGMA ENCODING=\"UTF-8\";");
 
-    // Performance tuning of SQLite with PRAGMAs
-    // http://www.sqlite.org/pragma.html
-    db_.Execute("PRAGMA SYNCHRONOUS=NORMAL;");
-    db_.Execute("PRAGMA JOURNAL_MODE=WAL;");
-    db_.Execute("PRAGMA LOCKING_MODE=EXCLUSIVE;");
-    db_.Execute("PRAGMA WAL_AUTOCHECKPOINT=1000;");
-    //db_.Execute("PRAGMA TEMP_STORE=memory");
+      // Performance tuning of SQLite with PRAGMAs
+      // http://www.sqlite.org/pragma.html
+      db_.Execute("PRAGMA SYNCHRONOUS=NORMAL;");
+      db_.Execute("PRAGMA JOURNAL_MODE=WAL;");
+      db_.Execute("PRAGMA LOCKING_MODE=EXCLUSIVE;");
+      db_.Execute("PRAGMA WAL_AUTOCHECKPOINT=1000;");
+      //db_.Execute("PRAGMA TEMP_STORE=memory");
 
-    // Make "LIKE" case-sensitive in SQLite 
-    db_.Execute("PRAGMA case_sensitive_like = true;");
-    
+      // Make "LIKE" case-sensitive in SQLite 
+      db_.Execute("PRAGMA case_sensitive_like = true;");
+    }
+
+    VoidDatabaseListener listener;
+      
     {
-      SQLite::Transaction t(db_);
-      t.Begin();
+      std::unique_ptr<ITransaction> transaction(StartTransaction(TransactionType_ReadOnly, listener));
 
       if (!db_.DoesTableExist("GlobalProperties"))
       {
@@ -399,7 +1334,7 @@
 
       // Check the version of the database
       std::string tmp;
-      if (!LookupGlobalProperty(tmp, GlobalProperty_DatabaseSchemaVersion))
+      if (!transaction->LookupGlobalProperty(tmp, GlobalProperty_DatabaseSchemaVersion, true /* unused in SQLite */))
       {
         tmp = "Unknown";
       }
@@ -424,7 +1359,7 @@
       // New in Orthanc 1.5.1
       if (version_ == 6)
       {
-        if (!LookupGlobalProperty(tmp, GlobalProperty_GetTotalSizeIsFast) ||
+        if (!transaction->LookupGlobalProperty(tmp, GlobalProperty_GetTotalSizeIsFast, true /* unused in SQLite */) ||
             tmp != "1")
         {
           LOG(INFO) << "Installing the SQLite triggers to track the size of the attachments";
@@ -434,14 +1369,18 @@
         }
       }
 
-      t.Commit();
+      transaction->Commit(0);
     }
-
-    signalRemainingAncestor_ = new Internals::SignalRemainingAncestor;
-    db_.Register(signalRemainingAncestor_);
   }
 
 
+  void SQLiteDatabaseWrapper::Close()
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    db_.Close();
+  }
+
+  
   static void ExecuteUpgradeScript(SQLite::Connection& db,
                                    ServerResources::FileResourceId script)
   {
@@ -456,6 +1395,8 @@
   void SQLiteDatabaseWrapper::Upgrade(unsigned int targetVersion,
                                       IStorageArea& storageArea)
   {
+    boost::mutex::scoped_lock lock(mutex_);
+
     if (targetVersion != 6)
     {
       throw OrthancException(ErrorCode_IncompatibleDatabaseVersion);
@@ -491,205 +1432,56 @@
       // No change in the DB schema, the step from version 5 to 6 only
       // consists in reconstructing the main DICOM tags information
       // (as more tags got included).
-      db_.BeginTransaction();
-      ServerToolbox::ReconstructMainDicomTags(*this, storageArea, ResourceType_Patient);
-      ServerToolbox::ReconstructMainDicomTags(*this, storageArea, ResourceType_Study);
-      ServerToolbox::ReconstructMainDicomTags(*this, storageArea, ResourceType_Series);
-      ServerToolbox::ReconstructMainDicomTags(*this, storageArea, ResourceType_Instance);
-      db_.Execute("UPDATE GlobalProperties SET value=\"6\" WHERE property=" +
-                  boost::lexical_cast<std::string>(GlobalProperty_DatabaseSchemaVersion) + ";");
-      db_.CommitTransaction();
+
+      VoidDatabaseListener listener;
+      
+      {
+        std::unique_ptr<ITransaction> transaction(StartTransaction(TransactionType_ReadWrite, listener));
+        ServerToolbox::ReconstructMainDicomTags(*transaction, storageArea, ResourceType_Patient);
+        ServerToolbox::ReconstructMainDicomTags(*transaction, storageArea, ResourceType_Study);
+        ServerToolbox::ReconstructMainDicomTags(*transaction, storageArea, ResourceType_Series);
+        ServerToolbox::ReconstructMainDicomTags(*transaction, storageArea, ResourceType_Instance);
+        db_.Execute("UPDATE GlobalProperties SET value=\"6\" WHERE property=" +
+                    boost::lexical_cast<std::string>(GlobalProperty_DatabaseSchemaVersion) + ";");
+        transaction->Commit(0);
+      }
+      
       version_ = 6;
     }
   }
 
 
-  void SQLiteDatabaseWrapper::SetListener(IDatabaseListener& listener)
-  {
-    listener_ = &listener;
-    db_.Register(new Internals::SignalFileDeleted(listener));
-    db_.Register(new Internals::SignalResourceDeleted(listener));
-  }
-
-
-  void SQLiteDatabaseWrapper::ClearTable(const std::string& tableName)
-  {
-    db_.Execute("DELETE FROM " + tableName);    
-  }
-
-
-  bool SQLiteDatabaseWrapper::LookupParent(int64_t& parentId,
-                                           int64_t resourceId)
+  IDatabaseWrapper::ITransaction* SQLiteDatabaseWrapper::StartTransaction(TransactionType type,
+                                                                          IDatabaseListener& listener)
   {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT parentId FROM Resources WHERE internalId=?");
-    s.BindInt64(0, resourceId);
-
-    if (!s.Step())
+    switch (type)
     {
-      throw OrthancException(ErrorCode_UnknownResource);
-    }
+      case TransactionType_ReadOnly:
+        return new ReadOnlyTransaction(*this, listener);  // This is a no-op transaction in SQLite (thanks to mutex)
 
-    if (s.ColumnIsNull(0))
-    {
-      return false;
-    }
-    else
-    {
-      parentId = s.ColumnInt(0);
-      return true;
+      case TransactionType_ReadWrite:
+      {
+        std::unique_ptr<ReadWriteTransaction> transaction;
+        transaction.reset(new ReadWriteTransaction(*this, listener));
+        transaction->Begin();
+        return transaction.release();
+      }
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
     }
   }
 
-
-  ResourceType SQLiteDatabaseWrapper::GetResourceType(int64_t resourceId)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT resourceType FROM Resources WHERE internalId=?");
-    s.BindInt64(0, resourceId);
-    
-    if (s.Step())
-    {
-      return static_cast<ResourceType>(s.ColumnInt(0));
-    }
-    else
-    { 
-      throw OrthancException(ErrorCode_UnknownResource);
-    }
-  }
-
-
-  std::string SQLiteDatabaseWrapper::GetPublicId(int64_t resourceId)
+  
+  void SQLiteDatabaseWrapper::FlushToDisk()
   {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT publicId FROM Resources WHERE internalId=?");
-    s.BindInt64(0, resourceId);
-    
-    if (s.Step())
-    { 
-      return s.ColumnString(0);
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_UnknownResource);
-    }
-  }
-
-
-  void SQLiteDatabaseWrapper::GetChanges(std::list<ServerIndexChange>& target /*out*/,
-                                         bool& done /*out*/,
-                                         int64_t since,
-                                         uint32_t maxResults)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes WHERE seq>? ORDER BY seq LIMIT ?");
-    s.BindInt64(0, since);
-    s.BindInt(1, maxResults + 1);
-    GetChangesInternal(target, done, s, maxResults);
-  }
-
-
-  void SQLiteDatabaseWrapper::GetLastChange(std::list<ServerIndexChange>& target /*out*/)
-  {
-    bool done;  // Ignored
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes ORDER BY seq DESC LIMIT 1");
-    GetChangesInternal(target, done, s, 1);
+    boost::mutex::scoped_lock lock(mutex_);
+    db_.FlushToDisk();
   }
 
 
-  class SQLiteDatabaseWrapper::Transaction : public IDatabaseWrapper::ITransaction
-  {
-  private:
-    SQLiteDatabaseWrapper&                that_;
-    std::unique_ptr<SQLite::Transaction>  transaction_;
-    int64_t                               initialDiskSize_;
-
-  public:
-    Transaction(SQLiteDatabaseWrapper& that) :
-      that_(that),
-      transaction_(new SQLite::Transaction(that_.db_))
-    {
-#if defined(NDEBUG)
-      // Release mode
-      initialDiskSize_ = 0;
-#else
-      // Debug mode
-      initialDiskSize_ = static_cast<int64_t>(that_.GetTotalCompressedSize());
-#endif
-    }
-
-    virtual void Begin()
-    {
-      transaction_->Begin();
-    }
-
-    virtual void Rollback() 
-    {
-      transaction_->Rollback();
-    }
-
-    virtual void Commit(int64_t fileSizeDelta /* only used in debug */)
-    {
-      transaction_->Commit();
-
-      assert(initialDiskSize_ + fileSizeDelta >= 0 &&
-             initialDiskSize_ + fileSizeDelta == static_cast<int64_t>(that_.GetTotalCompressedSize()));
-    }
-  };
-
-
-  IDatabaseWrapper::ITransaction* SQLiteDatabaseWrapper::StartTransaction()
-  {
-    return new Transaction(*this);
-  }
-
-
-  void SQLiteDatabaseWrapper::GetAllMetadata(std::map<MetadataType, std::string>& target,
-                                             int64_t id)
-  {
-    target.clear();
-
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT type, value FROM Metadata WHERE id=?");
-    s.BindInt64(0, id);
-
-    while (s.Step())
-    {
-      MetadataType key = static_cast<MetadataType>(s.ColumnInt(0));
-      target[key] = s.ColumnString(1);
-    }
-  }
-
-
-  void SQLiteDatabaseWrapper::SetGlobalProperty(GlobalProperty property,
-                                                const std::string& value)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO GlobalProperties VALUES(?, ?)");
-    s.BindInt(0, property);
-    s.BindString(1, value);
-    s.Run();
-  }
-
-
-  bool SQLiteDatabaseWrapper::LookupGlobalProperty(std::string& target,
-                                                   GlobalProperty property)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT value FROM GlobalProperties WHERE property=?");
-    s.BindInt(0, property);
-
-    if (!s.Step())
-    {
-      return false;
-    }
-    else
-    {
-      target = s.ColumnString(0);
-      return true;
-    }
-  }
-
-
-  int64_t SQLiteDatabaseWrapper::CreateResource(const std::string& publicId,
-                                                ResourceType type)
+  int64_t SQLiteDatabaseWrapper::UnitTestsTransaction::CreateResource(const std::string& publicId,
+                                                                      ResourceType type)
   {
     SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Resources VALUES(NULL, ?, ?, NULL)");
     s.BindInt(0, type);
@@ -699,33 +1491,8 @@
   }
 
 
-  bool SQLiteDatabaseWrapper::LookupResource(int64_t& id,
-                                             ResourceType& type,
-                                             const std::string& publicId)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT internalId, resourceType FROM Resources WHERE publicId=?");
-    s.BindString(0, publicId);
-
-    if (!s.Step())
-    {
-      return false;
-    }
-    else
-    {
-      id = s.ColumnInt(0);
-      type = static_cast<ResourceType>(s.ColumnInt(1));
-
-      // Check whether there is a single resource with this public id
-      assert(!s.Step());
-
-      return true;
-    }
-  }
-
-
-  void SQLiteDatabaseWrapper::AttachChild(int64_t parent,
-                                          int64_t child)
+  void SQLiteDatabaseWrapper::UnitTestsTransaction::AttachChild(int64_t parent,
+                                                                int64_t child)
   {
     SQLite::Statement s(db_, SQLITE_FROM_HERE, "UPDATE Resources SET parentId = ? WHERE internalId = ?");
     s.BindInt64(0, parent);
@@ -734,150 +1501,9 @@
   }
 
 
-  void SQLiteDatabaseWrapper::SetMetadata(int64_t id,
-                                          MetadataType type,
-                                          const std::string& value)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO Metadata VALUES(?, ?, ?)");
-    s.BindInt64(0, id);
-    s.BindInt(1, type);
-    s.BindString(2, value);
-    s.Run();
-  }
-
-
-  void SQLiteDatabaseWrapper::DeleteMetadata(int64_t id,
-                                             MetadataType type)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM Metadata WHERE id=? and type=?");
-    s.BindInt64(0, id);
-    s.BindInt(1, type);
-    s.Run();
-  }
-
-
-  bool SQLiteDatabaseWrapper::LookupMetadata(std::string& target,
-                                             int64_t id,
-                                             MetadataType type)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT value FROM Metadata WHERE id=? AND type=?");
-    s.BindInt64(0, id);
-    s.BindInt(1, type);
-
-    if (!s.Step())
-    {
-      return false;
-    }
-    else
-    {
-      target = s.ColumnString(0);
-      return true;
-    }
-  }
-
-
-  void SQLiteDatabaseWrapper::AddAttachment(int64_t id,
-                                            const FileInfo& attachment)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO AttachedFiles VALUES(?, ?, ?, ?, ?, ?, ?, ?)");
-    s.BindInt64(0, id);
-    s.BindInt(1, attachment.GetContentType());
-    s.BindString(2, attachment.GetUuid());
-    s.BindInt64(3, attachment.GetCompressedSize());
-    s.BindInt64(4, attachment.GetUncompressedSize());
-    s.BindInt(5, attachment.GetCompressionType());
-    s.BindString(6, attachment.GetUncompressedMD5());
-    s.BindString(7, attachment.GetCompressedMD5());
-    s.Run();
-  }
-
-
-  void SQLiteDatabaseWrapper::DeleteAttachment(int64_t id,
-                                               FileContentType attachment)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM AttachedFiles WHERE id=? AND fileType=?");
-    s.BindInt64(0, id);
-    s.BindInt(1, attachment);
-    s.Run();
-  }
-
-
-  void SQLiteDatabaseWrapper::ListAvailableAttachments(std::set<FileContentType>& target,
-                                                       int64_t id)
-  {
-    target.clear();
-
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT fileType FROM AttachedFiles WHERE id=?");
-    s.BindInt64(0, id);
-
-    while (s.Step())
-    {
-      target.insert(static_cast<FileContentType>(s.ColumnInt(0)));
-    }
-  }
-
-  bool SQLiteDatabaseWrapper::LookupAttachment(FileInfo& attachment,
-                                               int64_t id,
-                                               FileContentType contentType)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT uuid, uncompressedSize, compressionType, compressedSize, "
-                        "uncompressedMD5, compressedMD5 FROM AttachedFiles WHERE id=? AND fileType=?");
-    s.BindInt64(0, id);
-    s.BindInt(1, contentType);
-
-    if (!s.Step())
-    {
-      return false;
-    }
-    else
-    {
-      attachment = FileInfo(s.ColumnString(0),
-                            contentType,
-                            s.ColumnInt64(1),
-                            s.ColumnString(4),
-                            static_cast<CompressionType>(s.ColumnInt(2)),
-                            s.ColumnInt64(3),
-                            s.ColumnString(5));
-      return true;
-    }
-  }
-
-
-  void SQLiteDatabaseWrapper::ClearMainDicomTags(int64_t id)
-  {
-    {
-      SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM DicomIdentifiers WHERE id=?");
-      s.BindInt64(0, id);
-      s.Run();
-    }
-
-    {
-      SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM MainDicomTags WHERE id=?");
-      s.BindInt64(0, id);
-      s.Run();
-    }
-  }
-
-
-  void SQLiteDatabaseWrapper::SetMainDicomTag(int64_t id,
-                                              const DicomTag& tag,
-                                              const std::string& value)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO MainDicomTags VALUES(?, ?, ?, ?)");
-    s.BindInt64(0, id);
-    s.BindInt(1, tag.GetGroup());
-    s.BindInt(2, tag.GetElement());
-    s.BindString(3, value);
-    s.Run();
-  }
-
-
-  void SQLiteDatabaseWrapper::SetIdentifierTag(int64_t id,
-                                               const DicomTag& tag,
-                                               const std::string& value)
+  void SQLiteDatabaseWrapper::UnitTestsTransaction::SetIdentifierTag(int64_t id,
+                                                                     const DicomTag& tag,
+                                                                     const std::string& value)
   {
     SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO DicomIdentifiers VALUES(?, ?, ?, ?)");
     s.BindInt64(0, id);
@@ -888,427 +1514,40 @@
   }
 
 
-  void SQLiteDatabaseWrapper::GetMainDicomTags(DicomMap& map,
-                                               int64_t id)
+  void SQLiteDatabaseWrapper::UnitTestsTransaction::SetMainDicomTag(int64_t id,
+                                                                    const DicomTag& tag,
+                                                                    const std::string& value)
   {
-    map.Clear();
-
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM MainDicomTags WHERE id=?");
-    s.BindInt64(0, id);
-    while (s.Step())
-    {
-      map.SetValue(s.ColumnInt(1),
-                   s.ColumnInt(2),
-                   s.ColumnString(3), false);
-    }
-  }
-
-
-  void SQLiteDatabaseWrapper::GetChildrenPublicId(std::list<std::string>& target,
-                                                  int64_t id)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.publicId FROM Resources AS a, Resources AS b  "
-                        "WHERE a.parentId = b.internalId AND b.internalId = ?");     
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO MainDicomTags VALUES(?, ?, ?, ?)");
     s.BindInt64(0, id);
-
-    target.clear();
-
-    while (s.Step())
-    {
-      target.push_back(s.ColumnString(0));
-    }
-  }
-
-
-  void SQLiteDatabaseWrapper::GetChildrenInternalId(std::list<int64_t>& target,
-                                                    int64_t id)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.internalId FROM Resources AS a, Resources AS b  "
-                        "WHERE a.parentId = b.internalId AND b.internalId = ?");     
-    s.BindInt64(0, id);
-
-    target.clear();
-
-    while (s.Step())
-    {
-      target.push_back(s.ColumnInt64(0));
-    }
-  }
-
-
-  void SQLiteDatabaseWrapper::LogChange(int64_t internalId,
-                                        const ServerIndexChange& change)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Changes VALUES(NULL, ?, ?, ?, ?)");
-    s.BindInt(0, change.GetChangeType());
-    s.BindInt64(1, internalId);
-    s.BindInt(2, change.GetResourceType());
-    s.BindString(3, change.GetDate());
+    s.BindInt(1, tag.GetGroup());
+    s.BindInt(2, tag.GetElement());
+    s.BindString(3, value);
     s.Run();
   }
 
 
-  void SQLiteDatabaseWrapper::LogExportedResource(const ExportedResource& resource)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "INSERT INTO ExportedResources VALUES(NULL, ?, ?, ?, ?, ?, ?, ?, ?)");
-
-    s.BindInt(0, resource.GetResourceType());
-    s.BindString(1, resource.GetPublicId());
-    s.BindString(2, resource.GetModality());
-    s.BindString(3, resource.GetPatientId());
-    s.BindString(4, resource.GetStudyInstanceUid());
-    s.BindString(5, resource.GetSeriesInstanceUid());
-    s.BindString(6, resource.GetSopInstanceUid());
-    s.BindString(7, resource.GetDate());
-    s.Run();      
-  }
-
-
-  void SQLiteDatabaseWrapper::GetExportedResources(std::list<ExportedResource>& target,
-                                                   bool& done,
-                                                   int64_t since,
-                                                   uint32_t maxResults)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT * FROM ExportedResources WHERE seq>? ORDER BY seq LIMIT ?");
-    s.BindInt64(0, since);
-    s.BindInt(1, maxResults + 1);
-    GetExportedResourcesInternal(target, done, s, maxResults);
-  }
-
-    
-  void SQLiteDatabaseWrapper::GetLastExportedResource(std::list<ExportedResource>& target)
-  {
-    bool done;  // Ignored
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT * FROM ExportedResources ORDER BY seq DESC LIMIT 1");
-    GetExportedResourcesInternal(target, done, s, 1);
-  }
-
-    
-  uint64_t SQLiteDatabaseWrapper::GetTotalCompressedSize()
-  {
-    // Old SQL query that was used in Orthanc <= 1.5.0:
-    // SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(compressedSize) FROM AttachedFiles");
-
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT value FROM GlobalIntegers WHERE key=0");
-    s.Run();
-    return static_cast<uint64_t>(s.ColumnInt64(0));
-  }
-
-    
-  uint64_t SQLiteDatabaseWrapper::GetTotalUncompressedSize()
+  int64_t SQLiteDatabaseWrapper::UnitTestsTransaction::GetTableRecordCount(const std::string& table)
   {
-    // Old SQL query that was used in Orthanc <= 1.5.0:
-    // SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(uncompressedSize) FROM AttachedFiles");
-
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT value FROM GlobalIntegers WHERE key=1");
-    s.Run();
-    return static_cast<uint64_t>(s.ColumnInt64(0));
-  }
-
-
-  uint64_t SQLiteDatabaseWrapper::GetResourceCount(ResourceType resourceType)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT COUNT(*) FROM Resources WHERE resourceType=?");
-    s.BindInt(0, resourceType);
-    
-    if (!s.Step())
-    {
-      return 0;
-    }
-    else
+    /**
+     * "Generally one cannot use SQL parameters/placeholders for
+     * database identifiers (tables, columns, views, schemas, etc.) or
+     * database functions (e.g., CURRENT_DATE), but instead only for
+     * binding literal values." => To avoid any SQL injection, we
+     * check that the "table" parameter has only alphabetic
+     * characters.
+     * https://stackoverflow.com/a/1274764/881731
+     **/
+    for (size_t i = 0; i < table.size(); i++)
     {
-      int64_t c = s.ColumnInt(0);
-      assert(!s.Step());
-      return c;
-    }
-  }
-
-
-  void SQLiteDatabaseWrapper::GetAllPublicIds(std::list<std::string>& target,
-                                              ResourceType resourceType)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Resources WHERE resourceType=?");
-    s.BindInt(0, resourceType);
-
-    target.clear();
-    while (s.Step())
-    {
-      target.push_back(s.ColumnString(0));
-    }
-  }
-
-
-  void SQLiteDatabaseWrapper::GetAllPublicIds(std::list<std::string>& target,
-                                              ResourceType resourceType,
-                                              size_t since,
-                                              size_t limit)
-  {
-    if (limit == 0)
-    {
-      target.clear();
-      return;
+      if (!isalpha(table[i]))
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
     }
 
-    SQLite::Statement s(db_, SQLITE_FROM_HERE,
-                        "SELECT publicId FROM Resources WHERE "
-                        "resourceType=? LIMIT ? OFFSET ?");
-    s.BindInt(0, resourceType);
-    s.BindInt64(1, limit);
-    s.BindInt64(2, since);
-
-    target.clear();
-    while (s.Step())
-    {
-      target.push_back(s.ColumnString(0));
-    }
-  }
-
-
-  bool SQLiteDatabaseWrapper::SelectPatientToRecycle(int64_t& internalId)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE,
-                        "SELECT patientId FROM PatientRecyclingOrder ORDER BY seq ASC LIMIT 1");
-   
-    if (!s.Step())
-    {
-      // No patient remaining or all the patients are protected
-      return false;
-    }
-    else
-    {
-      internalId = s.ColumnInt(0);
-      return true;
-    }    
-  }
-
-
-  bool SQLiteDatabaseWrapper::SelectPatientToRecycle(int64_t& internalId,
-                                                     int64_t patientIdToAvoid)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE,
-                        "SELECT patientId FROM PatientRecyclingOrder "
-                        "WHERE patientId != ? ORDER BY seq ASC LIMIT 1");
-    s.BindInt64(0, patientIdToAvoid);
-
-    if (!s.Step())
-    {
-      // No patient remaining or all the patients are protected
-      return false;
-    }
-    else
-    {
-      internalId = s.ColumnInt(0);
-      return true;
-    }   
-  }
-
-
-  bool SQLiteDatabaseWrapper::IsProtectedPatient(int64_t internalId)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE,
-                        "SELECT * FROM PatientRecyclingOrder WHERE patientId = ?");
-    s.BindInt64(0, internalId);
-    return !s.Step();
-  }
-
-
-  void SQLiteDatabaseWrapper::SetProtectedPatient(int64_t internalId, 
-                                                  bool isProtected)
-  {
-    if (isProtected)
-    {
-      SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM PatientRecyclingOrder WHERE patientId=?");
-      s.BindInt64(0, internalId);
-      s.Run();
-    }
-    else if (IsProtectedPatient(internalId))
-    {
-      SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO PatientRecyclingOrder VALUES(NULL, ?)");
-      s.BindInt64(0, internalId);
-      s.Run();
-    }
-    else
-    {
-      // Nothing to do: The patient is already unprotected
-    }
-  }
-
-
-  bool SQLiteDatabaseWrapper::IsExistingResource(int64_t internalId)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT * FROM Resources WHERE internalId=?");
-    s.BindInt64(0, internalId);
-    return s.Step();
-  }
-
-
-  bool SQLiteDatabaseWrapper::IsDiskSizeAbove(uint64_t threshold)
-  {
-    return GetTotalCompressedSize() > threshold;
-  }
-
-
-
-  class SQLiteDatabaseWrapper::LookupFormatter : public ISqlLookupFormatter
-  {
-  private:
-    std::list<std::string>  values_;
-
-  public:
-    virtual std::string GenerateParameter(const std::string& value)
-    {
-      values_.push_back(value);
-      return "?";
-    }
-    
-    virtual std::string FormatResourceType(ResourceType level)
-    {
-      return boost::lexical_cast<std::string>(level);
-    }
-
-    virtual std::string FormatWildcardEscape()
-    {
-      return "ESCAPE '\\'";
-    }
-
-    void Bind(SQLite::Statement& statement) const
-    {
-      size_t pos = 0;
-      
-      for (std::list<std::string>::const_iterator
-             it = values_.begin(); it != values_.end(); ++it, pos++)
-      {
-        statement.BindString(pos, *it);
-      }
-    }
-  };
-
-  
-  static void AnswerLookup(std::list<std::string>& resourcesId,
-                           std::list<std::string>& instancesId,
-                           SQLite::Connection& db,
-                           ResourceType level)
-  {
-    resourcesId.clear();
-    instancesId.clear();
-    
-    std::unique_ptr<SQLite::Statement> statement;
-    
-    switch (level)
-    {
-      case ResourceType_Patient:
-      {
-        statement.reset(
-          new SQLite::Statement(
-            db, SQLITE_FROM_HERE,
-            "SELECT patients.publicId, instances.publicID FROM Lookup AS patients "
-            "INNER JOIN Resources studies ON patients.internalId=studies.parentId "
-            "INNER JOIN Resources series ON studies.internalId=series.parentId "
-            "INNER JOIN Resources instances ON series.internalId=instances.parentId "
-            "GROUP BY patients.publicId"));
-      
-        break;
-      }
-
-      case ResourceType_Study:
-      {
-        statement.reset(
-          new SQLite::Statement(
-            db, SQLITE_FROM_HERE,
-            "SELECT studies.publicId, instances.publicID FROM Lookup AS studies "
-            "INNER JOIN Resources series ON studies.internalId=series.parentId "
-            "INNER JOIN Resources instances ON series.internalId=instances.parentId "
-            "GROUP BY studies.publicId"));
-      
-        break;
-      }
-
-      case ResourceType_Series:
-      {
-        statement.reset(
-          new SQLite::Statement(
-            db, SQLITE_FROM_HERE,
-            "SELECT series.publicId, instances.publicID FROM Lookup AS series "
-            "INNER JOIN Resources instances ON series.internalId=instances.parentId "
-            "GROUP BY series.publicId"));
-      
-        break;
-      }
-
-      case ResourceType_Instance:
-      {
-        statement.reset(
-          new SQLite::Statement(
-            db, SQLITE_FROM_HERE, "SELECT publicId, publicId FROM Lookup"));
-        
-        break;
-      }
-      
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-    }
-
-    assert(statement.get() != NULL);
-      
-    while (statement->Step())
-    {
-      resourcesId.push_back(statement->ColumnString(0));
-      instancesId.push_back(statement->ColumnString(1));
-    }
-  }
-
-
-  void SQLiteDatabaseWrapper::ApplyLookupResources(std::list<std::string>& resourcesId,
-                                                   std::list<std::string>* instancesId,
-                                                   const std::vector<DatabaseConstraint>& lookup,
-                                                   ResourceType queryLevel,
-                                                   size_t limit)
-  {
-    LookupFormatter formatter;
-
-    std::string sql;
-    LookupFormatter::Apply(sql, formatter, lookup, queryLevel, limit);
-
-    sql = "CREATE TEMPORARY TABLE Lookup AS " + sql;
-    
-    {
-      SQLite::Statement s(db_, SQLITE_FROM_HERE, "DROP TABLE IF EXISTS Lookup");
-      s.Run();
-    }
-
-    {
-      SQLite::Statement statement(db_, sql);
-      formatter.Bind(statement);
-      statement.Run();
-    }
-
-    if (instancesId != NULL)
-    {
-      AnswerLookup(resourcesId, *instancesId, db_, queryLevel);
-    }
-    else
-    {
-      resourcesId.clear();
-    
-      SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Lookup");
-        
-      while (s.Step())
-      {
-        resourcesId.push_back(s.ColumnString(0));
-      }
-    }
-  }
-
-
-  int64_t SQLiteDatabaseWrapper::GetLastChangeIndex()
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT seq FROM sqlite_sequence WHERE name='Changes'");
+    // Don't use "SQLITE_FROM_HERE", otherwise "table" would be cached
+    SQLite::Statement s(db_, "SELECT COUNT(*) FROM " + table);
 
     if (s.Step())
     {
@@ -1318,35 +1557,40 @@
     }
     else
     {
-      // No change has been recorded so far in the database
-      return 0;
+      throw OrthancException(ErrorCode_InternalError);
     }
   }
 
 
-  void SQLiteDatabaseWrapper::TagMostRecentPatient(int64_t patient)
+  bool SQLiteDatabaseWrapper::UnitTestsTransaction::GetParentPublicId(std::string& target,
+                                                                      int64_t id)
   {
-    {
-      SQLite::Statement s(db_, SQLITE_FROM_HERE,
-                          "DELETE FROM PatientRecyclingOrder WHERE patientId=?");
-      s.BindInt64(0, patient);
-      s.Run();
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.publicId FROM Resources AS a, Resources AS b "
+                        "WHERE a.internalId = b.parentId AND b.internalId = ?");     
+    s.BindInt64(0, id);
 
-      assert(db_.GetLastChangeCount() == 0 ||
-             db_.GetLastChangeCount() == 1);
-      
-      if (db_.GetLastChangeCount() == 0)
-      {
-        // The patient was protected, there was nothing to delete from the recycling order
-        return;
-      }
+    if (s.Step())
+    {
+      target = s.ColumnString(0);
+      return true;
+    }
+    else
+    {
+      return false;
     }
+  }
 
+
+  void SQLiteDatabaseWrapper::UnitTestsTransaction::GetChildren(std::list<std::string>& childrenPublicIds,
+                                                                int64_t id)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Resources WHERE parentId=?");
+    s.BindInt64(0, id);
+
+    childrenPublicIds.clear();
+    while (s.Step())
     {
-      SQLite::Statement s(db_, SQLITE_FROM_HERE,
-                          "INSERT INTO PatientRecyclingOrder VALUES(NULL, ?)");
-      s.BindInt64(0, patient);
-      s.Run();
+      childrenPublicIds.push_back(s.ColumnString(0));
     }
   }
 }
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h	Tue Apr 20 18:11:29 2021 +0200
@@ -36,38 +36,32 @@
 #include "IDatabaseWrapper.h"
 
 #include "../../../OrthancFramework/Sources/SQLite/Connection.h"
-#include "Compatibility/ICreateInstance.h"
-#include "Compatibility/IGetChildrenMetadata.h"
-#include "Compatibility/ILookupResourceAndParent.h"
-#include "Compatibility/ISetResourcesContent.h"
+
+#include <boost/thread/mutex.hpp>
 
 namespace Orthanc
 {
-  namespace Internals
-  {
-    class SignalRemainingAncestor;
-  }
-
   /**
    * This class manages an instance of the Orthanc SQLite database. It
    * translates low-level requests into SQL statements. Mutual
    * exclusion MUST be implemented at a higher level.
    **/
-  class SQLiteDatabaseWrapper :
-    public IDatabaseWrapper,
-    public Compatibility::ICreateInstance,
-    public Compatibility::IGetChildrenMetadata,
-    public Compatibility::ILookupResourceAndParent,
-    public Compatibility::ISetResourcesContent
+  class SQLiteDatabaseWrapper : public IDatabaseWrapper
   {
   private:
-    class Transaction;
+    class TransactionBase;
+    class SignalFileDeleted;
+    class SignalResourceDeleted;
+    class SignalRemainingAncestor;
+    class ReadOnlyTransaction;
+    class ReadWriteTransaction;
     class LookupFormatter;
 
-    IDatabaseListener* listener_;
-    SQLite::Connection db_;
-    Internals::SignalRemainingAncestor* signalRemainingAncestor_;
-    unsigned int version_;
+    boost::mutex              mutex_;
+    SQLite::Connection        db_;
+    TransactionBase*          activeTransaction_;
+    SignalRemainingAncestor*  signalRemainingAncestor_;
+    unsigned int              version_;
 
     void GetChangesInternal(std::list<ServerIndexChange>& target,
                             bool& done,
@@ -79,294 +73,80 @@
                                       SQLite::Statement& s,
                                       uint32_t maxResults);
 
-    void ClearTable(const std::string& tableName);
-
-    // Unused => could be removed
-    int GetGlobalIntegerProperty(GlobalProperty property,
-                                 int defaultValue);
-
   public:
     SQLiteDatabaseWrapper(const std::string& path);
 
     SQLiteDatabaseWrapper();
 
-    virtual void Open()
-      ORTHANC_OVERRIDE;
+    virtual ~SQLiteDatabaseWrapper();
 
-    virtual void Close()
-      ORTHANC_OVERRIDE
-    {
-      db_.Close();
-    }
+    virtual void Open() ORTHANC_OVERRIDE;
 
-    virtual void SetListener(IDatabaseListener& listener)
-      ORTHANC_OVERRIDE;
+    virtual void Close() ORTHANC_OVERRIDE;
 
-    virtual bool LookupParent(int64_t& parentId,
-                              int64_t resourceId)
-      ORTHANC_OVERRIDE;
-
-    virtual std::string GetPublicId(int64_t resourceId)
-      ORTHANC_OVERRIDE;
-
-    virtual ResourceType GetResourceType(int64_t resourceId)
+    virtual IDatabaseWrapper::ITransaction* StartTransaction(TransactionType type,
+                                                             IDatabaseListener& listener)
       ORTHANC_OVERRIDE;
 
-    virtual void DeleteResource(int64_t id)
-      ORTHANC_OVERRIDE;
-
-    virtual void GetChanges(std::list<ServerIndexChange>& target /*out*/,
-                            bool& done /*out*/,
-                            int64_t since,
-                            uint32_t maxResults)
-      ORTHANC_OVERRIDE;
-
-    virtual void GetLastChange(std::list<ServerIndexChange>& target /*out*/)
-      ORTHANC_OVERRIDE;
+    virtual void FlushToDisk() ORTHANC_OVERRIDE;
 
-    virtual IDatabaseWrapper::ITransaction* StartTransaction()
-      ORTHANC_OVERRIDE;
-
-    virtual void FlushToDisk()
-      ORTHANC_OVERRIDE
-    {
-      db_.FlushToDisk();
-    }
-
-    virtual bool HasFlushToDisk() const
-      ORTHANC_OVERRIDE
+    virtual bool HasFlushToDisk() const ORTHANC_OVERRIDE
     {
       return true;
     }
 
-    virtual void ClearChanges()
-      ORTHANC_OVERRIDE
-    {
-      ClearTable("Changes");
-    }
-
-    virtual void ClearExportedResources()
-      ORTHANC_OVERRIDE
-    {
-      ClearTable("ExportedResources");
-    }
-
-    virtual void GetAllMetadata(std::map<MetadataType, std::string>& target,
-                                int64_t id)
-      ORTHANC_OVERRIDE;
-
-    virtual unsigned int GetDatabaseVersion()
-      ORTHANC_OVERRIDE
+    virtual unsigned int GetDatabaseVersion() ORTHANC_OVERRIDE
     {
       return version_;
     }
 
     virtual void Upgrade(unsigned int targetVersion,
-                         IStorageArea& storageArea)
-      ORTHANC_OVERRIDE;
-
+                         IStorageArea& storageArea) ORTHANC_OVERRIDE;
 
-    /**
-     * The methods declared below are for unit testing only!
-     **/
-
-    const char* GetErrorMessage() const
+    virtual bool HasRevisionsSupport() const ORTHANC_OVERRIDE
     {
-      return db_.GetErrorMessage();
+      return false;  // TODO - REVISIONS
     }
 
-    void GetChildren(std::list<std::string>& childrenPublicIds,
-                     int64_t id);
-
-    int64_t GetTableRecordCount(const std::string& table);
-    
-    bool GetParentPublicId(std::string& target,
-                           int64_t id);
-
-
 
     /**
-     * Until Orthanc 1.4.0, the methods below were part of the
-     * "DatabaseWrapperBase" class, that is now placed in the
-     * graveyard.
+     * The "StartTransaction()" method is guaranteed to return a class
+     * derived from "UnitTestsTransaction". The methods of
+     * "UnitTestsTransaction" give access to additional information
+     * about the underlying SQLite database to be used in unit tests.
      **/
-
-    virtual void SetGlobalProperty(GlobalProperty property,
-                                   const std::string& value)
-      ORTHANC_OVERRIDE;
-
-    virtual bool LookupGlobalProperty(std::string& target,
-                                      GlobalProperty property)
-      ORTHANC_OVERRIDE;
-
-    virtual int64_t CreateResource(const std::string& publicId,
-                                   ResourceType type)
-      ORTHANC_OVERRIDE;
-
-    virtual bool LookupResource(int64_t& id,
-                                ResourceType& type,
-                                const std::string& publicId)
-      ORTHANC_OVERRIDE;
-
-    virtual void AttachChild(int64_t parent,
-                             int64_t child)
-      ORTHANC_OVERRIDE;
-
-    virtual void SetMetadata(int64_t id,
-                             MetadataType type,
-                             const std::string& value)
-      ORTHANC_OVERRIDE;
-
-    virtual void DeleteMetadata(int64_t id,
-                                MetadataType type)
-      ORTHANC_OVERRIDE;
-
-    virtual bool LookupMetadata(std::string& target,
-                                int64_t id,
-                                MetadataType type)
-      ORTHANC_OVERRIDE;
-
-    virtual void AddAttachment(int64_t id,
-                               const FileInfo& attachment)
-      ORTHANC_OVERRIDE;
+    class UnitTestsTransaction : public ITransaction
+    {
+    protected:
+      SQLite::Connection& db_;
+      
+    public:
+      UnitTestsTransaction(SQLite::Connection& db) :
+        db_(db)
+      {
+      }
+      
+      void GetChildren(std::list<std::string>& childrenPublicIds,
+                       int64_t id);
 
-    virtual void DeleteAttachment(int64_t id,
-                                  FileContentType attachment)
-      ORTHANC_OVERRIDE;
-
-    virtual void ListAvailableAttachments(std::set<FileContentType>& target,
-                                          int64_t id)
-      ORTHANC_OVERRIDE;
-
-    virtual bool LookupAttachment(FileInfo& attachment,
-                                  int64_t id,
-                                  FileContentType contentType)
-      ORTHANC_OVERRIDE;
-
-    virtual void ClearMainDicomTags(int64_t id)
-      ORTHANC_OVERRIDE;
-
-    virtual void SetMainDicomTag(int64_t id,
-                                 const DicomTag& tag,
-                                 const std::string& value)
-      ORTHANC_OVERRIDE;
-
-    virtual void SetIdentifierTag(int64_t id,
-                                  const DicomTag& tag,
-                                  const std::string& value)
-      ORTHANC_OVERRIDE;
-
-    virtual void GetMainDicomTags(DicomMap& map,
-                                  int64_t id)
-      ORTHANC_OVERRIDE;
-
-    virtual void GetChildrenPublicId(std::list<std::string>& target,
-                                     int64_t id)
-      ORTHANC_OVERRIDE;
-
-    virtual void GetChildrenInternalId(std::list<int64_t>& target,
-                                       int64_t id)
-      ORTHANC_OVERRIDE;
-
-    virtual void LogChange(int64_t internalId,
-                           const ServerIndexChange& change)
-      ORTHANC_OVERRIDE;
-
-    virtual void LogExportedResource(const ExportedResource& resource)
-      ORTHANC_OVERRIDE;
+      int64_t GetTableRecordCount(const std::string& table);
     
-    virtual void GetExportedResources(std::list<ExportedResource>& target /*out*/,
-                                      bool& done /*out*/,
-                                      int64_t since,
-                                      uint32_t maxResults)
-      ORTHANC_OVERRIDE;
-
-    virtual void GetLastExportedResource(std::list<ExportedResource>& target /*out*/)
-      ORTHANC_OVERRIDE;
-
-    virtual uint64_t GetTotalCompressedSize()
-      ORTHANC_OVERRIDE;
-    
-    virtual uint64_t GetTotalUncompressedSize()
-      ORTHANC_OVERRIDE;
-
-    virtual uint64_t GetResourceCount(ResourceType resourceType)
-      ORTHANC_OVERRIDE;
-
-    virtual void GetAllPublicIds(std::list<std::string>& target,
-                                 ResourceType resourceType)
-      ORTHANC_OVERRIDE;
+      bool GetParentPublicId(std::string& target,
+                             int64_t id);
 
-    virtual void GetAllPublicIds(std::list<std::string>& target,
-                                 ResourceType resourceType,
-                                 size_t since,
-                                 size_t limit)
-      ORTHANC_OVERRIDE;
-
-    virtual bool SelectPatientToRecycle(int64_t& internalId)
-      ORTHANC_OVERRIDE;
+      int64_t CreateResource(const std::string& publicId,
+                             ResourceType type);
 
-    virtual bool SelectPatientToRecycle(int64_t& internalId,
-                                        int64_t patientIdToAvoid)
-      ORTHANC_OVERRIDE;
-
-    virtual bool IsProtectedPatient(int64_t internalId)
-      ORTHANC_OVERRIDE;
-
-    virtual void SetProtectedPatient(int64_t internalId, 
-                                     bool isProtected)
-      ORTHANC_OVERRIDE;
-
-    virtual bool IsExistingResource(int64_t internalId)
-      ORTHANC_OVERRIDE;
+      void AttachChild(int64_t parent,
+                       int64_t child);
 
-    virtual bool IsDiskSizeAbove(uint64_t threshold)
-      ORTHANC_OVERRIDE;
-
-    virtual void ApplyLookupResources(std::list<std::string>& resourcesId,
-                                      std::list<std::string>* instancesId,
-                                      const std::vector<DatabaseConstraint>& lookup,
-                                      ResourceType queryLevel,
-                                      size_t limit)
-      ORTHANC_OVERRIDE;
-
-    virtual bool CreateInstance(CreateInstanceResult& result,
-                                int64_t& instanceId,
-                                const std::string& patient,
-                                const std::string& study,
-                                const std::string& series,
-                                const std::string& instance)
-      ORTHANC_OVERRIDE
-    {
-      return ICreateInstance::Apply
-        (*this, result, instanceId, patient, study, series, instance);
-    }
+      void SetIdentifierTag(int64_t id,
+                            const DicomTag& tag,
+                            const std::string& value);
 
-    virtual void SetResourcesContent(const Orthanc::ResourcesContent& content)
-      ORTHANC_OVERRIDE
-    {
-      ISetResourcesContent::Apply(*this, content);
-    }
-
-    virtual void GetChildrenMetadata(std::list<std::string>& target,
-                                     int64_t resourceId,
-                                     MetadataType metadata)
-      ORTHANC_OVERRIDE
-    {
-      IGetChildrenMetadata::Apply(*this, target, resourceId, metadata);
-    }
-
-    virtual int64_t GetLastChangeIndex() ORTHANC_OVERRIDE;
-
-    virtual void TagMostRecentPatient(int64_t patient) ORTHANC_OVERRIDE;
-
-    virtual bool LookupResourceAndParent(int64_t& id,
-                                         ResourceType& type,
-                                         std::string& parentPublicId,
-                                         const std::string& publicId)
-      ORTHANC_OVERRIDE
-    {
-      return ILookupResourceAndParent::Apply(*this, id, type, parentPublicId, publicId);
-    }
+      void SetMainDicomTag(int64_t id,
+                           const DicomTag& tag,
+                           const std::string& value);
+    };
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Tue Apr 20 18:11:29 2021 +0200
@@ -0,0 +1,3377 @@
+/**
+ * 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 General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * 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 "../PrecompiledHeadersServer.h"
+#include "StatelessDatabaseOperations.h"
+
+#ifndef NOMINMAX
+#define NOMINMAX
+#endif
+
+#include "../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
+#include "../../../OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h"
+#include "../../../OrthancFramework/Sources/Logging.h"
+#include "../../../OrthancFramework/Sources/OrthancException.h"
+#include "../OrthancConfiguration.h"
+#include "../Search/DatabaseLookup.h"
+#include "../ServerIndexChange.h"
+#include "../ServerToolbox.h"
+#include "ResourcesContent.h"
+
+#include <boost/lexical_cast.hpp>
+#include <boost/tuple/tuple.hpp>
+#include <stack>
+
+
+namespace Orthanc
+{
+  namespace
+  {
+    /**
+     * Some handy templates to reduce the verbosity in the definitions
+     * of the internal classes.
+     **/
+    
+    template <typename Operations,
+              typename Tuple>
+    class TupleOperationsWrapper : public StatelessDatabaseOperations::IReadOnlyOperations
+    {
+    protected:
+      Operations&   operations_;
+      const Tuple&  tuple_;
+    
+    public:
+      TupleOperationsWrapper(Operations& operations,
+                             const Tuple& tuple) :
+        operations_(operations),
+        tuple_(tuple)
+      {
+      }
+    
+      virtual void Apply(StatelessDatabaseOperations::ReadOnlyTransaction& transaction) ORTHANC_OVERRIDE
+      {
+        operations_.ApplyTuple(transaction, tuple_);
+      }
+    };
+
+
+    template <typename T1>
+    class ReadOnlyOperationsT1 : public boost::noncopyable
+    {
+    public:
+      typedef typename boost::tuple<T1>  Tuple;
+      
+      virtual ~ReadOnlyOperationsT1()
+      {
+      }
+
+      virtual void ApplyTuple(StatelessDatabaseOperations::ReadOnlyTransaction& transaction,
+                              const Tuple& tuple) = 0;
+
+      void Apply(StatelessDatabaseOperations& index,
+                 T1 t1)
+      {
+        const Tuple tuple(t1);
+        TupleOperationsWrapper<ReadOnlyOperationsT1, Tuple> wrapper(*this, tuple);
+        index.Apply(wrapper);
+      }
+    };
+
+
+    template <typename T1,
+              typename T2>
+    class ReadOnlyOperationsT2 : public boost::noncopyable
+    {
+    public:
+      typedef typename boost::tuple<T1, T2>  Tuple;
+      
+      virtual ~ReadOnlyOperationsT2()
+      {
+      }
+
+      virtual void ApplyTuple(StatelessDatabaseOperations::ReadOnlyTransaction& transaction,
+                              const Tuple& tuple) = 0;
+
+      void Apply(StatelessDatabaseOperations& index,
+                 T1 t1,
+                 T2 t2)
+      {
+        const Tuple tuple(t1, t2);
+        TupleOperationsWrapper<ReadOnlyOperationsT2, Tuple> wrapper(*this, tuple);
+        index.Apply(wrapper);
+      }
+    };
+
+
+    template <typename T1,
+              typename T2,
+              typename T3>
+    class ReadOnlyOperationsT3 : public boost::noncopyable
+    {
+    public:
+      typedef typename boost::tuple<T1, T2, T3>  Tuple;
+      
+      virtual ~ReadOnlyOperationsT3()
+      {
+      }
+
+      virtual void ApplyTuple(StatelessDatabaseOperations::ReadOnlyTransaction& transaction,
+                              const Tuple& tuple) = 0;
+
+      void Apply(StatelessDatabaseOperations& index,
+                 T1 t1,
+                 T2 t2,
+                 T3 t3)
+      {
+        const Tuple tuple(t1, t2, t3);
+        TupleOperationsWrapper<ReadOnlyOperationsT3, Tuple> wrapper(*this, tuple);
+        index.Apply(wrapper);
+      }
+    };
+
+
+    template <typename T1,
+              typename T2,
+              typename T3,
+              typename T4>
+    class ReadOnlyOperationsT4 : public boost::noncopyable
+    {
+    public:
+      typedef typename boost::tuple<T1, T2, T3, T4>  Tuple;
+      
+      virtual ~ReadOnlyOperationsT4()
+      {
+      }
+
+      virtual void ApplyTuple(StatelessDatabaseOperations::ReadOnlyTransaction& transaction,
+                              const Tuple& tuple) = 0;
+
+      void Apply(StatelessDatabaseOperations& index,
+                 T1 t1,
+                 T2 t2,
+                 T3 t3,
+                 T4 t4)
+      {
+        const Tuple tuple(t1, t2, t3, t4);
+        TupleOperationsWrapper<ReadOnlyOperationsT4, Tuple> wrapper(*this, tuple);
+        index.Apply(wrapper);
+      }
+    };
+
+
+    template <typename T1,
+              typename T2,
+              typename T3,
+              typename T4,
+              typename T5>
+    class ReadOnlyOperationsT5 : public boost::noncopyable
+    {
+    public:
+      typedef typename boost::tuple<T1, T2, T3, T4, T5>  Tuple;
+      
+      virtual ~ReadOnlyOperationsT5()
+      {
+      }
+
+      virtual void ApplyTuple(StatelessDatabaseOperations::ReadOnlyTransaction& transaction,
+                              const Tuple& tuple) = 0;
+
+      void Apply(StatelessDatabaseOperations& index,
+                 T1 t1,
+                 T2 t2,
+                 T3 t3,
+                 T4 t4,
+                 T5 t5)
+      {
+        const Tuple tuple(t1, t2, t3, t4, t5);
+        TupleOperationsWrapper<ReadOnlyOperationsT5, Tuple> wrapper(*this, tuple);
+        index.Apply(wrapper);
+      }
+    };
+
+
+    template <typename T1,
+              typename T2,
+              typename T3,
+              typename T4,
+              typename T5,
+              typename T6>
+    class ReadOnlyOperationsT6 : public boost::noncopyable
+    {
+    public:
+      typedef typename boost::tuple<T1, T2, T3, T4, T5, T6>  Tuple;
+      
+      virtual ~ReadOnlyOperationsT6()
+      {
+      }
+
+      virtual void ApplyTuple(StatelessDatabaseOperations::ReadOnlyTransaction& transaction,
+                              const Tuple& tuple) = 0;
+
+      void Apply(StatelessDatabaseOperations& index,
+                 T1 t1,
+                 T2 t2,
+                 T3 t3,
+                 T4 t4,
+                 T5 t5,
+                 T6 t6)
+      {
+        const Tuple tuple(t1, t2, t3, t4, t5, t6);
+        TupleOperationsWrapper<ReadOnlyOperationsT6, Tuple> wrapper(*this, tuple);
+        index.Apply(wrapper);
+      }
+    };
+  }
+
+
+  template <typename T>
+  static void FormatLog(Json::Value& target,
+                        const std::list<T>& log,
+                        const std::string& name,
+                        bool done,
+                        int64_t since,
+                        bool hasLast,
+                        int64_t last)
+  {
+    Json::Value items = Json::arrayValue;
+    for (typename std::list<T>::const_iterator
+           it = log.begin(); it != log.end(); ++it)
+    {
+      Json::Value item;
+      it->Format(item);
+      items.append(item);
+    }
+
+    target = Json::objectValue;
+    target[name] = items;
+    target["Done"] = done;
+
+    if (!hasLast)
+    {
+      // Best-effort guess of the last index in the sequence
+      if (log.empty())
+      {
+        last = since;
+      }
+      else
+      {
+        last = log.back().GetSeq();
+      }
+    }
+    
+    target["Last"] = static_cast<int>(last);
+  }
+
+
+  static void CopyListToVector(std::vector<std::string>& target,
+                               const std::list<std::string>& source)
+  {
+    target.resize(source.size());
+
+    size_t pos = 0;
+    
+    for (std::list<std::string>::const_iterator
+           it = source.begin(); it != source.end(); ++it)
+    {
+      target[pos] = *it;
+      pos ++;
+    }      
+  }
+
+
+  class StatelessDatabaseOperations::MainDicomTagsRegistry : public boost::noncopyable
+  {
+  private:
+    class TagInfo
+    {
+    private:
+      ResourceType  level_;
+      DicomTagType  type_;
+
+    public:
+      TagInfo()
+      {
+      }
+
+      TagInfo(ResourceType level,
+              DicomTagType type) :
+        level_(level),
+        type_(type)
+      {
+      }
+
+      ResourceType GetLevel() const
+      {
+        return level_;
+      }
+
+      DicomTagType GetType() const
+      {
+        return type_;
+      }
+    };
+      
+    typedef std::map<DicomTag, TagInfo>   Registry;
+
+
+    Registry  registry_;
+      
+    void LoadTags(ResourceType level)
+    {
+      {
+        const DicomTag* tags = NULL;
+        size_t size;
+  
+        ServerToolbox::LoadIdentifiers(tags, size, level);
+  
+        for (size_t i = 0; i < size; i++)
+        {
+          if (registry_.find(tags[i]) == registry_.end())
+          {
+            registry_[tags[i]] = TagInfo(level, DicomTagType_Identifier);
+          }
+          else
+          {
+            // These patient-level tags are copied in the study level
+            assert(level == ResourceType_Study &&
+                   (tags[i] == DICOM_TAG_PATIENT_ID ||
+                    tags[i] == DICOM_TAG_PATIENT_NAME ||
+                    tags[i] == DICOM_TAG_PATIENT_BIRTH_DATE));
+          }
+        }
+      }
+
+      {
+        std::set<DicomTag> tags;
+        DicomMap::GetMainDicomTags(tags, level);
+
+        for (std::set<DicomTag>::const_iterator
+               tag = tags.begin(); tag != tags.end(); ++tag)
+        {
+          if (registry_.find(*tag) == registry_.end())
+          {
+            registry_[*tag] = TagInfo(level, DicomTagType_Main);
+          }
+        }
+      }
+    }
+
+  public:
+    MainDicomTagsRegistry()
+    {
+      LoadTags(ResourceType_Patient);
+      LoadTags(ResourceType_Study);
+      LoadTags(ResourceType_Series);
+      LoadTags(ResourceType_Instance); 
+    }
+
+    void LookupTag(ResourceType& level,
+                   DicomTagType& type,
+                   const DicomTag& tag) const
+    {
+      Registry::const_iterator it = registry_.find(tag);
+
+      if (it == registry_.end())
+      {
+        // Default values
+        level = ResourceType_Instance;
+        type = DicomTagType_Generic;
+      }
+      else
+      {
+        level = it->second.GetLevel();
+        type = it->second.GetType();
+      }
+    }
+  };
+
+
+  void StatelessDatabaseOperations::ReadWriteTransaction::LogChange(int64_t internalId,
+                                                                    ChangeType changeType,
+                                                                    ResourceType resourceType,
+                                                                    const std::string& publicId)
+  {
+    ServerIndexChange change(changeType, resourceType, publicId);
+
+    if (changeType <= ChangeType_INTERNAL_LastLogged)
+    {
+      transaction_.LogChange(internalId, change);
+    }
+
+    GetTransactionContext().SignalChange(change);
+  }
+
+
+  SeriesStatus StatelessDatabaseOperations::ReadOnlyTransaction::GetSeriesStatus(int64_t id,
+                                                                                 int64_t expectedNumberOfInstances)
+  {
+    std::list<std::string> values;
+    transaction_.GetChildrenMetadata(values, id, MetadataType_Instance_IndexInSeries);
+
+    std::set<int64_t> instances;
+
+    for (std::list<std::string>::const_iterator
+           it = values.begin(); it != values.end(); ++it)
+    {
+      int64_t index;
+
+      try
+      {
+        index = boost::lexical_cast<int64_t>(*it);
+      }
+      catch (boost::bad_lexical_cast&)
+      {
+        return SeriesStatus_Unknown;
+      }
+      
+      if (!(index > 0 && index <= expectedNumberOfInstances))
+      {
+        // Out-of-range instance index
+        return SeriesStatus_Inconsistent;
+      }
+
+      if (instances.find(index) != instances.end())
+      {
+        // Twice the same instance index
+        return SeriesStatus_Inconsistent;
+      }
+
+      instances.insert(index);
+    }
+
+    if (static_cast<int64_t>(instances.size()) == expectedNumberOfInstances)
+    {
+      return SeriesStatus_Complete;
+    }
+    else
+    {
+      return SeriesStatus_Missing;
+    }
+  }
+
+
+  void StatelessDatabaseOperations::NormalizeLookup(std::vector<DatabaseConstraint>& target,
+                                                    const DatabaseLookup& source,
+                                                    ResourceType queryLevel) const
+  {
+    assert(mainDicomTagsRegistry_.get() != NULL);
+
+    target.clear();
+    target.reserve(source.GetConstraintsCount());
+
+    for (size_t i = 0; i < source.GetConstraintsCount(); i++)
+    {
+      ResourceType level;
+      DicomTagType type;
+      
+      mainDicomTagsRegistry_->LookupTag(level, type, source.GetConstraint(i).GetTag());
+
+      if (type == DicomTagType_Identifier ||
+          type == DicomTagType_Main)
+      {
+        // Use the fact that patient-level tags are copied at the study level
+        if (level == ResourceType_Patient &&
+            queryLevel != ResourceType_Patient)
+        {
+          level = ResourceType_Study;
+        }
+        
+        target.push_back(source.GetConstraint(i).ConvertToDatabaseConstraint(level, type));
+      }
+    }
+  }
+
+
+  class StatelessDatabaseOperations::Transaction : public boost::noncopyable
+  {
+  private:
+    IDatabaseWrapper&                                db_;
+    std::unique_ptr<IDatabaseWrapper::ITransaction>  transaction_;
+    std::unique_ptr<ITransactionContext>             context_;
+    bool                                             isCommitted_;
+    
+  public:
+    Transaction(IDatabaseWrapper& db,
+                ITransactionContextFactory& factory,
+                TransactionType type) :
+      db_(db),
+      isCommitted_(false)
+    {
+      context_.reset(factory.Create());
+      if (context_.get() == NULL)
+      {
+        throw OrthancException(ErrorCode_NullPointer);
+      }      
+      
+      transaction_.reset(db_.StartTransaction(type, *context_));
+      if (transaction_.get() == NULL)
+      {
+        throw OrthancException(ErrorCode_NullPointer);
+      }
+    }
+
+    ~Transaction()
+    {
+      if (!isCommitted_)
+      {
+        try
+        {
+          transaction_->Rollback();
+        }
+        catch (OrthancException& e)
+        {
+          LOG(INFO) << "Cannot rollback transaction: " << e.What();
+        }
+      }
+    }
+
+    IDatabaseWrapper::ITransaction& GetDatabaseTransaction()
+    {
+      assert(transaction_.get() != NULL);
+      return *transaction_;
+    }
+
+    void Commit()
+    {
+      if (isCommitted_)
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        int64_t delta = context_->GetCompressedSizeDelta();
+
+        transaction_->Commit(delta);
+        context_->Commit();
+        isCommitted_ = true;
+      }
+    }
+
+    ITransactionContext& GetContext() const
+    {
+      assert(context_.get() != NULL);
+      return *context_;
+    }
+  };
+  
+
+  void StatelessDatabaseOperations::ApplyInternal(IReadOnlyOperations* readOperations,
+                                                  IReadWriteOperations* writeOperations)
+  {
+    boost::shared_lock<boost::shared_mutex> lock(mutex_);  // To protect "factory_" and "maxRetries_"
+
+    if ((readOperations == NULL && writeOperations == NULL) ||
+        (readOperations != NULL && writeOperations != NULL))
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    if (factory_.get() == NULL)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls, "No transaction context was provided");     
+    }
+    
+    unsigned int attempt = 0;
+
+    for (;;)
+    {
+      try
+      {
+        if (readOperations != NULL)
+        {
+          /**
+           * IMPORTANT: In Orthanc <= 1.9.1, there was no transaction
+           * in this case. This was OK because of the presence of the
+           * global mutex that was protecting the database.
+           **/
+          
+          Transaction transaction(db_, *factory_, TransactionType_ReadOnly);  // TODO - Only if not "TransactionType_Implicit"
+          {
+            ReadOnlyTransaction t(transaction.GetDatabaseTransaction(), transaction.GetContext());
+            readOperations->Apply(t);
+          }
+          transaction.Commit();
+        }
+        else
+        {
+          assert(writeOperations != NULL);
+          
+          Transaction transaction(db_, *factory_, TransactionType_ReadWrite);
+          {
+            ReadWriteTransaction t(transaction.GetDatabaseTransaction(), transaction.GetContext());
+            writeOperations->Apply(t);
+          }
+          transaction.Commit();
+        }
+        
+        return;  // Success
+      }
+      catch (OrthancException& e)
+      {
+        if (e.GetErrorCode() == ErrorCode_DatabaseCannotSerialize)
+        {
+          if (attempt >= maxRetries_)
+          {
+            throw;
+          }
+          else
+          {
+            attempt++;
+
+            // The "rand()" adds some jitter to de-synchronize writers
+            boost::this_thread::sleep(boost::posix_time::milliseconds(100 * attempt + 5 * (rand() % 10)));
+          }          
+        }
+        else
+        {
+          throw;
+        }
+      }
+    }
+  }
+
+  
+  StatelessDatabaseOperations::StatelessDatabaseOperations(IDatabaseWrapper& db) : 
+    db_(db),
+    mainDicomTagsRegistry_(new MainDicomTagsRegistry),
+    hasFlushToDisk_(db.HasFlushToDisk()),
+    maxRetries_(0)
+  {
+  }
+
+
+  void StatelessDatabaseOperations::FlushToDisk()
+  {
+    try
+    {
+      db_.FlushToDisk();
+    }
+    catch (OrthancException&)
+    {
+      LOG(ERROR) << "Cannot flush the SQLite database to the disk (is your filesystem full?)";
+    }
+  }
+
+
+  void StatelessDatabaseOperations::SetTransactionContextFactory(ITransactionContextFactory* factory)
+  {
+    boost::unique_lock<boost::shared_mutex> lock(mutex_);
+
+    if (factory == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    else if (factory_.get() != NULL)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      factory_.reset(factory);
+    }
+  }
+    
+
+  void StatelessDatabaseOperations::SetMaxDatabaseRetries(unsigned int maxRetries)
+  {
+    boost::unique_lock<boost::shared_mutex> lock(mutex_);
+    maxRetries_ = maxRetries;
+  }
+  
+
+  void StatelessDatabaseOperations::Apply(IReadOnlyOperations& operations)
+  {
+    ApplyInternal(&operations, NULL);
+  }
+  
+
+  void StatelessDatabaseOperations::Apply(IReadWriteOperations& operations)
+  {
+    ApplyInternal(NULL, &operations);
+  }
+  
+
+  bool StatelessDatabaseOperations::ExpandResource(Json::Value& target,
+                                                   const std::string& publicId,
+                                                   ResourceType level)
+  {    
+    class Operations : public ReadOnlyOperationsT4<bool&, Json::Value&, const std::string&, ResourceType>
+    {
+    private:
+      static void MainDicomTagsToJson(ReadOnlyTransaction& transaction,
+                                      Json::Value& target,
+                                      int64_t resourceId,
+                                      ResourceType resourceType)
+      {
+        DicomMap tags;
+        transaction.GetMainDicomTags(tags, resourceId);
+
+        if (resourceType == ResourceType_Study)
+        {
+          DicomMap t1, t2;
+          tags.ExtractStudyInformation(t1);
+          tags.ExtractPatientInformation(t2);
+
+          target["MainDicomTags"] = Json::objectValue;
+          FromDcmtkBridge::ToJson(target["MainDicomTags"], t1, true);
+
+          target["PatientMainDicomTags"] = Json::objectValue;
+          FromDcmtkBridge::ToJson(target["PatientMainDicomTags"], t2, true);
+        }
+        else
+        {
+          target["MainDicomTags"] = Json::objectValue;
+          FromDcmtkBridge::ToJson(target["MainDicomTags"], tags, true);
+        }
+      }
+
+  
+      static bool LookupStringMetadata(std::string& result,
+                                       const std::map<MetadataType, std::string>& metadata,
+                                       MetadataType type)
+      {
+        std::map<MetadataType, std::string>::const_iterator found = metadata.find(type);
+
+        if (found == metadata.end())
+        {
+          return false;
+        }
+        else
+        {
+          result = found->second;
+          return true;
+        }
+      }
+
+
+      static bool LookupIntegerMetadata(int64_t& result,
+                                        const std::map<MetadataType, std::string>& metadata,
+                                        MetadataType type)
+      {
+        std::string s;
+        if (!LookupStringMetadata(s, metadata, type))
+        {
+          return false;
+        }
+
+        try
+        {
+          result = boost::lexical_cast<int64_t>(s);
+          return true;
+        }
+        catch (boost::bad_lexical_cast&)
+        {
+          return false;
+        }
+      }
+
+
+    public:
+      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
+                              const Tuple& tuple) ORTHANC_OVERRIDE
+      {
+        // Lookup for the requested resource
+        int64_t internalId;  // unused
+        ResourceType type;
+        std::string parent;
+        if (!transaction.LookupResourceAndParent(internalId, type, parent, tuple.get<2>()) ||
+            type != tuple.get<3>())
+        {
+          tuple.get<0>() = false;
+        }
+        else
+        {
+          Json::Value& target = tuple.get<1>();
+          target = Json::objectValue;
+        
+          // Set information about the parent resource (if it exists)
+          if (type == ResourceType_Patient)
+          {
+            if (!parent.empty())
+            {
+              throw OrthancException(ErrorCode_DatabasePlugin);
+            }
+          }
+          else
+          {
+            if (parent.empty())
+            {
+              throw OrthancException(ErrorCode_DatabasePlugin);
+            }
+
+            switch (type)
+            {
+              case ResourceType_Study:
+                target["ParentPatient"] = parent;
+                break;
+
+              case ResourceType_Series:
+                target["ParentStudy"] = parent;
+                break;
+
+              case ResourceType_Instance:
+                target["ParentSeries"] = parent;
+                break;
+
+              default:
+                throw OrthancException(ErrorCode_InternalError);
+            }
+          }
+
+          // List the children resources
+          std::list<std::string> children;
+          transaction.GetChildrenPublicId(children, internalId);
+
+          if (type != ResourceType_Instance)
+          {
+            Json::Value c = Json::arrayValue;
+
+            for (std::list<std::string>::const_iterator
+                   it = children.begin(); it != children.end(); ++it)
+            {
+              c.append(*it);
+            }
+
+            switch (type)
+            {
+              case ResourceType_Patient:
+                target["Studies"] = c;
+                break;
+
+              case ResourceType_Study:
+                target["Series"] = c;
+                break;
+
+              case ResourceType_Series:
+                target["Instances"] = c;
+                break;
+
+              default:
+                throw OrthancException(ErrorCode_InternalError);
+            }
+          }
+
+          // Extract the metadata
+          std::map<MetadataType, std::string> metadata;
+          transaction.GetAllMetadata(metadata, internalId);
+
+          // Set the resource type
+          switch (type)
+          {
+            case ResourceType_Patient:
+              target["Type"] = "Patient";
+              break;
+
+            case ResourceType_Study:
+              target["Type"] = "Study";
+              break;
+
+            case ResourceType_Series:
+            {
+              target["Type"] = "Series";
+
+              int64_t i;
+              if (LookupIntegerMetadata(i, metadata, MetadataType_Series_ExpectedNumberOfInstances))
+              {
+                target["ExpectedNumberOfInstances"] = static_cast<int>(i);
+                target["Status"] = EnumerationToString(transaction.GetSeriesStatus(internalId, i));
+              }
+              else
+              {
+                target["ExpectedNumberOfInstances"] = Json::nullValue;
+                target["Status"] = EnumerationToString(SeriesStatus_Unknown);
+              }
+
+              break;
+            }
+
+            case ResourceType_Instance:
+            {
+              target["Type"] = "Instance";
+
+              FileInfo attachment;
+              int64_t revision;  // ignored
+              if (!transaction.LookupAttachment(attachment, revision, internalId, FileContentType_Dicom))
+              {
+                throw OrthancException(ErrorCode_InternalError);
+              }
+
+              target["FileSize"] = static_cast<unsigned int>(attachment.GetUncompressedSize());
+              target["FileUuid"] = attachment.GetUuid();
+
+              int64_t i;
+              if (LookupIntegerMetadata(i, metadata, MetadataType_Instance_IndexInSeries))
+              {
+                target["IndexInSeries"] = static_cast<int>(i);
+              }
+              else
+              {
+                target["IndexInSeries"] = Json::nullValue;
+              }
+
+              break;
+            }
+
+            default:
+              throw OrthancException(ErrorCode_InternalError);
+          }
+
+          // Record the remaining information
+          target["ID"] = tuple.get<2>();
+          MainDicomTagsToJson(transaction, target, internalId, type);
+
+          std::string tmp;
+
+          if (LookupStringMetadata(tmp, metadata, MetadataType_AnonymizedFrom))
+          {
+            target["AnonymizedFrom"] = tmp;
+          }
+
+          if (LookupStringMetadata(tmp, metadata, MetadataType_ModifiedFrom))
+          {
+            target["ModifiedFrom"] = tmp;
+          }
+
+          if (type == ResourceType_Patient ||
+              type == ResourceType_Study ||
+              type == ResourceType_Series)
+          {
+            target["IsStable"] = !transaction.GetTransactionContext().IsUnstableResource(internalId);
+
+            if (LookupStringMetadata(tmp, metadata, MetadataType_LastUpdate))
+            {
+              target["LastUpdate"] = tmp;
+            }
+          }
+
+          tuple.get<0>() = true;
+        }
+      }
+    };
+
+    bool found;
+    Operations operations;
+    operations.Apply(*this, found, target, publicId, level);
+    return found;
+  }
+
+
+  void StatelessDatabaseOperations::GetAllMetadata(std::map<MetadataType, std::string>& target,
+                                                   const std::string& publicId,
+                                                   ResourceType level)
+  {
+    class Operations : public ReadOnlyOperationsT3<std::map<MetadataType, std::string>&, const std::string&, ResourceType>
+    {
+    public:
+      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
+                              const Tuple& tuple) ORTHANC_OVERRIDE
+      {
+        ResourceType type;
+        int64_t id;
+        if (!transaction.LookupResource(id, type, tuple.get<1>()) ||
+            tuple.get<2>() != type)
+        {
+          throw OrthancException(ErrorCode_UnknownResource);
+        }
+        else
+        {
+          transaction.GetAllMetadata(tuple.get<0>(), id);
+        }
+      }
+    };
+
+    Operations operations;
+    operations.Apply(*this, target, publicId, level);
+  }
+
+
+  bool StatelessDatabaseOperations::LookupAttachment(FileInfo& attachment,
+                                                     int64_t& revision,
+                                                     const std::string& instancePublicId,
+                                                     FileContentType contentType)
+  {
+    class Operations : public ReadOnlyOperationsT5<bool&, FileInfo&, int64_t&, const std::string&, FileContentType>
+    {
+    public:
+      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
+                              const Tuple& tuple) ORTHANC_OVERRIDE
+      {
+        int64_t internalId;
+        ResourceType type;
+        if (!transaction.LookupResource(internalId, type, tuple.get<3>()))
+        {
+          throw OrthancException(ErrorCode_UnknownResource);
+        }
+        else if (transaction.LookupAttachment(tuple.get<1>(), tuple.get<2>(), internalId, tuple.get<4>()))
+        {
+          assert(tuple.get<1>().GetContentType() == tuple.get<4>());
+          tuple.get<0>() = true;
+        }
+        else
+        {
+          tuple.get<0>() = false;
+        }
+      }
+    };
+
+    bool found;
+    Operations operations;
+    operations.Apply(*this, found, attachment, revision, instancePublicId, contentType);
+    return found;
+  }
+
+
+  void StatelessDatabaseOperations::GetAllUuids(std::list<std::string>& target,
+                                                ResourceType resourceType)
+  {
+    class Operations : public ReadOnlyOperationsT2<std::list<std::string>&, ResourceType>
+    {
+    public:
+      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
+                              const Tuple& tuple) ORTHANC_OVERRIDE
+      {
+        // TODO - CANDIDATE FOR "TransactionType_Implicit"
+        transaction.GetAllPublicIds(tuple.get<0>(), tuple.get<1>());
+      }
+    };
+
+    Operations operations;
+    operations.Apply(*this, target, resourceType);
+  }
+
+
+  void StatelessDatabaseOperations::GetAllUuids(std::list<std::string>& target,
+                                                ResourceType resourceType,
+                                                size_t since,
+                                                size_t limit)
+  {
+    if (limit == 0)
+    {
+      target.clear();
+    }
+    else
+    {
+      class Operations : public ReadOnlyOperationsT4<std::list<std::string>&, ResourceType, size_t, size_t>
+      {
+      public:
+        virtual void ApplyTuple(ReadOnlyTransaction& transaction,
+                                const Tuple& tuple) ORTHANC_OVERRIDE
+        {
+          // TODO - CANDIDATE FOR "TransactionType_Implicit"
+          transaction.GetAllPublicIds(tuple.get<0>(), tuple.get<1>(), tuple.get<2>(), tuple.get<3>());
+        }
+      };
+
+      Operations operations;
+      operations.Apply(*this, target, resourceType, since, limit);
+    }
+  }
+
+
+  void StatelessDatabaseOperations::GetGlobalStatistics(/* out */ uint64_t& diskSize,
+                                                        /* out */ uint64_t& uncompressedSize,
+                                                        /* out */ uint64_t& countPatients, 
+                                                        /* out */ uint64_t& countStudies, 
+                                                        /* out */ uint64_t& countSeries, 
+                                                        /* out */ uint64_t& countInstances)
+  {
+    class Operations : public ReadOnlyOperationsT6<uint64_t&, uint64_t&, uint64_t&, uint64_t&, uint64_t&, uint64_t&>
+    {
+    public:
+      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
+                              const Tuple& tuple) ORTHANC_OVERRIDE
+      {
+        tuple.get<0>() = transaction.GetTotalCompressedSize();
+        tuple.get<1>() = transaction.GetTotalUncompressedSize();
+        tuple.get<2>() = transaction.GetResourcesCount(ResourceType_Patient);
+        tuple.get<3>() = transaction.GetResourcesCount(ResourceType_Study);
+        tuple.get<4>() = transaction.GetResourcesCount(ResourceType_Series);
+        tuple.get<5>() = transaction.GetResourcesCount(ResourceType_Instance);
+      }
+    };
+    
+    Operations operations;
+    operations.Apply(*this, diskSize, uncompressedSize, countPatients,
+                     countStudies, countSeries, countInstances);
+  }
+
+
+  void StatelessDatabaseOperations::GetChanges(Json::Value& target,
+                                               int64_t since,                               
+                                               unsigned int maxResults)
+  {
+    class Operations : public ReadOnlyOperationsT3<Json::Value&, int64_t, unsigned int>
+    {
+    public:
+      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
+                              const Tuple& tuple) ORTHANC_OVERRIDE
+      {
+        // NB: In Orthanc <= 1.3.2, a transaction was missing, as
+        // "GetLastChange()" involves calls to "GetPublicId()"
+
+        std::list<ServerIndexChange> changes;
+        bool done;
+        bool hasLast = false;
+        int64_t last = 0;
+
+        transaction.GetChanges(changes, done, tuple.get<1>(), tuple.get<2>());
+        if (changes.empty())
+        {
+          last = transaction.GetLastChangeIndex();
+          hasLast = true;
+        }
+
+        FormatLog(tuple.get<0>(), changes, "Changes", done, tuple.get<1>(), hasLast, last);
+      }
+    };
+    
+    Operations operations;
+    operations.Apply(*this, target, since, maxResults);
+  }
+
+
+  void StatelessDatabaseOperations::GetLastChange(Json::Value& target)
+  {
+    class Operations : public ReadOnlyOperationsT1<Json::Value&>
+    {
+    public:
+      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
+                              const Tuple& tuple) ORTHANC_OVERRIDE
+      {
+        // NB: In Orthanc <= 1.3.2, a transaction was missing, as
+        // "GetLastChange()" involves calls to "GetPublicId()"
+
+        std::list<ServerIndexChange> changes;
+        bool hasLast = false;
+        int64_t last = 0;
+
+        transaction.GetLastChange(changes);
+        if (changes.empty())
+        {
+          last = transaction.GetLastChangeIndex();
+          hasLast = true;
+        }
+
+        FormatLog(tuple.get<0>(), changes, "Changes", true, 0, hasLast, last);
+      }
+    };
+    
+    Operations operations;
+    operations.Apply(*this, target);
+  }
+
+
+  void StatelessDatabaseOperations::GetExportedResources(Json::Value& target,
+                                                         int64_t since,
+                                                         unsigned int maxResults)
+  {
+    class Operations : public ReadOnlyOperationsT3<Json::Value&, int64_t, unsigned int>
+    {
+    public:
+      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
+                              const Tuple& tuple) ORTHANC_OVERRIDE
+      {
+        // TODO - CANDIDATE FOR "TransactionType_Implicit"
+
+        std::list<ExportedResource> exported;
+        bool done;
+        transaction.GetExportedResources(exported, done, tuple.get<1>(), tuple.get<2>());
+        FormatLog(tuple.get<0>(), exported, "Exports", done, tuple.get<1>(), false, -1);
+      }
+    };
+    
+    Operations operations;
+    operations.Apply(*this, target, since, maxResults);
+  }
+
+
+  void StatelessDatabaseOperations::GetLastExportedResource(Json::Value& target)
+  {
+    class Operations : public ReadOnlyOperationsT1<Json::Value&>
+    {
+    public:
+      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
+                              const Tuple& tuple) ORTHANC_OVERRIDE
+      {
+        // TODO - CANDIDATE FOR "TransactionType_Implicit"
+
+        std::list<ExportedResource> exported;
+        transaction.GetLastExportedResource(exported);
+        FormatLog(tuple.get<0>(), exported, "Exports", true, 0, false, -1);
+      }
+    };
+    
+    Operations operations;
+    operations.Apply(*this, target);
+  }
+
+
+  bool StatelessDatabaseOperations::IsProtectedPatient(const std::string& publicId)
+  {
+    class Operations : public ReadOnlyOperationsT2<bool&, const std::string&>
+    {
+    public:
+      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
+                              const Tuple& tuple) ORTHANC_OVERRIDE
+      {
+        // Lookup for the requested resource
+        int64_t id;
+        ResourceType type;
+        if (!transaction.LookupResource(id, type, tuple.get<1>()) ||
+            type != ResourceType_Patient)
+        {
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+        }
+        else
+        {
+          tuple.get<0>() = transaction.IsProtectedPatient(id);
+        }
+      }
+    };
+
+    bool isProtected;
+    Operations operations;
+    operations.Apply(*this, isProtected, publicId);
+    return isProtected;
+  }
+
+
+  void StatelessDatabaseOperations::GetChildren(std::list<std::string>& result,
+                                                const std::string& publicId)
+  {
+    class Operations : public ReadOnlyOperationsT2<std::list<std::string>&, const std::string&>
+    {
+    public:
+      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
+                              const Tuple& tuple) ORTHANC_OVERRIDE
+      {
+        ResourceType type;
+        int64_t resource;
+        if (!transaction.LookupResource(resource, type, tuple.get<1>()))
+        {
+          throw OrthancException(ErrorCode_UnknownResource);
+        }
+        else if (type == ResourceType_Instance)
+        {
+          // An instance cannot have a child
+          throw OrthancException(ErrorCode_BadParameterType);
+        }
+        else
+        {
+          std::list<int64_t> tmp;
+          transaction.GetChildrenInternalId(tmp, resource);
+
+          tuple.get<0>().clear();
+
+          for (std::list<int64_t>::const_iterator 
+                 it = tmp.begin(); it != tmp.end(); ++it)
+          {
+            tuple.get<0>().push_back(transaction.GetPublicId(*it));
+          }
+        }
+      }
+    };
+    
+    Operations operations;
+    operations.Apply(*this, result, publicId);
+  }
+
+
+  void StatelessDatabaseOperations::GetChildInstances(std::list<std::string>& result,
+                                                      const std::string& publicId)
+  {
+    class Operations : public ReadOnlyOperationsT2<std::list<std::string>&, const std::string&>
+    {
+    public:
+      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
+                              const Tuple& tuple) ORTHANC_OVERRIDE
+      {
+        tuple.get<0>().clear();
+        
+        ResourceType type;
+        int64_t top;
+        if (!transaction.LookupResource(top, type, tuple.get<1>()))
+        {
+          throw OrthancException(ErrorCode_UnknownResource);
+        }
+        else if (type == ResourceType_Instance)
+        {
+          // The resource is already an instance: Do not go down the hierarchy
+          tuple.get<0>().push_back(tuple.get<1>());
+        }
+        else
+        {
+          std::stack<int64_t> toExplore;
+          toExplore.push(top);
+
+          std::list<int64_t> tmp;
+          while (!toExplore.empty())
+          {
+            // Get the internal ID of the current resource
+            int64_t resource = toExplore.top();
+            toExplore.pop();
+
+            // TODO - This could be optimized by seeing how many
+            // levels "type == transaction.GetResourceType(top)" is
+            // above the "instances level"
+            if (transaction.GetResourceType(resource) == ResourceType_Instance)
+            {
+              tuple.get<0>().push_back(transaction.GetPublicId(resource));
+            }
+            else
+            {
+              // Tag all the children of this resource as to be explored
+              transaction.GetChildrenInternalId(tmp, resource);
+              for (std::list<int64_t>::const_iterator 
+                     it = tmp.begin(); it != tmp.end(); ++it)
+              {
+                toExplore.push(*it);
+              }
+            }
+          }
+        }
+      }
+    };
+    
+    Operations operations;
+    operations.Apply(*this, result, publicId);
+  }
+
+
+  bool StatelessDatabaseOperations::LookupMetadata(std::string& target,
+                                                   int64_t& revision,
+                                                   const std::string& publicId,
+                                                   ResourceType expectedType,
+                                                   MetadataType type)
+  {
+    class Operations : public ReadOnlyOperationsT6<bool&, std::string&, int64_t&,
+                                                   const std::string&, ResourceType, MetadataType>
+    {
+    public:
+      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
+                              const Tuple& tuple) ORTHANC_OVERRIDE
+      {
+        ResourceType resourceType;
+        int64_t id;
+        if (!transaction.LookupResource(id, resourceType, tuple.get<3>()) ||
+            resourceType != tuple.get<4>())
+        {
+          throw OrthancException(ErrorCode_UnknownResource);
+        }
+        else
+        {
+          tuple.get<0>() = transaction.LookupMetadata(tuple.get<1>(), tuple.get<2>(), id, tuple.get<5>());
+        }
+      }
+    };
+
+    bool found;
+    Operations operations;
+    operations.Apply(*this, found, target, revision, publicId, expectedType, type);
+    return found;
+  }
+
+
+  void StatelessDatabaseOperations::ListAvailableAttachments(std::set<FileContentType>& target,
+                                                             const std::string& publicId,
+                                                             ResourceType expectedType)
+  {
+    class Operations : public ReadOnlyOperationsT3<std::set<FileContentType>&, const std::string&, ResourceType>
+    {
+    public:
+      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
+                              const Tuple& tuple) ORTHANC_OVERRIDE
+      {
+        ResourceType type;
+        int64_t id;
+        if (!transaction.LookupResource(id, type, tuple.get<1>()) ||
+            tuple.get<2>() != type)
+        {
+          throw OrthancException(ErrorCode_UnknownResource);
+        }
+        else
+        {
+          transaction.ListAvailableAttachments(tuple.get<0>(), id);
+        }
+      }
+    };
+    
+    Operations operations;
+    operations.Apply(*this, target, publicId, expectedType);
+  }
+
+
+  bool StatelessDatabaseOperations::LookupParent(std::string& target,
+                                                 const std::string& publicId)
+  {
+    class Operations : public ReadOnlyOperationsT3<bool&, std::string&, const std::string&>
+    {
+    public:
+      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
+                              const Tuple& tuple) ORTHANC_OVERRIDE
+      {
+        ResourceType type;
+        int64_t id;
+        if (!transaction.LookupResource(id, type, tuple.get<2>()))
+        {
+          throw OrthancException(ErrorCode_UnknownResource);
+        }
+        else
+        {
+          int64_t parentId;
+          if (transaction.LookupParent(parentId, id))
+          {
+            tuple.get<1>() = transaction.GetPublicId(parentId);
+            tuple.get<0>() = true;
+          }
+          else
+          {
+            tuple.get<0>() = false;
+          }
+        }
+      }
+    };
+
+    bool found;
+    Operations operations;
+    operations.Apply(*this, found, target, publicId);
+    return found;
+  }
+
+
+  void StatelessDatabaseOperations::GetResourceStatistics(/* out */ ResourceType& type,
+                                                          /* out */ uint64_t& diskSize, 
+                                                          /* out */ uint64_t& uncompressedSize, 
+                                                          /* out */ unsigned int& countStudies, 
+                                                          /* out */ unsigned int& countSeries, 
+                                                          /* out */ unsigned int& countInstances, 
+                                                          /* out */ uint64_t& dicomDiskSize, 
+                                                          /* out */ uint64_t& dicomUncompressedSize, 
+                                                          const std::string& publicId)
+  {
+    class Operations : public IReadOnlyOperations
+    {
+    private:
+      ResourceType&      type_;
+      uint64_t&          diskSize_; 
+      uint64_t&          uncompressedSize_; 
+      unsigned int&      countStudies_; 
+      unsigned int&      countSeries_; 
+      unsigned int&      countInstances_; 
+      uint64_t&          dicomDiskSize_; 
+      uint64_t&          dicomUncompressedSize_; 
+      const std::string& publicId_;
+        
+    public:
+      explicit Operations(ResourceType& type,
+                          uint64_t& diskSize, 
+                          uint64_t& uncompressedSize, 
+                          unsigned int& countStudies, 
+                          unsigned int& countSeries, 
+                          unsigned int& countInstances, 
+                          uint64_t& dicomDiskSize, 
+                          uint64_t& dicomUncompressedSize, 
+                          const std::string& publicId) :
+        type_(type),
+        diskSize_(diskSize),
+        uncompressedSize_(uncompressedSize),
+        countStudies_(countStudies),
+        countSeries_(countSeries),
+        countInstances_(countInstances),
+        dicomDiskSize_(dicomDiskSize),
+        dicomUncompressedSize_(dicomUncompressedSize),
+        publicId_(publicId)
+      {
+      }
+      
+      virtual void Apply(ReadOnlyTransaction& transaction) ORTHANC_OVERRIDE
+      {
+        int64_t top;
+        if (!transaction.LookupResource(top, type_, publicId_))
+        {
+          throw OrthancException(ErrorCode_UnknownResource);
+        }
+        else
+        {
+          countInstances_ = 0;
+          countSeries_ = 0;
+          countStudies_ = 0;
+          diskSize_ = 0;
+          uncompressedSize_ = 0;
+          dicomDiskSize_ = 0;
+          dicomUncompressedSize_ = 0;
+
+          std::stack<int64_t> toExplore;
+          toExplore.push(top);
+
+          while (!toExplore.empty())
+          {
+            // Get the internal ID of the current resource
+            int64_t resource = toExplore.top();
+            toExplore.pop();
+
+            ResourceType thisType = transaction.GetResourceType(resource);
+
+            std::set<FileContentType> f;
+            transaction.ListAvailableAttachments(f, resource);
+
+            for (std::set<FileContentType>::const_iterator
+                   it = f.begin(); it != f.end(); ++it)
+            {
+              FileInfo attachment;
+              int64_t revision;  // ignored
+              if (transaction.LookupAttachment(attachment, revision, resource, *it))
+              {
+                if (attachment.GetContentType() == FileContentType_Dicom)
+                {
+                  dicomDiskSize_ += attachment.GetCompressedSize();
+                  dicomUncompressedSize_ += attachment.GetUncompressedSize();
+                }
+          
+                diskSize_ += attachment.GetCompressedSize();
+                uncompressedSize_ += attachment.GetUncompressedSize();
+              }
+            }
+
+            if (thisType == ResourceType_Instance)
+            {
+              countInstances_++;
+            }
+            else
+            {
+              switch (thisType)
+              {
+                case ResourceType_Study:
+                  countStudies_++;
+                  break;
+
+                case ResourceType_Series:
+                  countSeries_++;
+                  break;
+
+                default:
+                  break;
+              }
+
+              // Tag all the children of this resource as to be explored
+              std::list<int64_t> tmp;
+              transaction.GetChildrenInternalId(tmp, resource);
+              for (std::list<int64_t>::const_iterator 
+                     it = tmp.begin(); it != tmp.end(); ++it)
+              {
+                toExplore.push(*it);
+              }
+            }
+          }
+
+          if (countStudies_ == 0)
+          {
+            countStudies_ = 1;
+          }
+
+          if (countSeries_ == 0)
+          {
+            countSeries_ = 1;
+          }
+        }
+      }
+    };
+
+    Operations operations(type, diskSize, uncompressedSize, countStudies, countSeries,
+                          countInstances, dicomDiskSize, dicomUncompressedSize, publicId);
+    Apply(operations);
+  }
+
+
+  void StatelessDatabaseOperations::LookupIdentifierExact(std::vector<std::string>& result,
+                                                          ResourceType level,
+                                                          const DicomTag& tag,
+                                                          const std::string& value)
+  {
+    assert((level == ResourceType_Patient && tag == DICOM_TAG_PATIENT_ID) ||
+           (level == ResourceType_Study && tag == DICOM_TAG_STUDY_INSTANCE_UID) ||
+           (level == ResourceType_Study && tag == DICOM_TAG_ACCESSION_NUMBER) ||
+           (level == ResourceType_Series && tag == DICOM_TAG_SERIES_INSTANCE_UID) ||
+           (level == ResourceType_Instance && tag == DICOM_TAG_SOP_INSTANCE_UID));
+    
+    result.clear();
+
+    DicomTagConstraint c(tag, ConstraintType_Equal, value, true, true);
+
+    std::vector<DatabaseConstraint> query;
+    query.push_back(c.ConvertToDatabaseConstraint(level, DicomTagType_Identifier));
+
+
+    class Operations : public IReadOnlyOperations
+    {
+    private:
+      std::vector<std::string>&               result_;
+      const std::vector<DatabaseConstraint>&  query_;
+      ResourceType                            level_;
+      
+    public:
+      Operations(std::vector<std::string>& result,
+                 const std::vector<DatabaseConstraint>& query,
+                 ResourceType level) :
+        result_(result),
+        query_(query),
+        level_(level)
+      {
+      }
+
+      virtual void Apply(ReadOnlyTransaction& transaction) ORTHANC_OVERRIDE
+      {
+        // TODO - CANDIDATE FOR "TransactionType_Implicit"
+        std::list<std::string> tmp;
+        transaction.ApplyLookupResources(tmp, NULL, query_, level_, 0);
+        CopyListToVector(result_, tmp);
+      }
+    };
+
+    Operations operations(result, query, level);
+    Apply(operations);
+  }
+
+
+  bool StatelessDatabaseOperations::LookupGlobalProperty(std::string& value,
+                                                         GlobalProperty property,
+                                                         bool shared)
+  {
+    class Operations : public ReadOnlyOperationsT4<bool&, std::string&, GlobalProperty, bool>
+    {
+    public:
+      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
+                              const Tuple& tuple) ORTHANC_OVERRIDE
+      {
+        // TODO - CANDIDATE FOR "TransactionType_Implicit"
+        tuple.get<0>() = transaction.LookupGlobalProperty(tuple.get<1>(), tuple.get<2>(), tuple.get<3>());
+      }
+    };
+
+    bool found;
+    Operations operations;
+    operations.Apply(*this, found, value, property, shared);
+    return found;
+  }
+  
+
+  std::string StatelessDatabaseOperations::GetGlobalProperty(GlobalProperty property,
+                                                             bool shared,
+                                                             const std::string& defaultValue)
+  {
+    std::string s;
+    if (LookupGlobalProperty(s, property, shared))
+    {
+      return s;
+    }
+    else
+    {
+      return defaultValue;
+    }
+  }
+
+
+  bool StatelessDatabaseOperations::GetMainDicomTags(DicomMap& result,
+                                                     const std::string& publicId,
+                                                     ResourceType expectedType,
+                                                     ResourceType levelOfInterest)
+  {
+    // Yes, the following test could be shortened, but we wish to make it as clear as possible
+    if (!(expectedType == ResourceType_Patient  && levelOfInterest == ResourceType_Patient) &&
+        !(expectedType == ResourceType_Study    && levelOfInterest == ResourceType_Patient) &&
+        !(expectedType == ResourceType_Study    && levelOfInterest == ResourceType_Study)   &&
+        !(expectedType == ResourceType_Series   && levelOfInterest == ResourceType_Series)  &&
+        !(expectedType == ResourceType_Instance && levelOfInterest == ResourceType_Instance))
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+
+    class Operations : public ReadOnlyOperationsT5<bool&, DicomMap&, const std::string&, ResourceType, ResourceType>
+    {
+    public:
+      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
+                              const Tuple& tuple) ORTHANC_OVERRIDE
+      {
+        // Lookup for the requested resource
+        int64_t id;
+        ResourceType type;
+        if (!transaction.LookupResource(id, type, tuple.get<2>()) ||
+            type != tuple.get<3>())
+        {
+          tuple.get<0>() = false;
+        }
+        else if (type == ResourceType_Study)
+        {
+          DicomMap tmp;
+          transaction.GetMainDicomTags(tmp, id);
+
+          switch (tuple.get<4>())
+          {
+            case ResourceType_Patient:
+              tmp.ExtractPatientInformation(tuple.get<1>());
+              tuple.get<0>() = true;
+              break;
+
+            case ResourceType_Study:
+              tmp.ExtractStudyInformation(tuple.get<1>());
+              tuple.get<0>() = true;
+              break;
+
+            default:
+              throw OrthancException(ErrorCode_InternalError);
+          }
+        }
+        else
+        {
+          transaction.GetMainDicomTags(tuple.get<1>(), id);
+          tuple.get<0>() = true;
+        }    
+      }
+    };
+
+    result.Clear();
+
+    bool found;
+    Operations operations;
+    operations.Apply(*this, found, result, publicId, expectedType, levelOfInterest);
+    return found;
+  }
+
+
+  bool StatelessDatabaseOperations::GetAllMainDicomTags(DicomMap& result,
+                                                        const std::string& instancePublicId)
+  {
+    class Operations : public ReadOnlyOperationsT3<bool&, DicomMap&, const std::string&>
+    {
+    public:
+      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
+                              const Tuple& tuple) ORTHANC_OVERRIDE
+      {
+        // Lookup for the requested resource
+        int64_t instance;
+        ResourceType type;
+        if (!transaction.LookupResource(instance, type, tuple.get<2>()) ||
+            type != ResourceType_Instance)
+        {
+          tuple.get<0>() =  false;
+        }
+        else
+        {
+          DicomMap tmp;
+
+          transaction.GetMainDicomTags(tmp, instance);
+          tuple.get<1>().Merge(tmp);
+
+          int64_t series;
+          if (!transaction.LookupParent(series, instance))
+          {
+            throw OrthancException(ErrorCode_InternalError);
+          }
+
+          tmp.Clear();
+          transaction.GetMainDicomTags(tmp, series);
+          tuple.get<1>().Merge(tmp);
+
+          int64_t study;
+          if (!transaction.LookupParent(study, series))
+          {
+            throw OrthancException(ErrorCode_InternalError);
+          }
+
+          tmp.Clear();
+          transaction.GetMainDicomTags(tmp, study);
+          tuple.get<1>().Merge(tmp);
+
+#ifndef NDEBUG
+          {
+            // Sanity test to check that all the main DICOM tags from the
+            // patient level are copied at the study level
+        
+            int64_t patient;
+            if (!transaction.LookupParent(patient, study))
+            {
+              throw OrthancException(ErrorCode_InternalError);
+            }
+
+            tmp.Clear();
+            transaction.GetMainDicomTags(tmp, study);
+
+            std::set<DicomTag> patientTags;
+            tmp.GetTags(patientTags);
+
+            for (std::set<DicomTag>::const_iterator
+                   it = patientTags.begin(); it != patientTags.end(); ++it)
+            {
+              assert(tuple.get<1>().HasTag(*it));
+            }
+          }
+#endif
+      
+          tuple.get<0>() =  true;
+        }
+      }
+    };
+
+    result.Clear();
+    
+    bool found;
+    Operations operations;
+    operations.Apply(*this, found, result, instancePublicId);
+    return found;
+  }
+
+
+  bool StatelessDatabaseOperations::LookupResourceType(ResourceType& type,
+                                                       const std::string& publicId)
+  {
+    class Operations : public ReadOnlyOperationsT3<bool&, ResourceType&, const std::string&>
+    {
+    public:
+      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
+                              const Tuple& tuple) ORTHANC_OVERRIDE
+      {
+        // TODO - CANDIDATE FOR "TransactionType_Implicit"
+        int64_t id;
+        tuple.get<0>() = transaction.LookupResource(id, tuple.get<1>(), tuple.get<2>());
+      }
+    };
+
+    bool found;
+    Operations operations;
+    operations.Apply(*this, found, type, publicId);
+    return found;
+  }
+
+
+  bool StatelessDatabaseOperations::LookupParent(std::string& target,
+                                                 const std::string& publicId,
+                                                 ResourceType parentType)
+  {
+    class Operations : public ReadOnlyOperationsT4<bool&, std::string&, const std::string&, ResourceType>
+    {
+    public:
+      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
+                              const Tuple& tuple) ORTHANC_OVERRIDE
+      {
+        ResourceType type;
+        int64_t id;
+        if (!transaction.LookupResource(id, type, tuple.get<2>()))
+        {
+          throw OrthancException(ErrorCode_UnknownResource);
+        }
+
+        while (type != tuple.get<3>())
+        {
+          int64_t parentId;
+
+          if (type == ResourceType_Patient ||    // Cannot further go up in hierarchy
+              !transaction.LookupParent(parentId, id))
+          {
+            tuple.get<0>() = false;
+            return;
+          }
+
+          id = parentId;
+          type = GetParentResourceType(type);
+        }
+
+        tuple.get<0>() = true;
+        tuple.get<1>() = transaction.GetPublicId(id);
+      }
+    };
+
+    bool found;
+    Operations operations;
+    operations.Apply(*this, found, target, publicId, parentType);
+    return found;
+  }
+
+
+  void StatelessDatabaseOperations::ApplyLookupResources(std::vector<std::string>& resourcesId,
+                                                         std::vector<std::string>* instancesId,
+                                                         const DatabaseLookup& lookup,
+                                                         ResourceType queryLevel,
+                                                         size_t limit)
+  {
+    class Operations : public ReadOnlyOperationsT4<bool, const std::vector<DatabaseConstraint>&, ResourceType, size_t>
+    {
+    private:
+      std::list<std::string>  resourcesList_;
+      std::list<std::string>  instancesList_;
+      
+    public:
+      const std::list<std::string>& GetResourcesList() const
+      {
+        return resourcesList_;
+      }
+
+      const std::list<std::string>& GetInstancesList() const
+      {
+        return instancesList_;
+      }
+
+      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
+                              const Tuple& tuple) ORTHANC_OVERRIDE
+      {
+        // TODO - CANDIDATE FOR "TransactionType_Implicit"
+        if (tuple.get<0>())
+        {
+          transaction.ApplyLookupResources(resourcesList_, &instancesList_, tuple.get<1>(), tuple.get<2>(), tuple.get<3>());
+        }
+        else
+        {
+          transaction.ApplyLookupResources(resourcesList_, NULL, tuple.get<1>(), tuple.get<2>(), tuple.get<3>());
+        }
+      }
+    };
+
+
+    std::vector<DatabaseConstraint> normalized;
+    NormalizeLookup(normalized, lookup, queryLevel);
+
+    Operations operations;
+    operations.Apply(*this, (instancesId != NULL), normalized, queryLevel, limit);
+    
+    CopyListToVector(resourcesId, operations.GetResourcesList());
+
+    if (instancesId != NULL)
+    { 
+      CopyListToVector(*instancesId, operations.GetInstancesList());
+    }
+  }
+
+
+  bool StatelessDatabaseOperations::DeleteResource(Json::Value& target,
+                                                   const std::string& uuid,
+                                                   ResourceType expectedType)
+  {
+    class Operations : public IReadWriteOperations
+    {
+    private:
+      bool                found_;
+      Json::Value&        target_;
+      const std::string&  uuid_;
+      ResourceType        expectedType_;
+      
+    public:
+      Operations(Json::Value& target,
+                 const std::string& uuid,
+                 ResourceType expectedType) :
+        found_(false),
+        target_(target),
+        uuid_(uuid),
+        expectedType_(expectedType)
+      {
+      }
+
+      bool IsFound() const
+      {
+        return found_;
+      }
+
+      virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE
+      {
+        int64_t id;
+        ResourceType type;
+        if (!transaction.LookupResource(id, type, uuid_) ||
+            expectedType_ != type)
+        {
+          found_ = false;
+        }
+        else
+        {
+          found_ = true;
+          transaction.DeleteResource(id);
+
+          std::string remainingPublicId;
+          ResourceType remainingLevel;
+          if (transaction.GetTransactionContext().LookupRemainingLevel(remainingPublicId, remainingLevel))
+          {
+            target_["RemainingAncestor"] = Json::Value(Json::objectValue);
+            target_["RemainingAncestor"]["Path"] = GetBasePath(remainingLevel, remainingPublicId);
+            target_["RemainingAncestor"]["Type"] = EnumerationToString(remainingLevel);
+            target_["RemainingAncestor"]["ID"] = remainingPublicId;
+          }
+          else
+          {
+            target_["RemainingAncestor"] = Json::nullValue;
+          }
+        }
+      }
+    };
+
+    Operations operations(target, uuid, expectedType);
+    Apply(operations);
+    return operations.IsFound();
+  }
+
+
+  void StatelessDatabaseOperations::LogExportedResource(const std::string& publicId,
+                                                        const std::string& remoteModality)
+  {
+    class Operations : public IReadWriteOperations
+    {
+    private:
+      const std::string&  publicId_;
+      const std::string&  remoteModality_;
+
+    public:
+      Operations(const std::string& publicId,
+                 const std::string& remoteModality) :
+        publicId_(publicId),
+        remoteModality_(remoteModality)
+      {
+      }
+      
+      virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE
+      {
+        int64_t id;
+        ResourceType type;
+        if (!transaction.LookupResource(id, type, publicId_))
+        {
+          throw OrthancException(ErrorCode_InexistentItem);
+        }
+
+        std::string patientId;
+        std::string studyInstanceUid;
+        std::string seriesInstanceUid;
+        std::string sopInstanceUid;
+
+        int64_t currentId = id;
+        ResourceType currentType = type;
+
+        // Iteratively go up inside the patient/study/series/instance hierarchy
+        bool done = false;
+        while (!done)
+        {
+          DicomMap map;
+          transaction.GetMainDicomTags(map, currentId);
+
+          switch (currentType)
+          {
+            case ResourceType_Patient:
+              if (map.HasTag(DICOM_TAG_PATIENT_ID))
+              {
+                patientId = map.GetValue(DICOM_TAG_PATIENT_ID).GetContent();
+              }
+              done = true;
+              break;
+
+            case ResourceType_Study:
+              if (map.HasTag(DICOM_TAG_STUDY_INSTANCE_UID))
+              {
+                studyInstanceUid = map.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).GetContent();
+              }
+              currentType = ResourceType_Patient;
+              break;
+
+            case ResourceType_Series:
+              if (map.HasTag(DICOM_TAG_SERIES_INSTANCE_UID))
+              {
+                seriesInstanceUid = map.GetValue(DICOM_TAG_SERIES_INSTANCE_UID).GetContent();
+              }
+              currentType = ResourceType_Study;
+              break;
+
+            case ResourceType_Instance:
+              if (map.HasTag(DICOM_TAG_SOP_INSTANCE_UID))
+              {
+                sopInstanceUid = map.GetValue(DICOM_TAG_SOP_INSTANCE_UID).GetContent();
+              }
+              currentType = ResourceType_Series;
+              break;
+
+            default:
+              throw OrthancException(ErrorCode_InternalError);
+          }
+
+          // If we have not reached the Patient level, find the parent of
+          // the current resource
+          if (!done)
+          {
+            bool ok = transaction.LookupParent(currentId, currentId);
+            (void) ok;  // Remove warning about unused variable in release builds
+            assert(ok);
+          }
+        }
+
+        ExportedResource resource(-1, 
+                                  type,
+                                  publicId_,
+                                  remoteModality_,
+                                  SystemToolbox::GetNowIsoString(true /* use UTC time (not local time) */),
+                                  patientId,
+                                  studyInstanceUid,
+                                  seriesInstanceUid,
+                                  sopInstanceUid);
+
+        transaction.LogExportedResource(resource);
+      }
+    };
+
+    Operations operations(publicId, remoteModality);
+    Apply(operations);
+  }
+
+
+  void StatelessDatabaseOperations::SetProtectedPatient(const std::string& publicId,
+                                                        bool isProtected)
+  {
+    class Operations : public IReadWriteOperations
+    {
+    private:
+      const std::string&  publicId_;
+      bool                isProtected_;
+
+    public:
+      Operations(const std::string& publicId,
+                 bool isProtected) :
+        publicId_(publicId),
+        isProtected_(isProtected)
+      {
+      }
+
+      virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE
+      {
+        // Lookup for the requested resource
+        int64_t id;
+        ResourceType type;
+        if (!transaction.LookupResource(id, type, publicId_) ||
+            type != ResourceType_Patient)
+        {
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+        }
+        else
+        {
+          transaction.SetProtectedPatient(id, isProtected_);
+        }
+      }
+    };
+
+    Operations operations(publicId, isProtected);
+    Apply(operations);
+
+    if (isProtected)
+    {
+      LOG(INFO) << "Patient " << publicId << " has been protected";
+    }
+    else
+    {
+      LOG(INFO) << "Patient " << publicId << " has been unprotected";
+    }
+  }
+
+
+  void StatelessDatabaseOperations::SetMetadata(int64_t& newRevision,
+                                                const std::string& publicId,
+                                                MetadataType type,
+                                                const std::string& value,
+                                                bool hasOldRevision,
+                                                int64_t oldRevision)
+  {
+    class Operations : public IReadWriteOperations
+    {
+    private:
+      int64_t&            newRevision_;
+      const std::string&  publicId_;
+      MetadataType        type_;
+      const std::string&  value_;
+      bool                hasOldRevision_;
+      int64_t             oldRevision_;
+
+    public:
+      Operations(int64_t& newRevision,
+                 const std::string& publicId,
+                 MetadataType type,
+                 const std::string& value,
+                 bool hasOldRevision,
+                 int64_t oldRevision) :
+        newRevision_(newRevision),
+        publicId_(publicId),
+        type_(type),
+        value_(value),
+        hasOldRevision_(hasOldRevision),
+        oldRevision_(oldRevision)
+      {
+      }
+
+      virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE
+      {
+        ResourceType resourceType;
+        int64_t id;
+        if (!transaction.LookupResource(id, resourceType, publicId_))
+        {
+          throw OrthancException(ErrorCode_UnknownResource);
+        }
+        else
+        {
+          std::string oldValue;
+          int64_t expectedRevision;
+          if (transaction.LookupMetadata(oldValue, expectedRevision, id, type_))
+          {
+            if (hasOldRevision_ &&
+                expectedRevision != oldRevision_)
+            {
+              throw OrthancException(ErrorCode_Revision);
+            }
+            else
+            {
+              newRevision_ = expectedRevision + 1;
+            }
+          }
+          else
+          {
+            // The metadata is not existing yet: Ignore "oldRevision"
+            // and initialize a new sequence of revisions
+            newRevision_ = 0;
+          }
+
+          transaction.SetMetadata(id, type_, value_, newRevision_);
+          
+          if (IsUserMetadata(type_))
+          {
+            transaction.LogChange(id, ChangeType_UpdatedMetadata, resourceType, publicId_);
+          }
+        }
+      }
+    };
+
+    Operations operations(newRevision, publicId, type, value, hasOldRevision, oldRevision);
+    Apply(operations);
+  }
+
+
+  void StatelessDatabaseOperations::OverwriteMetadata(const std::string& publicId,
+                                                      MetadataType type,
+                                                      const std::string& value)
+  {
+    int64_t newRevision;  // Unused
+    SetMetadata(newRevision, publicId, type, value, false /* no old revision */, -1 /* dummy */);
+  }
+
+
+  bool StatelessDatabaseOperations::DeleteMetadata(const std::string& publicId,
+                                                   MetadataType type,
+                                                   bool hasRevision,
+                                                   int64_t revision)
+  {
+    class Operations : public IReadWriteOperations
+    {
+    private:
+      const std::string&  publicId_;
+      MetadataType        type_;
+      bool                hasRevision_;
+      int64_t             revision_;
+      bool                found_;
+
+    public:
+      Operations(const std::string& publicId,
+                 MetadataType type,
+                 bool hasRevision,
+                 int64_t revision) :
+        publicId_(publicId),
+        type_(type),
+        hasRevision_(hasRevision),
+        revision_(revision),
+        found_(false)
+      {
+      }
+
+      bool HasFound() const
+      {
+        return found_;
+      }
+
+      virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE
+      {
+        ResourceType resourceType;
+        int64_t id;
+        if (!transaction.LookupResource(id, resourceType, publicId_))
+        {
+          throw OrthancException(ErrorCode_UnknownResource);
+        }
+        else
+        {
+          std::string s;
+          int64_t expectedRevision;
+          if (transaction.LookupMetadata(s, expectedRevision, id, type_))
+          {
+            if (hasRevision_ &&
+                expectedRevision != revision_)
+            {
+              throw OrthancException(ErrorCode_Revision);
+            }
+            
+            found_ = true;
+            transaction.DeleteMetadata(id, type_);
+
+            if (IsUserMetadata(type_))
+            {
+              transaction.LogChange(id, ChangeType_UpdatedMetadata, resourceType, publicId_);
+            }
+          }
+          else
+          {
+            found_ = false;
+          }
+        }
+      }
+    };
+
+    Operations operations(publicId, type, hasRevision, revision);
+    Apply(operations);
+    return operations.HasFound();
+  }
+
+
+  uint64_t StatelessDatabaseOperations::IncrementGlobalSequence(GlobalProperty sequence,
+                                                                bool shared)
+  {
+    class Operations : public IReadWriteOperations
+    {
+    private:
+      uint64_t       newValue_;
+      GlobalProperty sequence_;
+      bool           shared_;
+
+    public:
+      Operations(GlobalProperty sequence,
+                 bool shared) :
+        newValue_(0),  // Dummy initialization
+        sequence_(sequence),
+        shared_(shared)
+      {
+      }
+
+      uint64_t GetNewValue() const
+      {
+        return newValue_;
+      }
+
+      virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE
+      {
+        std::string oldString;
+
+        if (transaction.LookupGlobalProperty(oldString, sequence_, shared_))
+        {
+          uint64_t oldValue;
+      
+          try
+          {
+            oldValue = boost::lexical_cast<uint64_t>(oldString);
+          }
+          catch (boost::bad_lexical_cast&)
+          {
+            LOG(ERROR) << "Cannot read the global sequence "
+                       << boost::lexical_cast<std::string>(sequence_) << ", resetting it";
+            oldValue = 0;
+          }
+
+          newValue_ = oldValue + 1;
+        }
+        else
+        {
+          // Initialize the sequence at "1"
+          newValue_ = 1;
+        }
+
+        transaction.SetGlobalProperty(sequence_, shared_, boost::lexical_cast<std::string>(newValue_));
+      }
+    };
+
+    Operations operations(sequence, shared);
+    Apply(operations);
+    assert(operations.GetNewValue() != 0);
+    return operations.GetNewValue();
+  }
+
+
+  void StatelessDatabaseOperations::DeleteChanges()
+  {
+    class Operations : public IReadWriteOperations
+    {
+    public:
+      virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE
+      {
+        transaction.ClearChanges();
+      }
+    };
+
+    Operations operations;
+    Apply(operations);
+  }
+
+  
+  void StatelessDatabaseOperations::DeleteExportedResources()
+  {
+    class Operations : public IReadWriteOperations
+    {
+    public:
+      virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE
+      {
+        transaction.ClearExportedResources();
+      }
+    };
+
+    Operations operations;
+    Apply(operations);
+  }
+
+
+  void StatelessDatabaseOperations::SetGlobalProperty(GlobalProperty property,
+                                                      bool shared,
+                                                      const std::string& value)
+  {
+    class Operations : public IReadWriteOperations
+    {
+    private:
+      GlobalProperty      property_;
+      bool                shared_;
+      const std::string&  value_;
+      
+    public:
+      Operations(GlobalProperty property,
+                 bool shared,
+                 const std::string& value) :
+        property_(property),
+        shared_(shared),
+        value_(value)
+      {
+      }
+        
+      virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE
+      {
+        transaction.SetGlobalProperty(property_, shared_, value_);
+      }
+    };
+
+    Operations operations(property, shared, value);
+    Apply(operations);
+  }
+
+
+  bool StatelessDatabaseOperations::DeleteAttachment(const std::string& publicId,
+                                                     FileContentType type,
+                                                     bool hasRevision,
+                                                     int64_t revision)
+  {
+    class Operations : public IReadWriteOperations
+    {
+    private:
+      const std::string&  publicId_;
+      FileContentType     type_;
+      bool                hasRevision_;
+      int64_t             revision_;
+      bool                found_;
+
+    public:
+      Operations(const std::string& publicId,
+                 FileContentType type,
+                 bool hasRevision,
+                 int64_t revision) :
+        publicId_(publicId),
+        type_(type),
+        hasRevision_(hasRevision),
+        revision_(revision),
+        found_(false)
+      {
+      }
+        
+      bool HasFound() const
+      {
+        return found_;
+      }
+      
+      virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE
+      {
+        ResourceType resourceType;
+        int64_t id;
+        if (!transaction.LookupResource(id, resourceType, publicId_))
+        {
+          throw OrthancException(ErrorCode_UnknownResource);
+        }
+        else
+        {
+          FileInfo info;
+          int64_t expectedRevision;
+          if (transaction.LookupAttachment(info, expectedRevision, id, type_))
+          {
+            if (hasRevision_ &&
+                expectedRevision != revision_)
+            {
+              throw OrthancException(ErrorCode_Revision);
+            }
+            
+            found_ = true;
+            transaction.DeleteAttachment(id, type_);
+          
+            if (IsUserContentType(type_))
+            {
+              transaction.LogChange(id, ChangeType_UpdatedAttachment, resourceType, publicId_);
+            }
+          }
+          else
+          {
+            found_ = false;
+          }
+        }
+      }
+    };
+
+    Operations operations(publicId, type, hasRevision, revision);
+    Apply(operations);
+    return operations.HasFound();
+  }
+
+
+  void StatelessDatabaseOperations::LogChange(int64_t internalId,
+                                              ChangeType changeType,
+                                              const std::string& publicId,
+                                              ResourceType level)
+  {
+    class Operations : public IReadWriteOperations
+    {
+    private:
+      int64_t             internalId_;
+      ChangeType          changeType_;
+      const std::string&  publicId_;
+      ResourceType        level_;
+      
+    public:
+      Operations(int64_t internalId,
+                 ChangeType changeType,
+                 const std::string& publicId,
+                 ResourceType level) :
+        internalId_(internalId),
+        changeType_(changeType),
+        publicId_(publicId),
+        level_(level)
+      {
+      }
+        
+      virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE
+      {
+        int64_t id;
+        ResourceType type;
+        if (transaction.LookupResource(id, type, publicId_) &&
+            id == internalId_)
+        {
+          /**
+           * Make sure that the resource is still existing, with the
+           * same internal ID, which indicates the absence of bouncing
+           * (if deleting then recreating the same resource). Don't
+           * throw an exception if the resource has been deleted,
+           * because this function might e.g. be called from
+           * "StatelessDatabaseOperations::UnstableResourcesMonitorThread()"
+           * (for which a deleted resource is *not* an error case).
+           **/
+          if (type == level_)
+          {
+            transaction.LogChange(id, changeType_, type, publicId_);
+          }
+          else
+          {
+            // Consistency check
+            throw OrthancException(ErrorCode_UnknownResource);
+          }
+        }
+      }
+    };
+
+    Operations operations(internalId, changeType, publicId, level);
+    Apply(operations);
+  }
+
+
+  void StatelessDatabaseOperations::ReconstructInstance(const ParsedDicomFile& dicom)
+  {
+    class Operations : public IReadWriteOperations
+    {
+    private:
+      DicomMap                              summary_;
+      std::unique_ptr<DicomInstanceHasher>  hasher_;
+      bool                                  hasTransferSyntax_;
+      DicomTransferSyntax                   transferSyntax_;
+
+      static void ReplaceMetadata(ReadWriteTransaction& transaction,
+                                  int64_t instance,
+                                  MetadataType metadata,
+                                  const std::string& value)
+      {
+        std::string oldValue;
+        int64_t oldRevision;
+        
+        if (transaction.LookupMetadata(oldValue, oldRevision, instance, metadata))
+        {
+          transaction.SetMetadata(instance, metadata, value, oldRevision + 1);
+        }
+        else
+        {
+          transaction.SetMetadata(instance, metadata, value, 0);
+        }
+      }
+      
+    public:
+      explicit Operations(const ParsedDicomFile& dicom)
+      {
+        OrthancConfiguration::DefaultExtractDicomSummary(summary_, dicom);
+        hasher_.reset(new DicomInstanceHasher(summary_));
+        hasTransferSyntax_ = dicom.LookupTransferSyntax(transferSyntax_);
+      }
+        
+      virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE
+      {
+        int64_t patient = -1, study = -1, series = -1, instance = -1;
+
+        ResourceType type1, type2, type3, type4;      
+        if (!transaction.LookupResource(patient, type1, hasher_->HashPatient()) ||
+            !transaction.LookupResource(study, type2, hasher_->HashStudy()) ||
+            !transaction.LookupResource(series, type3, hasher_->HashSeries()) ||
+            !transaction.LookupResource(instance, type4, hasher_->HashInstance()) ||
+            type1 != ResourceType_Patient ||
+            type2 != ResourceType_Study ||
+            type3 != ResourceType_Series ||
+            type4 != ResourceType_Instance ||
+            patient == -1 ||
+            study == -1 ||
+            series == -1 ||
+            instance == -1)
+        {
+          throw OrthancException(ErrorCode_InternalError);
+        }
+
+        transaction.ClearMainDicomTags(patient);
+        transaction.ClearMainDicomTags(study);
+        transaction.ClearMainDicomTags(series);
+        transaction.ClearMainDicomTags(instance);
+
+        {
+          ResourcesContent content(false /* prevent the setting of metadata */);
+          content.AddResource(patient, ResourceType_Patient, summary_);
+          content.AddResource(study, ResourceType_Study, summary_);
+          content.AddResource(series, ResourceType_Series, summary_);
+          content.AddResource(instance, ResourceType_Instance, summary_);
+          transaction.SetResourcesContent(content);
+        }
+
+        if (hasTransferSyntax_)
+        {
+          ReplaceMetadata(transaction, instance, MetadataType_Instance_TransferSyntax, GetTransferSyntaxUid(transferSyntax_));
+        }
+
+        const DicomValue* value;
+        if ((value = summary_.TestAndGetValue(DICOM_TAG_SOP_CLASS_UID)) != NULL &&
+            !value->IsNull() &&
+            !value->IsBinary())
+        {
+          ReplaceMetadata(transaction, instance, MetadataType_Instance_SopClassUid, value->GetContent());
+        }
+      }
+    };
+
+    Operations operations(dicom);
+    Apply(operations);
+  }
+
+
+  static bool IsRecyclingNeeded(IDatabaseWrapper::ITransaction& transaction,
+                                uint64_t maximumStorageSize,
+                                unsigned int maximumPatients,
+                                uint64_t addedInstanceSize)
+  {
+    if (maximumStorageSize != 0)
+    {
+      if (maximumStorageSize < addedInstanceSize)
+      {
+        throw OrthancException(ErrorCode_FullStorage, "Cannot store an instance of size " +
+                               boost::lexical_cast<std::string>(addedInstanceSize) +
+                               " bytes in a storage area limited to " +
+                               boost::lexical_cast<std::string>(maximumStorageSize));
+      }
+      
+      if (transaction.IsDiskSizeAbove(maximumStorageSize - addedInstanceSize))
+      {
+        return true;
+      }
+    }
+
+    if (maximumPatients != 0)
+    {
+      uint64_t patientCount = transaction.GetResourcesCount(ResourceType_Patient);
+      if (patientCount > maximumPatients)
+      {
+        return true;
+      }
+    }
+
+    return false;
+  }
+  
+
+  void StatelessDatabaseOperations::ReadWriteTransaction::Recycle(uint64_t maximumStorageSize,
+                                                                  unsigned int maximumPatients,
+                                                                  uint64_t addedInstanceSize,
+                                                                  const std::string& newPatientId)
+  {
+    // TODO - Performance: Avoid calls to "IsRecyclingNeeded()"
+    
+    if (IsRecyclingNeeded(transaction_, maximumStorageSize, maximumPatients, addedInstanceSize))
+    {
+      // Check whether other DICOM instances from this patient are
+      // already stored
+      int64_t patientToAvoid;
+      bool hasPatientToAvoid;
+
+      if (newPatientId.empty())
+      {
+        hasPatientToAvoid = false;
+      }
+      else
+      {
+        ResourceType type;
+        hasPatientToAvoid = transaction_.LookupResource(patientToAvoid, type, newPatientId);
+        if (type != ResourceType_Patient)
+        {
+          throw OrthancException(ErrorCode_InternalError);
+        }
+      }
+
+      // Iteratively select patient to remove until there is enough
+      // space in the DICOM store
+      int64_t patientToRecycle;
+      while (true)
+      {
+        // If other instances of this patient are already in the store,
+        // we must avoid to recycle them
+        bool ok = (hasPatientToAvoid ?
+                   transaction_.SelectPatientToRecycle(patientToRecycle, patientToAvoid) :
+                   transaction_.SelectPatientToRecycle(patientToRecycle));
+        
+        if (!ok)
+        {
+          throw OrthancException(ErrorCode_FullStorage);
+        }
+      
+        LOG(TRACE) << "Recycling one patient";
+        transaction_.DeleteResource(patientToRecycle);
+
+        if (!IsRecyclingNeeded(transaction_, maximumStorageSize, maximumPatients, addedInstanceSize))
+        {
+          // OK, we're done
+          return;
+        }
+      }
+    }
+  }
+
+
+  void StatelessDatabaseOperations::StandaloneRecycling(uint64_t maximumStorageSize,
+                                                        unsigned int maximumPatientCount)
+  {
+    class Operations : public IReadWriteOperations
+    {
+    private:
+      uint64_t      maximumStorageSize_;
+      unsigned int  maximumPatientCount_;
+      
+    public:
+      Operations(uint64_t maximumStorageSize,
+                 unsigned int maximumPatientCount) :
+        maximumStorageSize_(maximumStorageSize),
+        maximumPatientCount_(maximumPatientCount)
+      {
+      }
+        
+      virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE
+      {
+        transaction.Recycle(maximumStorageSize_, maximumPatientCount_, 0, "");
+      }
+    };
+
+    if (maximumStorageSize != 0 ||
+        maximumPatientCount != 0)
+    {
+      Operations operations(maximumStorageSize, maximumPatientCount);
+      Apply(operations);
+    }
+  }
+
+
+  StoreStatus StatelessDatabaseOperations::Store(std::map<MetadataType, std::string>& instanceMetadata,
+                                                 const DicomMap& dicomSummary,
+                                                 const Attachments& attachments,
+                                                 const MetadataMap& metadata,
+                                                 const DicomInstanceOrigin& origin,
+                                                 bool overwrite,
+                                                 bool hasTransferSyntax,
+                                                 DicomTransferSyntax transferSyntax,
+                                                 bool hasPixelDataOffset,
+                                                 uint64_t pixelDataOffset,
+                                                 uint64_t maximumStorageSize,
+                                                 unsigned int maximumPatients)
+  {
+    class Operations : public IReadWriteOperations
+    {
+    private:
+      StoreStatus                          storeStatus_;
+      std::map<MetadataType, std::string>& instanceMetadata_;
+      const DicomMap&                      dicomSummary_;
+      const Attachments&                   attachments_;
+      const MetadataMap&                   metadata_;
+      const DicomInstanceOrigin&           origin_;
+      bool                                 overwrite_;
+      bool                                 hasTransferSyntax_;
+      DicomTransferSyntax                  transferSyntax_;
+      bool                                 hasPixelDataOffset_;
+      uint64_t                             pixelDataOffset_;
+      uint64_t                             maximumStorageSize_;
+      unsigned int                         maximumPatientCount_;
+
+      // Auto-computed fields
+      bool          hasExpectedInstances_;
+      int64_t       expectedInstances_;
+      std::string   hashPatient_;
+      std::string   hashStudy_;
+      std::string   hashSeries_;
+      std::string   hashInstance_;
+
+      
+      static void SetInstanceMetadata(ResourcesContent& content,
+                                      std::map<MetadataType, std::string>& instanceMetadata,
+                                      int64_t instance,
+                                      MetadataType metadata,
+                                      const std::string& value)
+      {
+        content.AddMetadata(instance, metadata, value);
+        instanceMetadata[metadata] = value;
+      }
+
+      
+      static bool ComputeExpectedNumberOfInstances(int64_t& target,
+                                                   const DicomMap& dicomSummary)
+      {
+        try
+        {
+          const DicomValue* value;
+          const DicomValue* value2;
+          
+          if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_IMAGES_IN_ACQUISITION)) != NULL &&
+              !value->IsNull() &&
+              !value->IsBinary() &&
+              (value2 = dicomSummary.TestAndGetValue(DICOM_TAG_NUMBER_OF_TEMPORAL_POSITIONS)) != NULL &&
+              !value2->IsNull() &&
+              !value2->IsBinary())
+          {
+            // Patch for series with temporal positions thanks to Will Ryder
+            int64_t imagesInAcquisition = boost::lexical_cast<int64_t>(value->GetContent());
+            int64_t countTemporalPositions = boost::lexical_cast<int64_t>(value2->GetContent());
+            target = imagesInAcquisition * countTemporalPositions;
+            return (target > 0);
+          }
+
+          else if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_NUMBER_OF_SLICES)) != NULL &&
+                   !value->IsNull() &&
+                   !value->IsBinary() &&
+                   (value2 = dicomSummary.TestAndGetValue(DICOM_TAG_NUMBER_OF_TIME_SLICES)) != NULL &&
+                   !value2->IsBinary() &&
+                   !value2->IsNull())
+          {
+            // Support of Cardio-PET images
+            int64_t numberOfSlices = boost::lexical_cast<int64_t>(value->GetContent());
+            int64_t numberOfTimeSlices = boost::lexical_cast<int64_t>(value2->GetContent());
+            target = numberOfSlices * numberOfTimeSlices;
+            return (target > 0);
+          }
+
+          else if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_CARDIAC_NUMBER_OF_IMAGES)) != NULL &&
+                   !value->IsNull() &&
+                   !value->IsBinary())
+          {
+            target = boost::lexical_cast<int64_t>(value->GetContent());
+            return (target > 0);
+          }
+        }
+        catch (OrthancException&)
+        {
+        }
+        catch (boost::bad_lexical_cast&)
+        {
+        }
+
+        return false;
+      }
+
+    public:
+      Operations(std::map<MetadataType, std::string>& instanceMetadata,
+                 const DicomMap& dicomSummary,
+                 const Attachments& attachments,
+                 const MetadataMap& metadata,
+                 const DicomInstanceOrigin& origin,
+                 bool overwrite,
+                 bool hasTransferSyntax,
+                 DicomTransferSyntax transferSyntax,
+                 bool hasPixelDataOffset,
+                 uint64_t pixelDataOffset,
+                 uint64_t maximumStorageSize,
+                 unsigned int maximumPatientCount) :
+        storeStatus_(StoreStatus_Failure),
+        instanceMetadata_(instanceMetadata),
+        dicomSummary_(dicomSummary),
+        attachments_(attachments),
+        metadata_(metadata),
+        origin_(origin),
+        overwrite_(overwrite),
+        hasTransferSyntax_(hasTransferSyntax),
+        transferSyntax_(transferSyntax),
+        hasPixelDataOffset_(hasPixelDataOffset),
+        pixelDataOffset_(pixelDataOffset),
+        maximumStorageSize_(maximumStorageSize),
+        maximumPatientCount_(maximumPatientCount)
+      {
+        hasExpectedInstances_ = ComputeExpectedNumberOfInstances(expectedInstances_, dicomSummary);
+    
+        instanceMetadata_.clear();
+
+        DicomInstanceHasher hasher(dicomSummary);
+        hashPatient_ = hasher.HashPatient();
+        hashStudy_ = hasher.HashStudy();
+        hashSeries_ = hasher.HashSeries();
+        hashInstance_ = hasher.HashInstance();
+      }
+
+      StoreStatus GetStoreStatus() const
+      {
+        return storeStatus_;
+      }
+        
+      virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE
+      {
+        try
+        {
+          IDatabaseWrapper::CreateInstanceResult status;
+          int64_t instanceId;
+
+          // Check whether this instance is already stored
+          if (!transaction.CreateInstance(status, instanceId, hashPatient_,
+                                          hashStudy_, hashSeries_, hashInstance_))
+          {
+            // The instance already exists
+        
+            if (overwrite_)
+            {
+              // Overwrite the old instance
+              LOG(INFO) << "Overwriting instance: " << hashInstance_;
+              transaction.DeleteResource(instanceId);
+
+              // Re-create the instance, now that the old one is removed
+              if (!transaction.CreateInstance(status, instanceId, hashPatient_,
+                                              hashStudy_, hashSeries_, hashInstance_))
+              {
+                throw OrthancException(ErrorCode_InternalError);
+              }
+            }
+            else
+            {
+              // Do nothing if the instance already exists and overwriting is disabled
+              transaction.GetAllMetadata(instanceMetadata_, instanceId);
+              storeStatus_ = StoreStatus_AlreadyStored;
+              return;
+            }
+          }
+
+
+          // Warn about the creation of new resources. The order must be
+          // from instance to patient.
+
+          // NB: In theory, could be sped up by grouping the underlying
+          // calls to "transaction.LogChange()". However, this would only have an
+          // impact when new patient/study/series get created, which
+          // occurs far less often that creating new instances. The
+          // positive impact looks marginal in practice.
+          transaction.LogChange(instanceId, ChangeType_NewInstance, ResourceType_Instance, hashInstance_);
+
+          if (status.isNewSeries_)
+          {
+            transaction.LogChange(status.seriesId_, ChangeType_NewSeries, ResourceType_Series, hashSeries_);
+          }
+      
+          if (status.isNewStudy_)
+          {
+            transaction.LogChange(status.studyId_, ChangeType_NewStudy, ResourceType_Study, hashStudy_);
+          }
+      
+          if (status.isNewPatient_)
+          {
+            transaction.LogChange(status.patientId_, ChangeType_NewPatient, ResourceType_Patient, hashPatient_);
+          }
+      
+      
+          // Ensure there is enough room in the storage for the new instance
+          uint64_t instanceSize = 0;
+          for (Attachments::const_iterator it = attachments_.begin();
+               it != attachments_.end(); ++it)
+          {
+            instanceSize += it->GetCompressedSize();
+          }
+
+          transaction.Recycle(maximumStorageSize_, maximumPatientCount_,
+                              instanceSize, hashPatient_ /* don't consider the current patient for recycling */);
+      
+     
+          // Attach the files to the newly created instance
+          for (Attachments::const_iterator it = attachments_.begin();
+               it != attachments_.end(); ++it)
+          {
+            transaction.AddAttachment(instanceId, *it, 0 /* this is the first revision */);
+          }
+
+      
+          {
+            ResourcesContent content(true /* new resource, metadata can be set */);
+      
+            // Populate the tags of the newly-created resources
+
+            content.AddResource(instanceId, ResourceType_Instance, dicomSummary_);
+
+            if (status.isNewSeries_)
+            {
+              content.AddResource(status.seriesId_, ResourceType_Series, dicomSummary_);
+            }
+
+            if (status.isNewStudy_)
+            {
+              content.AddResource(status.studyId_, ResourceType_Study, dicomSummary_);
+            }
+
+            if (status.isNewPatient_)
+            {
+              content.AddResource(status.patientId_, ResourceType_Patient, dicomSummary_);
+            }
+
+
+            // Attach the user-specified metadata
+
+            for (MetadataMap::const_iterator 
+                   it = metadata_.begin(); it != metadata_.end(); ++it)
+            {
+              switch (it->first.first)
+              {
+                case ResourceType_Patient:
+                  content.AddMetadata(status.patientId_, it->first.second, it->second);
+                  break;
+
+                case ResourceType_Study:
+                  content.AddMetadata(status.studyId_, it->first.second, it->second);
+                  break;
+
+                case ResourceType_Series:
+                  content.AddMetadata(status.seriesId_, it->first.second, it->second);
+                  break;
+
+                case ResourceType_Instance:
+                  SetInstanceMetadata(content, instanceMetadata_, instanceId,
+                                      it->first.second, it->second);
+                  break;
+
+                default:
+                  throw OrthancException(ErrorCode_ParameterOutOfRange);
+              }
+            }
+
+        
+            // Attach the auto-computed metadata for the patient/study/series levels
+            std::string now = SystemToolbox::GetNowIsoString(true /* use UTC time (not local time) */);
+            content.AddMetadata(status.seriesId_, MetadataType_LastUpdate, now);
+            content.AddMetadata(status.studyId_, MetadataType_LastUpdate, now);
+            content.AddMetadata(status.patientId_, MetadataType_LastUpdate, now);
+
+            if (status.isNewSeries_)
+            {
+              if (hasExpectedInstances_)
+              {
+                content.AddMetadata(status.seriesId_, MetadataType_Series_ExpectedNumberOfInstances,
+                                    boost::lexical_cast<std::string>(expectedInstances_));
+              }
+
+              // New in Orthanc 1.9.0
+              content.AddMetadata(status.seriesId_, MetadataType_RemoteAet,
+                                  origin_.GetRemoteAetC());
+            }
+
+        
+            // Attach the auto-computed metadata for the instance level,
+            // reflecting these additions into the input metadata map
+            SetInstanceMetadata(content, instanceMetadata_, instanceId,
+                                MetadataType_Instance_ReceptionDate, now);
+            SetInstanceMetadata(content, instanceMetadata_, instanceId, MetadataType_RemoteAet,
+                                origin_.GetRemoteAetC());
+            SetInstanceMetadata(content, instanceMetadata_, instanceId, MetadataType_Instance_Origin, 
+                                EnumerationToString(origin_.GetRequestOrigin()));
+
+
+            if (hasTransferSyntax_)
+            {
+              // New in Orthanc 1.2.0
+              SetInstanceMetadata(content, instanceMetadata_, instanceId,
+                                  MetadataType_Instance_TransferSyntax,
+                                  GetTransferSyntaxUid(transferSyntax_));
+            }
+
+            {
+              std::string s;
+
+              if (origin_.LookupRemoteIp(s))
+              {
+                // New in Orthanc 1.4.0
+                SetInstanceMetadata(content, instanceMetadata_, instanceId,
+                                    MetadataType_Instance_RemoteIp, s);
+              }
+
+              if (origin_.LookupCalledAet(s))
+              {
+                // New in Orthanc 1.4.0
+                SetInstanceMetadata(content, instanceMetadata_, instanceId,
+                                    MetadataType_Instance_CalledAet, s);
+              }
+
+              if (origin_.LookupHttpUsername(s))
+              {
+                // New in Orthanc 1.4.0
+                SetInstanceMetadata(content, instanceMetadata_, instanceId,
+                                    MetadataType_Instance_HttpUsername, s);
+              }
+            }
+
+            if (hasPixelDataOffset_)
+            {
+              // New in Orthanc 1.9.1
+              SetInstanceMetadata(content, instanceMetadata_, instanceId,
+                                  MetadataType_Instance_PixelDataOffset,
+                                  boost::lexical_cast<std::string>(pixelDataOffset_));
+            }
+        
+            const DicomValue* value;
+            if ((value = dicomSummary_.TestAndGetValue(DICOM_TAG_SOP_CLASS_UID)) != NULL &&
+                !value->IsNull() &&
+                !value->IsBinary())
+            {
+              SetInstanceMetadata(content, instanceMetadata_, instanceId,
+                                  MetadataType_Instance_SopClassUid, value->GetContent());
+            }
+
+
+            if ((value = dicomSummary_.TestAndGetValue(DICOM_TAG_INSTANCE_NUMBER)) != NULL ||
+                (value = dicomSummary_.TestAndGetValue(DICOM_TAG_IMAGE_INDEX)) != NULL)
+            {
+              if (!value->IsNull() && 
+                  !value->IsBinary())
+              {
+                SetInstanceMetadata(content, instanceMetadata_, instanceId,
+                                    MetadataType_Instance_IndexInSeries, Toolbox::StripSpaces(value->GetContent()));
+              }
+            }
+
+        
+            transaction.SetResourcesContent(content);
+          }
+
+  
+          // Check whether the series of this new instance is now completed
+          int64_t expectedNumberOfInstances;
+          if (ComputeExpectedNumberOfInstances(expectedNumberOfInstances, dicomSummary_))
+          {
+            SeriesStatus seriesStatus = transaction.GetSeriesStatus(status.seriesId_, expectedNumberOfInstances);
+            if (seriesStatus == SeriesStatus_Complete)
+            {
+              transaction.LogChange(status.seriesId_, ChangeType_CompletedSeries, ResourceType_Series, hashSeries_);
+            }
+          }
+          
+          transaction.LogChange(status.seriesId_, ChangeType_NewChildInstance, ResourceType_Series, hashSeries_);
+          transaction.LogChange(status.studyId_, ChangeType_NewChildInstance, ResourceType_Study, hashStudy_);
+          transaction.LogChange(status.patientId_, ChangeType_NewChildInstance, ResourceType_Patient, hashPatient_);
+          
+          // Mark the parent resources of this instance as unstable
+          transaction.GetTransactionContext().MarkAsUnstable(status.seriesId_, ResourceType_Series, hashSeries_);
+          transaction.GetTransactionContext().MarkAsUnstable(status.studyId_, ResourceType_Study, hashStudy_);
+          transaction.GetTransactionContext().MarkAsUnstable(status.patientId_, ResourceType_Patient, hashPatient_);
+          transaction.GetTransactionContext().SignalAttachmentsAdded(instanceSize);
+
+          storeStatus_ = StoreStatus_Success;          
+        }
+        catch (OrthancException& e)
+        {
+          if (e.GetErrorCode() == ErrorCode_DatabaseCannotSerialize)
+          {
+            throw;
+          }
+          else
+          {
+            LOG(ERROR) << "EXCEPTION [" << e.What() << "]";
+            storeStatus_ = StoreStatus_Failure;
+          }
+        }
+      }
+    };
+
+
+    Operations operations(instanceMetadata, dicomSummary, attachments, metadata, origin,
+                          overwrite, hasTransferSyntax, transferSyntax, hasPixelDataOffset,
+                          pixelDataOffset, maximumStorageSize, maximumPatients);
+    Apply(operations);
+    return operations.GetStoreStatus();
+  }
+
+
+  StoreStatus StatelessDatabaseOperations::AddAttachment(int64_t& newRevision,
+                                                         const FileInfo& attachment,
+                                                         const std::string& publicId,
+                                                         uint64_t maximumStorageSize,
+                                                         unsigned int maximumPatients,
+                                                         bool hasOldRevision,
+                                                         int64_t oldRevision)
+  {
+    class Operations : public IReadWriteOperations
+    {
+    private:
+      int64_t&            newRevision_;
+      StoreStatus         status_;
+      const FileInfo&     attachment_;
+      const std::string&  publicId_;
+      uint64_t            maximumStorageSize_;
+      unsigned int        maximumPatientCount_;
+      bool                hasOldRevision_;
+      int64_t             oldRevision_;
+
+    public:
+      Operations(int64_t& newRevision,
+                 const FileInfo& attachment,
+                 const std::string& publicId,
+                 uint64_t maximumStorageSize,
+                 unsigned int maximumPatientCount,
+                 bool hasOldRevision,
+                 int64_t oldRevision) :
+        newRevision_(newRevision),
+        status_(StoreStatus_Failure),
+        attachment_(attachment),
+        publicId_(publicId),
+        maximumStorageSize_(maximumStorageSize),
+        maximumPatientCount_(maximumPatientCount),
+        hasOldRevision_(hasOldRevision),
+        oldRevision_(oldRevision)
+      {
+      }
+
+      StoreStatus GetStatus() const
+      {
+        return status_;
+      }
+        
+      virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE
+      {
+        ResourceType resourceType;
+        int64_t resourceId;
+        if (!transaction.LookupResource(resourceId, resourceType, publicId_))
+        {
+          status_ = StoreStatus_Failure;  // Inexistent resource
+        }
+        else
+        {
+          // Possibly remove previous attachment
+          {
+            FileInfo oldFile;
+            int64_t expectedRevision;
+            if (transaction.LookupAttachment(oldFile, expectedRevision, resourceId, attachment_.GetContentType()))
+            {
+              if (hasOldRevision_ &&
+                  expectedRevision != oldRevision_)
+              {
+                throw OrthancException(ErrorCode_Revision);
+              }
+              else
+              {
+                newRevision_ = expectedRevision + 1;
+                transaction.DeleteAttachment(resourceId, attachment_.GetContentType());
+              }
+            }
+            else
+            {
+              // The attachment is not existing yet: Ignore "oldRevision"
+              // and initialize a new sequence of revisions
+              newRevision_ = 0;
+            }
+          }
+
+          // Locate the patient of the target resource
+          int64_t patientId = resourceId;
+          for (;;)
+          {
+            int64_t parent;
+            if (transaction.LookupParent(parent, patientId))
+            {
+              // We have not reached the patient level yet
+              patientId = parent;
+            }
+            else
+            {
+              // We have reached the patient level
+              break;
+            }
+          }
+
+          // Possibly apply the recycling mechanism while preserving this patient
+          assert(transaction.GetResourceType(patientId) == ResourceType_Patient);
+          transaction.Recycle(maximumStorageSize_, maximumPatientCount_,
+                              attachment_.GetCompressedSize(), transaction.GetPublicId(patientId));
+
+          transaction.AddAttachment(resourceId, attachment_, newRevision_);
+
+          if (IsUserContentType(attachment_.GetContentType()))
+          {
+            transaction.LogChange(resourceId, ChangeType_UpdatedAttachment, resourceType, publicId_);
+          }
+
+          transaction.GetTransactionContext().SignalAttachmentsAdded(attachment_.GetCompressedSize());
+
+          status_ = StoreStatus_Success;
+        }
+      }
+    };
+
+
+    Operations operations(newRevision, attachment, publicId, maximumStorageSize, maximumPatients, hasOldRevision, oldRevision);
+    Apply(operations);
+    return operations.GetStatus();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Tue Apr 20 18:11:29 2021 +0200
@@ -0,0 +1,639 @@
+/**
+ * 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 General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * 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
+
+#include "../../../OrthancFramework/Sources/DicomFormat/DicomMap.h"
+
+#include "IDatabaseWrapper.h"
+#include "../DicomInstanceOrigin.h"
+
+#include <boost/shared_ptr.hpp>
+#include <boost/thread/shared_mutex.hpp>
+
+
+namespace Orthanc
+{
+  class DatabaseLookup;
+  class ParsedDicomFile;
+  struct ServerIndexChange;
+
+  class StatelessDatabaseOperations : public boost::noncopyable
+  {
+  public:
+    typedef std::list<FileInfo> Attachments;
+    typedef std::map<std::pair<ResourceType, MetadataType>, std::string>  MetadataMap;
+
+    class ITransactionContext : public IDatabaseListener
+    {
+    public:
+      virtual ~ITransactionContext()
+      {
+      }
+
+      virtual void Commit() = 0;
+
+      virtual int64_t GetCompressedSizeDelta() = 0;
+
+      virtual bool IsUnstableResource(int64_t id) = 0;
+
+      virtual bool LookupRemainingLevel(std::string& remainingPublicId /* out */,
+                                        ResourceType& remainingLevel   /* out */) = 0;
+
+      virtual void MarkAsUnstable(int64_t id,
+                                  Orthanc::ResourceType type,
+                                  const std::string& publicId) = 0;
+
+      virtual void SignalAttachmentsAdded(uint64_t compressedSize) = 0;
+
+      virtual void SignalChange(const ServerIndexChange& change) = 0;
+    };
+
+    
+    class ITransactionContextFactory : public boost::noncopyable
+    {
+    public:
+      virtual ~ITransactionContextFactory()
+      {
+      }
+
+      // WARNING: This method can be invoked from several threads concurrently
+      virtual ITransactionContext* Create() = 0;
+    };
+
+
+    class ReadOnlyTransaction : public boost::noncopyable
+    {
+    private:
+      ITransactionContext&  context_;
+      
+    protected:
+      IDatabaseWrapper::ITransaction&  transaction_;
+      
+    public:
+      explicit ReadOnlyTransaction(IDatabaseWrapper::ITransaction& transaction,
+                                   ITransactionContext& context) :
+        context_(context),
+        transaction_(transaction)
+      {
+      }
+
+      ITransactionContext& GetTransactionContext()
+      {
+        return context_;
+      }
+
+      /**
+       * Higher-level constructions
+       **/
+
+      SeriesStatus GetSeriesStatus(int64_t id,
+                                   int64_t expectedNumberOfInstances);
+
+      
+      /**
+       * Read-only methods from "IDatabaseWrapper"
+       **/
+
+      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)
+      {
+        return transaction_.ApplyLookupResources(resourcesId, instancesId, lookup, queryLevel, limit);
+      }
+
+      void GetAllMetadata(std::map<MetadataType, std::string>& target,
+                          int64_t id)
+      {
+        transaction_.GetAllMetadata(target, id);
+      }
+
+      void GetAllPublicIds(std::list<std::string>& target,
+                           ResourceType resourceType)
+      {
+        return transaction_.GetAllPublicIds(target, resourceType);
+      }
+
+      void GetAllPublicIds(std::list<std::string>& target,
+                           ResourceType resourceType,
+                           size_t since,
+                           size_t limit)
+      {
+        return transaction_.GetAllPublicIds(target, resourceType, since, limit);
+      }  
+
+      void GetChanges(std::list<ServerIndexChange>& target /*out*/,
+                      bool& done /*out*/,
+                      int64_t since,
+                      uint32_t maxResults)
+      {
+        transaction_.GetChanges(target, done, since, maxResults);
+      }
+
+      void GetChildrenInternalId(std::list<int64_t>& target,
+                                 int64_t id)
+      {
+        transaction_.GetChildrenInternalId(target, id);
+      }
+
+      void GetChildrenPublicId(std::list<std::string>& target,
+                               int64_t id)
+      {
+        transaction_.GetChildrenPublicId(target, id);
+      }
+
+      void GetExportedResources(std::list<ExportedResource>& target /*out*/,
+                                bool& done /*out*/,
+                                int64_t since,
+                                uint32_t maxResults)
+      {
+        return transaction_.GetExportedResources(target, done, since, maxResults);
+      }
+
+      void GetLastChange(std::list<ServerIndexChange>& target /*out*/)
+      {
+        transaction_.GetLastChange(target);
+      }
+
+      void GetLastExportedResource(std::list<ExportedResource>& target /*out*/)
+      {
+        return transaction_.GetLastExportedResource(target);
+      }
+
+      int64_t GetLastChangeIndex()
+      {
+        return transaction_.GetLastChangeIndex();
+      }
+
+      void GetMainDicomTags(DicomMap& map,
+                            int64_t id)
+      {
+        transaction_.GetMainDicomTags(map, id);
+      }
+
+      std::string GetPublicId(int64_t resourceId)
+      {
+        return transaction_.GetPublicId(resourceId);
+      }
+      
+      uint64_t GetResourcesCount(ResourceType resourceType)
+      {
+        return transaction_.GetResourcesCount(resourceType);
+      }
+      
+      ResourceType GetResourceType(int64_t resourceId)
+      {
+        return transaction_.GetResourceType(resourceId);
+      }
+
+      uint64_t GetTotalCompressedSize()
+      {
+        return transaction_.GetTotalCompressedSize();
+      }
+    
+      uint64_t GetTotalUncompressedSize()
+      {
+        return transaction_.GetTotalUncompressedSize();
+      }
+      
+      bool IsProtectedPatient(int64_t internalId)
+      {
+        return transaction_.IsProtectedPatient(internalId);
+      }
+
+      void ListAvailableAttachments(std::set<FileContentType>& target,
+                                    int64_t id)
+      {
+        transaction_.ListAvailableAttachments(target, id);
+      }
+
+      bool LookupAttachment(FileInfo& attachment,
+                            int64_t& revision,
+                            int64_t id,
+                            FileContentType contentType)
+      {
+        return transaction_.LookupAttachment(attachment, revision, id, contentType);
+      }
+      
+      bool LookupGlobalProperty(std::string& target,
+                                GlobalProperty property,
+                                bool shared)
+      {
+        return transaction_.LookupGlobalProperty(target, property, shared);
+      }
+
+      bool LookupMetadata(std::string& target,
+                          int64_t& revision,
+                          int64_t id,
+                          MetadataType type)
+      {
+        return transaction_.LookupMetadata(target, revision, id, type);
+      }
+
+      bool LookupParent(int64_t& parentId,
+                        int64_t resourceId)
+      {
+        return transaction_.LookupParent(parentId, resourceId);
+      }
+        
+      bool LookupResource(int64_t& id,
+                          ResourceType& type,
+                          const std::string& publicId)
+      {
+        return transaction_.LookupResource(id, type, publicId);
+      }
+      
+      bool LookupResourceAndParent(int64_t& id,
+                                   ResourceType& type,
+                                   std::string& parentPublicId,
+                                   const std::string& publicId)
+      {
+        return transaction_.LookupResourceAndParent(id, type, parentPublicId, publicId);
+      }
+    };
+
+
+    class ReadWriteTransaction : public ReadOnlyTransaction
+    {
+    public:
+      ReadWriteTransaction(IDatabaseWrapper::ITransaction& transaction,
+                           ITransactionContext& context) :
+        ReadOnlyTransaction(transaction, context)
+      {
+      }
+
+      void AddAttachment(int64_t id,
+                         const FileInfo& attachment,
+                         int64_t revision)
+      {
+        transaction_.AddAttachment(id, attachment, revision);
+      }
+      
+      void ClearChanges()
+      {
+        transaction_.ClearChanges();
+      }
+
+      void ClearExportedResources()
+      {
+        transaction_.ClearExportedResources();
+      }
+
+      void ClearMainDicomTags(int64_t id)
+      {
+        return transaction_.ClearMainDicomTags(id);
+      }
+
+      bool CreateInstance(IDatabaseWrapper::CreateInstanceResult& result, /* out */
+                          int64_t& instanceId,          /* out */
+                          const std::string& patient,
+                          const std::string& study,
+                          const std::string& series,
+                          const std::string& instance)
+      {
+        return transaction_.CreateInstance(result, instanceId, patient, study, series, instance);
+      }
+
+      void DeleteAttachment(int64_t id,
+                            FileContentType attachment)
+      {
+        return transaction_.DeleteAttachment(id, attachment);
+      }
+      
+      void DeleteMetadata(int64_t id,
+                          MetadataType type)
+      {
+        transaction_.DeleteMetadata(id, type);
+      }
+
+      void DeleteResource(int64_t id)
+      {
+        transaction_.DeleteResource(id);
+      }
+
+      void LogChange(int64_t internalId,
+                     ChangeType changeType,
+                     ResourceType resourceType,
+                     const std::string& publicId);
+
+      void LogExportedResource(const ExportedResource& resource)
+      {
+        transaction_.LogExportedResource(resource);
+      }
+
+      void SetGlobalProperty(GlobalProperty property,
+                             bool shared,
+                             const std::string& value)
+      {
+        transaction_.SetGlobalProperty(property, shared, value);
+      }
+
+      void SetMetadata(int64_t id,
+                       MetadataType type,
+                       const std::string& value,
+                       int64_t revision)
+      {
+        return transaction_.SetMetadata(id, type, value, revision);
+      }
+
+      void SetProtectedPatient(int64_t internalId, 
+                               bool isProtected)
+      {
+        transaction_.SetProtectedPatient(internalId, isProtected);
+      }
+
+      void SetResourcesContent(const ResourcesContent& content)
+      {
+        transaction_.SetResourcesContent(content);
+      }
+
+      void Recycle(uint64_t maximumStorageSize,
+                   unsigned int maximumPatients,
+                   uint64_t addedInstanceSize,
+                   const std::string& newPatientId);
+    };
+
+
+    class IReadOnlyOperations : public boost::noncopyable
+    {
+    public:
+      virtual ~IReadOnlyOperations()
+      {
+      }
+
+      virtual void Apply(ReadOnlyTransaction& transaction) = 0;
+    };
+
+
+    class IReadWriteOperations : public boost::noncopyable
+    {
+    public:
+      virtual ~IReadWriteOperations()
+      {
+      }
+
+      virtual void Apply(ReadWriteTransaction& transaction) = 0;
+    };
+    
+
+  private:
+    class MainDicomTagsRegistry;
+    class Transaction;
+
+    IDatabaseWrapper&                            db_;
+    boost::shared_ptr<MainDicomTagsRegistry>     mainDicomTagsRegistry_;  // "shared_ptr" because of PImpl
+    bool                                         hasFlushToDisk_;
+
+    // Mutex to protect the configuration options
+    boost::shared_mutex                          mutex_;
+    std::unique_ptr<ITransactionContextFactory>  factory_;
+    unsigned int                                 maxRetries_;
+
+    void NormalizeLookup(std::vector<DatabaseConstraint>& target,
+                         const DatabaseLookup& source,
+                         ResourceType level) const;
+
+    void ApplyInternal(IReadOnlyOperations* readOperations,
+                       IReadWriteOperations* writeOperations);
+
+  protected:
+    void StandaloneRecycling(uint64_t maximumStorageSize,
+                             unsigned int maximumPatientCount);
+
+  public:
+    explicit StatelessDatabaseOperations(IDatabaseWrapper& database);
+
+    void SetTransactionContextFactory(ITransactionContextFactory* factory /* takes ownership */);
+
+    // Only used to handle "ErrorCode_DatabaseCannotSerialize" in the
+    // case of collision between multiple writers
+    void SetMaxDatabaseRetries(unsigned int maxRetries);
+    
+    // It is assumed that "GetDatabaseVersion()" can run out of a
+    // database transaction
+    unsigned int GetDatabaseVersion()
+    {
+      return db_.GetDatabaseVersion();
+    }
+
+    void FlushToDisk();
+
+    bool HasFlushToDisk() const
+    {
+      return hasFlushToDisk_;
+    }
+
+    void Apply(IReadOnlyOperations& operations);
+  
+    void Apply(IReadWriteOperations& operations);
+
+    bool ExpandResource(Json::Value& target,
+                        const std::string& publicId,
+                        ResourceType level);
+
+    void GetAllMetadata(std::map<MetadataType, std::string>& target,
+                        const std::string& publicId,
+                        ResourceType level);
+
+    void GetAllUuids(std::list<std::string>& target,
+                     ResourceType resourceType);
+
+    void GetAllUuids(std::list<std::string>& target,
+                     ResourceType resourceType,
+                     size_t since,
+                     size_t limit);
+
+    void GetGlobalStatistics(/* out */ uint64_t& diskSize,
+                             /* out */ uint64_t& uncompressedSize,
+                             /* out */ uint64_t& countPatients, 
+                             /* out */ uint64_t& countStudies, 
+                             /* out */ uint64_t& countSeries, 
+                             /* out */ uint64_t& countInstances);
+
+    bool LookupAttachment(FileInfo& attachment,
+                          int64_t& revision,
+                          const std::string& instancePublicId,
+                          FileContentType contentType);
+
+    void GetChanges(Json::Value& target,
+                    int64_t since,
+                    unsigned int maxResults);
+
+    void GetLastChange(Json::Value& target);
+
+    void GetExportedResources(Json::Value& target,
+                              int64_t since,
+                              unsigned int maxResults);
+
+    void GetLastExportedResource(Json::Value& target);
+
+    bool IsProtectedPatient(const std::string& publicId);
+
+    void GetChildren(std::list<std::string>& result,
+                     const std::string& publicId);
+
+    void GetChildInstances(std::list<std::string>& result,
+                           const std::string& publicId);
+
+    bool LookupMetadata(std::string& target,
+                        int64_t& revision,
+                        const std::string& publicId,
+                        ResourceType expectedType,
+                        MetadataType type);
+
+    void ListAvailableAttachments(std::set<FileContentType>& target,
+                                  const std::string& publicId,
+                                  ResourceType expectedType);
+
+    bool LookupParent(std::string& target,
+                      const std::string& publicId);
+
+    void GetResourceStatistics(/* out */ ResourceType& type,
+                               /* out */ uint64_t& diskSize, 
+                               /* out */ uint64_t& uncompressedSize, 
+                               /* out */ unsigned int& countStudies, 
+                               /* out */ unsigned int& countSeries, 
+                               /* out */ unsigned int& countInstances, 
+                               /* out */ uint64_t& dicomDiskSize, 
+                               /* out */ uint64_t& dicomUncompressedSize, 
+                               const std::string& publicId);
+
+    void LookupIdentifierExact(std::vector<std::string>& result,
+                               ResourceType level,
+                               const DicomTag& tag,
+                               const std::string& value);
+
+    bool LookupGlobalProperty(std::string& value,
+                              GlobalProperty property,
+                              bool shared);
+
+    std::string GetGlobalProperty(GlobalProperty property,
+                                  bool shared,
+                                  const std::string& defaultValue);
+
+    bool GetMainDicomTags(DicomMap& result,
+                          const std::string& publicId,
+                          ResourceType expectedType,
+                          ResourceType levelOfInterest);
+
+    // Only applicable at the instance level
+    bool GetAllMainDicomTags(DicomMap& result,
+                             const std::string& instancePublicId);
+
+    bool LookupResourceType(ResourceType& type,
+                            const std::string& publicId);
+
+    bool LookupParent(std::string& target,
+                      const std::string& publicId,
+                      ResourceType parentType);
+
+    void ApplyLookupResources(std::vector<std::string>& resourcesId,
+                              std::vector<std::string>* instancesId,  // Can be NULL if not needed
+                              const DatabaseLookup& lookup,
+                              ResourceType queryLevel,
+                              size_t limit);
+
+    bool DeleteResource(Json::Value& target /* out */,
+                        const std::string& uuid,
+                        ResourceType expectedType);
+
+    void LogExportedResource(const std::string& publicId,
+                             const std::string& remoteModality);
+
+    void SetProtectedPatient(const std::string& publicId,
+                             bool isProtected);
+
+    void SetMetadata(int64_t& newRevision /*out*/,
+                     const std::string& publicId,
+                     MetadataType type,
+                     const std::string& value,
+                     bool hasOldRevision,
+                     int64_t oldRevision);
+
+    // Same as "SetMetadata()", but doesn't care about revisions
+    void OverwriteMetadata(const std::string& publicId,
+                           MetadataType type,
+                           const std::string& value);
+
+    bool DeleteMetadata(const std::string& publicId,
+                        MetadataType type,
+                        bool hasRevision,
+                        int64_t revision);
+
+    uint64_t IncrementGlobalSequence(GlobalProperty sequence,
+                                     bool shared);
+
+    void DeleteChanges();
+
+    void DeleteExportedResources();
+
+    void SetGlobalProperty(GlobalProperty property,
+                           bool shared,
+                           const std::string& value);
+
+    bool DeleteAttachment(const std::string& publicId,
+                          FileContentType type,
+                          bool hasRevision,
+                          int64_t revision);
+
+    void LogChange(int64_t internalId,
+                   ChangeType changeType,
+                   const std::string& publicId,
+                   ResourceType level);
+
+    void ReconstructInstance(const ParsedDicomFile& dicom);
+
+    StoreStatus Store(std::map<MetadataType, std::string>& instanceMetadata,
+                      const DicomMap& dicomSummary,
+                      const Attachments& attachments,
+                      const MetadataMap& metadata,
+                      const DicomInstanceOrigin& origin,
+                      bool overwrite,
+                      bool hasTransferSyntax,
+                      DicomTransferSyntax transferSyntax,
+                      bool hasPixelDataOffset,
+                      uint64_t pixelDataOffset,
+                      uint64_t maximumStorageSize,
+                      unsigned int maximumPatients);
+
+    StoreStatus AddAttachment(int64_t& newRevision /*out*/,
+                              const FileInfo& attachment,
+                              const std::string& publicId,
+                              uint64_t maximumStorageSize,
+                              unsigned int maximumPatients,
+                              bool hasOldRevision,
+                              int64_t oldRevision);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/VoidDatabaseListener.cpp	Tue Apr 20 18:11:29 2021 +0200
@@ -0,0 +1,57 @@
+/**
+ * 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 General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * 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 "../PrecompiledHeadersServer.h"
+#include "VoidDatabaseListener.h"
+
+#include "../../../OrthancFramework/Sources/OrthancException.h"
+
+namespace Orthanc
+{
+  void VoidDatabaseListener::SignalRemainingAncestor(ResourceType parentType,
+                                                     const std::string& publicId)
+  {
+    throw OrthancException(ErrorCode_InternalError);
+  }
+      
+  void VoidDatabaseListener::SignalAttachmentDeleted(const FileInfo& info)
+  {
+    throw OrthancException(ErrorCode_InternalError);
+  }
+
+  void VoidDatabaseListener::SignalResourceDeleted(ResourceType type,
+                                                   const std::string& publicId)
+  {
+    throw OrthancException(ErrorCode_InternalError);
+  }      
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/VoidDatabaseListener.h	Tue Apr 20 18:11:29 2021 +0200
@@ -0,0 +1,56 @@
+/**
+ * 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 General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * 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
+
+#include "../../../OrthancFramework/Sources/Compatibility.h"
+#include "IDatabaseListener.h"
+
+namespace Orthanc
+{
+  /**
+   * This is a listener that can be used for transactions that do
+   * not create/delete attachments or resources.
+   **/
+  class VoidDatabaseListener : public IDatabaseListener
+  {
+  public:
+    virtual void SignalRemainingAncestor(ResourceType parentType,
+                                         const std::string& publicId) ORTHANC_OVERRIDE;
+      
+    virtual void SignalAttachmentDeleted(const FileInfo& info) ORTHANC_OVERRIDE;
+
+    virtual void SignalResourceDeleted(ResourceType type,
+                                       const std::string& publicId) ORTHANC_OVERRIDE;
+  };
+}
--- a/OrthancServer/Sources/LuaScripting.cpp	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancServer/Sources/LuaScripting.cpp	Tue Apr 20 18:11:29 2021 +0200
@@ -38,6 +38,7 @@
 #include "OrthancRestApi/OrthancRestApi.h"
 #include "ServerContext.h"
 
+#include "../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
 #include "../../OrthancFramework/Sources/HttpServer/StringHttpOutput.h"
 #include "../../OrthancFramework/Sources/Logging.h"
 #include "../../OrthancFramework/Sources/Lua/LuaFunctionCall.h"
@@ -126,6 +127,82 @@
   private:
     ServerIndexChange  change_;
 
+    class GetInfoOperations : public ServerIndex::IReadOnlyOperations
+    {
+    private:
+      const ServerIndexChange&            change_;
+      bool                                ok_;
+      DicomMap                            tags_;
+      std::map<MetadataType, std::string> metadata_;      
+
+    public:
+      explicit GetInfoOperations(const ServerIndexChange& change) :
+        change_(change),
+        ok_(false)
+      {
+      }
+      
+      virtual void Apply(ServerIndex::ReadOnlyTransaction& transaction) ORTHANC_OVERRIDE
+      {
+        int64_t internalId;
+        ResourceType level;
+        if (transaction.LookupResource(internalId, level, change_.GetPublicId()) &&
+            level == change_.GetResourceType())
+        {
+          transaction.GetMainDicomTags(tags_, internalId);
+          transaction.GetAllMetadata(metadata_, internalId);
+          ok_ = true;
+        }
+      }
+
+      void CallLua(LuaScripting& that,
+                   const char* name) const
+      {
+        if (ok_)
+        {
+          Json::Value formattedMetadata = Json::objectValue;
+
+          for (std::map<MetadataType, std::string>::const_iterator 
+                 it = metadata_.begin(); it != metadata_.end(); ++it)
+          {
+            std::string key = EnumerationToString(it->first);
+            formattedMetadata[key] = it->second;
+          }      
+
+          {
+            LuaScripting::Lock lock(that);
+
+            if (lock.GetLua().IsExistingFunction(name))
+            {
+              that.InitializeJob();
+
+              Json::Value json = Json::objectValue;
+
+              if (change_.GetResourceType() == ResourceType_Study)
+              {
+                DicomMap t;
+                tags_.ExtractStudyInformation(t);  // Discard patient-related tags
+                FromDcmtkBridge::ToJson(json, t, true);
+              }
+              else
+              {
+                FromDcmtkBridge::ToJson(json, tags_, true);
+              }
+
+              LuaFunctionCall call(lock.GetLua(), name);
+              call.PushString(change_.GetPublicId());
+              call.PushJson(json);
+              call.PushJson(formattedMetadata);
+              call.Execute();
+
+              that.SubmitJob();
+            }
+          }
+        }
+      }
+    };
+    
+
   public:
     explicit StableResourceEvent(const ServerIndexChange& change) :
       change_(change)
@@ -164,39 +241,9 @@
         }
       }
       
-      Json::Value tags;
-      
-      if (that.context_.GetIndex().LookupResource(tags, change_.GetPublicId(), change_.GetResourceType()))
-      {
-        std::map<MetadataType, std::string> metadata;
-        that.context_.GetIndex().GetAllMetadata(metadata, change_.GetPublicId(), change_.GetResourceType());
-        
-        Json::Value formattedMetadata = Json::objectValue;
-
-        for (std::map<MetadataType, std::string>::const_iterator 
-               it = metadata.begin(); it != metadata.end(); ++it)
-        {
-          std::string key = EnumerationToString(it->first);
-          formattedMetadata[key] = it->second;
-        }      
-
-        {
-          LuaScripting::Lock lock(that);
-
-          if (lock.GetLua().IsExistingFunction(name))
-          {
-            that.InitializeJob();
-
-            LuaFunctionCall call(lock.GetLua(), name);
-            call.PushString(change_.GetPublicId());
-            call.PushJson(tags["MainDicomTags"]);
-            call.PushJson(formattedMetadata);
-            call.Execute();
-
-            that.SubmitJob();
-          }
-        }
-      }
+      GetInfoOperations operations(change_);
+      that.context_.GetIndex().Apply(operations);
+      operations.CallLua(that, name);
     }
   };
 
--- a/OrthancServer/Sources/OrthancConfiguration.cpp	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancServer/Sources/OrthancConfiguration.cpp	Tue Apr 20 18:11:29 2021 +0200
@@ -53,6 +53,7 @@
 static const char* const ORTHANC_PEERS = "OrthancPeers";
 static const char* const ORTHANC_PEERS_IN_DB = "OrthancPeersInDatabase";
 static const char* const TEMPORARY_DIRECTORY = "TemporaryDirectory";
+static const char* const DATABASE_SERVER_IDENTIFIER = "DatabaseServerIdentifier";
 
 namespace Orthanc
 {
@@ -254,7 +255,7 @@
       }
       else
       {
-        std::string property = serverIndex_->GetGlobalProperty(GlobalProperty_Modalities, "{}");
+        std::string property = serverIndex_->GetGlobalProperty(GlobalProperty_Modalities, false /* not shared */, "{}");
 
         Json::Value modalities;
         if (Toolbox::ReadJson(modalities, property))
@@ -293,7 +294,7 @@
       }
       else
       {
-        std::string property = serverIndex_->GetGlobalProperty(GlobalProperty_Peers, "{}");
+        std::string property = serverIndex_->GetGlobalProperty(GlobalProperty_Peers, false /* not shared */, "{}");
 
         Json::Value peers;
         if (Toolbox::ReadJson(peers, property))
@@ -369,7 +370,7 @@
         std::string s;
         Toolbox::WriteFastJson(s, modalities);
         
-        serverIndex_->SetGlobalProperty(GlobalProperty_Modalities, s);
+        serverIndex_->SetGlobalProperty(GlobalProperty_Modalities, false /* not shared */, s);
       }
     }
     else
@@ -401,7 +402,7 @@
         std::string s;
         Toolbox::WriteFastJson(s, peers);
 
-        serverIndex_->SetGlobalProperty(GlobalProperty_Peers, s);
+        serverIndex_->SetGlobalProperty(GlobalProperty_Peers, false /* not shared */, s);
       }
     }
     else
@@ -1014,6 +1015,59 @@
   }
 
 
+  std::string OrthancConfiguration::GetDatabaseServerIdentifier() const
+  {
+    std::string id;
+
+    if (LookupStringParameter(id, DATABASE_SERVER_IDENTIFIER))
+    {
+      if (id.empty())
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange, "Global configuration option \"" +
+                               std::string(DATABASE_SERVER_IDENTIFIER) + "\" cannot be empty");
+      }
+      else
+      {
+        return id;
+      }
+    }
+    else
+    {
+      std::set<std::string> items;
+
+      {
+        std::set<std::string> mac;
+        SystemToolbox::GetMacAddresses(mac);
+
+        for (std::set<std::string>::const_iterator it = mac.begin(); it != mac.end(); ++it)
+        {
+          items.insert("mac=" + *it);
+        }
+      }
+
+      items.insert("aet=" + GetStringParameter("DicomAet", "ORTHANC"));
+      items.insert("dicom-port=" + boost::lexical_cast<std::string>(GetUnsignedIntegerParameter("DicomPort", 4242)));
+      items.insert("http-port=" + boost::lexical_cast<std::string>(GetUnsignedIntegerParameter("HttpPort", 8042)));
+
+      for (std::set<std::string>::const_iterator it = items.begin(); it != items.end(); ++it)
+      {
+        if (id.empty())
+        {
+          id = *it;
+        }
+        else
+        {
+          id += ("|" + *it);
+        }
+      }
+
+      std::string hash;
+      Toolbox::ComputeSHA1(hash, id);
+      return hash;
+    }
+  }
+
+  
   void OrthancConfiguration::DefaultExtractDicomSummary(DicomMap& target,
                                                         const ParsedDicomFile& dicom)
   {
--- a/OrthancServer/Sources/OrthancConfiguration.h	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancServer/Sources/OrthancConfiguration.h	Tue Apr 20 18:11:29 2021 +0200
@@ -251,6 +251,8 @@
 
     void GetAcceptedTransferSyntaxes(std::set<DicomTransferSyntax>& target) const;
 
+    std::string GetDatabaseServerIdentifier() const;
+
     static void DefaultExtractDicomSummary(DicomMap& target,
                                            const ParsedDicomFile& dicom);
 
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Tue Apr 20 18:11:29 2021 +0200
@@ -53,7 +53,7 @@
   
   static std::string GeneratePatientName(ServerContext& context)
   {
-    uint64_t seq = context.GetIndex().IncrementGlobalSequence(GlobalProperty_AnonymizationSequence);
+    uint64_t seq = context.GetIndex().IncrementGlobalSequence(GlobalProperty_AnonymizationSequence, true /* shared */);
     return "Anonymized" + boost::lexical_cast<std::string>(seq);
   }
 
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Tue Apr 20 18:11:29 2021 +0200
@@ -51,8 +51,6 @@
 #include "../ServerToolbox.h"
 #include "../SliceOrdering.h"
 
-#include "../../Plugins/Engine/OrthancPlugins.h"
-
 // This "include" is mandatory for Release builds using Linux Standard Base
 #include <boost/math/special_functions/round.hpp>
 
@@ -65,6 +63,9 @@
 static Orthanc::Semaphore throttlingSemaphore_(4);  // TODO => PARAMETER?
 
 
+static const std::string CHECK_REVISIONS = "CheckRevisions";
+
+
 namespace Orthanc
 {
   static std::string GetDocumentationSampleResource(ResourceType type)
@@ -168,10 +169,10 @@
     {
       if (expand)
       {
-        Json::Value item;
-        if (index.LookupResource(item, *resource, level))
+        Json::Value expanded;
+        if (index.ExpandResource(expanded, *resource, level))
         {
-          answer.append(item);
+          answer.append(expanded);
         }
       }
       else
@@ -255,11 +256,11 @@
         .SetHttpGetSample(GetDocumentationSampleResource(resourceType), true);
       return;
     }
-    
-    Json::Value result;
-    if (OrthancRestApi::GetIndex(call).LookupResource(result, call.GetUriComponent("id", ""), resourceType))
+
+    Json::Value json;
+    if (OrthancRestApi::GetIndex(call).ExpandResource(json, call.GetUriComponent("id", ""), resourceType))
     {
-      call.GetOutput().AnswerJson(result);
+      call.GetOutput().AnswerJson(json);
     }
   }
 
@@ -1414,12 +1415,13 @@
       return;
     }
 
-    std::map<MetadataType, std::string> metadata;
-
     assert(!call.GetFullUri().empty());
     const std::string publicId = call.GetUriComponent("id", "");
-    const ResourceType level = StringToResourceType(call.GetFullUri() [0].c_str());
-
+    ResourceType level = StringToResourceType(call.GetFullUri() [0].c_str());
+
+    typedef std::map<MetadataType, std::string>  Metadata;
+
+    Metadata metadata;
     OrthancRestApi::GetIndex(call).GetAllMetadata(metadata, publicId, level);
 
     Json::Value result;
@@ -1428,8 +1430,7 @@
     {
       result = Json::objectValue;
       
-      for (std::map<MetadataType, std::string>::const_iterator 
-             it = metadata.begin(); it != metadata.end(); ++it)
+      for (Metadata::const_iterator it = metadata.begin(); it != metadata.end(); ++it)
       {
         std::string key = EnumerationToString(it->first);
         result[key] = it->second;
@@ -1439,8 +1440,7 @@
     {
       result = Json::arrayValue;
       
-      for (std::map<MetadataType, std::string>::const_iterator 
-             it = metadata.begin(); it != metadata.end(); ++it)
+      for (Metadata::const_iterator it = metadata.begin(); it != metadata.end(); ++it)
       {       
         result.append(EnumerationToString(it->first));
       }
@@ -1450,6 +1450,37 @@
   }
 
 
+  static bool GetRevisionHeader(int64_t& revision /* out */,
+                                const RestApiCall& call,
+                                const std::string& header)
+  {
+    std::string lower;
+    Toolbox::ToLowerCase(lower, header);
+    
+    HttpToolbox::Arguments::const_iterator found = call.GetHttpHeaders().find(lower);
+    if (found == call.GetHttpHeaders().end())
+    {
+      return false;
+    }
+    else
+    {
+      std::string value = Toolbox::StripSpaces(found->second);
+      Toolbox::RemoveSurroundingQuotes(value);
+
+      try
+      {
+        revision = boost::lexical_cast<int64_t>(value);
+        return true;
+      }
+      catch (boost::bad_lexical_cast&)
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange, "The \"" + header +
+                               "\" HTTP header should contain the revision as an integer, but found: " + value);
+      }
+    }
+  }
+
+
   static void GetMetadata(RestApiGetCall& call)
   {
     if (call.IsDocumentation())
@@ -1462,7 +1493,9 @@
         .SetDescription("Get the value of a metadata that is associated with the given " + r)
         .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest")
         .SetUriArgument("name", "The name of the metadata, or its index (cf. `UserMetadata` configuration option)")
-        .AddAnswerType(MimeType_PlainText, "Value of the metadata");
+        .AddAnswerType(MimeType_PlainText, "Value of the metadata")
+        .SetAnswerHeader("ETag", "Revision of the metadata, to be used in further `PUT` or `DELETE` operations")
+        .SetHttpHeader("If-None-Match", "Optional revision of the metadata, to check if its content has changed");
       return;
     }
 
@@ -1474,9 +1507,22 @@
     MetadataType metadata = StringToMetadata(name);
 
     std::string value;
-    if (OrthancRestApi::GetIndex(call).LookupMetadata(value, publicId, level, metadata))
+    int64_t revision;
+    if (OrthancRestApi::GetIndex(call).LookupMetadata(value, revision, publicId, level, metadata))
     {
-      call.GetOutput().AnswerBuffer(value, MimeType_PlainText);
+      call.GetOutput().GetLowLevelOutput().
+        AddHeader("ETag", "\"" + boost::lexical_cast<std::string>(revision) + "\"");  // New in Orthanc 1.9.2
+
+      int64_t userRevision;
+      if (GetRevisionHeader(userRevision, call, "If-None-Match") &&
+          revision == userRevision)
+      {
+        call.GetOutput().GetLowLevelOutput().SendStatus(HttpStatus_304_NotModified);
+      }
+      else
+      {
+        call.GetOutput().AnswerBuffer(value, MimeType_PlainText);
+      }
     }
   }
 
@@ -1493,20 +1539,48 @@
         .SetDescription("Delete some metadata associated with the given DICOM " + r +
                         ". This call will fail if trying to delete a system metadata (i.e. whose index is < 1024).")
         .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest")
-        .SetUriArgument("name", "The name of the metadata, or its index (cf. `UserMetadata` configuration option)");
+        .SetUriArgument("name", "The name of the metadata, or its index (cf. `UserMetadata` configuration option)")
+        .SetHttpHeader("If-Match", "Revision of the metadata, to check if its content has not changed and can "
+                       "be deleted. This header is mandatory if `CheckRevision` option is `true`.");
       return;
     }
 
     CheckValidResourceType(call);
-
-    std::string publicId = call.GetUriComponent("id", "");
+    const std::string publicId = call.GetUriComponent("id", "");
+
     std::string name = call.GetUriComponent("name", "");
     MetadataType metadata = StringToMetadata(name);
 
     if (IsUserMetadata(metadata))  // It is forbidden to modify internal metadata
-    {      
-      OrthancRestApi::GetIndex(call).DeleteMetadata(publicId, metadata);
-      call.GetOutput().AnswerBuffer("", MimeType_PlainText);
+    {
+      bool found;
+      int64_t revision;
+      if (GetRevisionHeader(revision, call, "if-match"))
+      {
+        found = OrthancRestApi::GetIndex(call).DeleteMetadata(publicId, metadata, true, revision);
+      }
+      else
+      {
+        OrthancConfiguration::ReaderLock lock;
+        if (lock.GetConfiguration().GetBooleanParameter(CHECK_REVISIONS, false))
+        {
+          throw OrthancException(ErrorCode_Revision,
+                                 "HTTP header \"If-Match\" is missing, as \"CheckRevision\" is \"true\"");
+        }
+        else
+        {
+          found = OrthancRestApi::GetIndex(call).DeleteMetadata(publicId, metadata, false, -1 /* dummy value */);
+        }
+      }
+
+      if (found)
+      {
+        call.GetOutput().AnswerBuffer("", MimeType_PlainText);
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_UnknownResource);
+      }
     }
     else
     {
@@ -1528,7 +1602,8 @@
                         ". This call will fail if trying to modify a system metadata (i.e. whose index is < 1024).")
         .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest")
         .SetUriArgument("name", "The name of the metadata, or its index (cf. `UserMetadata` configuration option)")
-        .AddRequestType(MimeType_PlainText, "String value of the metadata");
+        .AddRequestType(MimeType_PlainText, "String value of the metadata")
+        .SetHttpHeader("If-Match", "Revision of the metadata, if this is not the first time this metadata is set.");
       return;
     }
 
@@ -1543,8 +1618,28 @@
 
     if (IsUserMetadata(metadata))  // It is forbidden to modify internal metadata
     {
-      // It is forbidden to modify internal metadata
-      OrthancRestApi::GetIndex(call).SetMetadata(publicId, metadata, value);
+      int64_t oldRevision;
+      bool hasOldRevision = GetRevisionHeader(oldRevision, call, "if-match");
+
+      if (!hasOldRevision)
+      {
+        OrthancConfiguration::ReaderLock lock;
+        if (lock.GetConfiguration().GetBooleanParameter(CHECK_REVISIONS, false))
+        {
+          // "StatelessDatabaseOperations::SetMetadata()" will ignore
+          // the actual value of "oldRevision" if the metadata is
+          // inexistent as expected
+          hasOldRevision = true;
+          oldRevision = -1;  // dummy value
+        }
+      }
+
+      int64_t newRevision;
+      OrthancRestApi::GetIndex(call).SetMetadata(newRevision, publicId, metadata, value, hasOldRevision, oldRevision);
+
+      call.GetOutput().GetLowLevelOutput().
+        AddHeader("ETag", "\"" + boost::lexical_cast<std::string>(newRevision) + "\"");  // New in Orthanc 1.9.2
+      
       call.GetOutput().AnswerBuffer("", MimeType_PlainText);
     }
     else
@@ -1591,15 +1686,48 @@
   }
 
 
-  static bool GetAttachmentInfo(FileInfo& info, RestApiCall& call)
+  static void AddAttachmentDocumentation(RestApiGetCall& call,
+                                         const std::string& resourceType)
+  {
+    call.GetDocumentation()
+      .SetUriArgument("id", "Orthanc identifier of the " + resourceType + " of interest")
+      .SetUriArgument("name", "The name of the attachment, or its index (cf. `UserContentType` configuration option)")
+      .SetAnswerHeader("ETag", "Revision of the attachment, to be used in further `PUT` or `DELETE` operations")
+      .SetHttpHeader("If-None-Match", "Optional revision of the attachment, to check if its content has changed");
+  }
+
+  
+  static bool GetAttachmentInfo(FileInfo& info,
+                                RestApiGetCall& call)
   {
     CheckValidResourceType(call);
  
-    std::string publicId = call.GetUriComponent("id", "");
-    std::string name = call.GetUriComponent("name", "");
+    const std::string publicId = call.GetUriComponent("id", "");
+    const std::string name = call.GetUriComponent("name", "");
     FileContentType contentType = StringToContentType(name);
 
-    return OrthancRestApi::GetIndex(call).LookupAttachment(info, publicId, contentType);
+    int64_t revision;
+    if (OrthancRestApi::GetIndex(call).LookupAttachment(info, revision, publicId, contentType))
+    {
+      call.GetOutput().GetLowLevelOutput().
+        AddHeader("ETag", "\"" + boost::lexical_cast<std::string>(revision) + "\"");  // New in Orthanc 1.9.2
+
+      int64_t userRevision;
+      if (GetRevisionHeader(userRevision, call, "If-None-Match") &&
+          revision == userRevision)
+      {
+        call.GetOutput().GetLowLevelOutput().SendStatus(HttpStatus_304_NotModified);
+        return false;
+      }
+      else
+      {
+        return true;
+      }
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
   }
 
 
@@ -1609,12 +1737,11 @@
     {
       ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str());
       std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */);
+      AddAttachmentDocumentation(call, r);
       call.GetDocumentation()
         .SetTag("Other")
         .SetSummary("List operations on attachments")
         .SetDescription("Get the list of the operations that are available for attachments associated with the given " + r)
-        .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest")
-        .SetUriArgument("name", "The name of the attachment, or its index (cf. `UserContentType` configuration option)")
         .AddAnswerType(MimeType_Json, "List of the available operations")
         .SetHttpGetSample("https://demo.orthanc-server.com/instances/d94d9a03-3003b047-a4affc69-322313b2-680530a2/attachments/dicom", true);
       return;
@@ -1670,7 +1797,9 @@
                         std::string(uncompress ? "" : ". The attachment will not be decompressed if `StorageCompression` is `true`."))
         .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest")
         .SetUriArgument("name", "The name of the attachment, or its index (cf. `UserContentType` configuration option)")
-        .AddAnswerType(MimeType_Binary, "The attachment");
+        .AddAnswerType(MimeType_Binary, "The attachment")
+        .SetAnswerHeader("ETag", "Revision of the attachment, to be used in further `PUT` or `DELETE` operations")
+        .SetHttpHeader("If-None-Match", "Optional revision of the metadata, to check if its content has changed");
       return;
     }
 
@@ -1683,14 +1812,32 @@
 
     if (uncompress)
     {
-      context.AnswerAttachment(call.GetOutput(), publicId, type);
+      FileInfo info;
+      if (GetAttachmentInfo(info, call))
+      {
+        context.AnswerAttachment(call.GetOutput(), publicId, type);
+      }
     }
     else
     {
       // Return the raw data (possibly compressed), as stored on the filesystem
       std::string content;
-      context.ReadAttachment(content, publicId, type, false);
-      call.GetOutput().AnswerBuffer(content, MimeType_Binary);
+      int64_t revision;
+      context.ReadAttachment(content, revision, publicId, type, false);
+
+      call.GetOutput().GetLowLevelOutput().
+        AddHeader("ETag", "\"" + boost::lexical_cast<std::string>(revision) + "\"");  // New in Orthanc 1.9.2
+
+      int64_t userRevision;
+      if (GetRevisionHeader(userRevision, call, "If-None-Match") &&
+          revision == userRevision)
+      {
+        call.GetOutput().GetLowLevelOutput().SendStatus(HttpStatus_304_NotModified);
+      }
+      else
+      {
+        call.GetOutput().AnswerBuffer(content, MimeType_Binary);
+      }
     }
   }
 
@@ -1701,12 +1848,11 @@
     {
       ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str());
       std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */);
+      AddAttachmentDocumentation(call, r);
       call.GetDocumentation()
         .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */))
         .SetSummary("Get size of attachment")
         .SetDescription("Get the size of one attachment associated with the given " + r)
-        .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest")
-        .SetUriArgument("name", "The name of the attachment, or its index (cf. `UserContentType` configuration option)")
         .AddAnswerType(MimeType_PlainText, "The size of the attachment");
       return;
     }
@@ -1725,13 +1871,12 @@
     {
       ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str());
       std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */);
+      AddAttachmentDocumentation(call, r);
       call.GetDocumentation()
         .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */))
         .SetSummary("Get size of attachment on disk")
         .SetDescription("Get the size of one attachment associated with the given " + r + ", as stored on the disk. "
                         "This is different from `.../size` iff `EnableStorage` is `true`.")
-        .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest")
-        .SetUriArgument("name", "The name of the attachment, or its index (cf. `UserContentType` configuration option)")
         .AddAnswerType(MimeType_PlainText, "The size of the attachment, as stored on the disk");
       return;
     }
@@ -1750,12 +1895,11 @@
     {
       ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str());
       std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */);
+      AddAttachmentDocumentation(call, r);
       call.GetDocumentation()
         .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */))
         .SetSummary("Get MD5 of attachment")
         .SetDescription("Get the MD5 hash of one attachment associated with the given " + r)
-        .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest")
-        .SetUriArgument("name", "The name of the attachment, or its index (cf. `UserContentType` configuration option)")
         .AddAnswerType(MimeType_PlainText, "The MD5 of the attachment");
       return;
     }
@@ -1775,13 +1919,12 @@
     {
       ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str());
       std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */);
+      AddAttachmentDocumentation(call, r);
       call.GetDocumentation()
         .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */))
         .SetSummary("Get MD5 of attachment on disk")
         .SetDescription("Get the MD5 hash of one attachment associated with the given " + r + ", as stored on the disk. "
                         "This is different from `.../md5` iff `EnableStorage` is `true`.")
-        .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest")
-        .SetUriArgument("name", "The name of the attachment, or its index (cf. `UserContentType` configuration option)")
         .AddAnswerType(MimeType_PlainText, "The MD5 of the attachment, as stored on the disk");
       return;
     }
@@ -1816,9 +1959,11 @@
 
     std::string publicId = call.GetUriComponent("id", "");
     std::string name = call.GetUriComponent("name", "");
+    FileContentType contentType = StringToContentType(name);
 
     FileInfo info;
-    if (!GetAttachmentInfo(info, call) ||
+    int64_t revision;  // Ignored
+    if (!OrthancRestApi::GetIndex(call).LookupAttachment(info, revision, publicId, contentType) ||
         info.GetCompressedMD5() == "" ||
         info.GetUncompressedMD5() == "")
     {
@@ -1830,7 +1975,7 @@
 
     // First check whether the compressed data is correctly stored in the disk
     std::string data;
-    context.ReadAttachment(data, publicId, StringToContentType(name), false);
+    context.ReadAttachment(data, revision, publicId, StringToContentType(name), false);
 
     std::string actualMD5;
     Toolbox::ComputeMD5(actualMD5, data);
@@ -1845,7 +1990,7 @@
       }
       else
       {
-        context.ReadAttachment(data, publicId, StringToContentType(name), true);        
+        context.ReadAttachment(data, revision, publicId, StringToContentType(name), true);        
         Toolbox::ComputeMD5(actualMD5, data);
         ok = (actualMD5 == info.GetUncompressedMD5());
       }
@@ -1877,7 +2022,8 @@
         .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest")
         .SetUriArgument("name", "The name of the attachment, or its index (cf. `UserContentType` configuration option)")
         .AddRequestType(MimeType_Binary, "Binary data containing the attachment")
-        .AddAnswerType(MimeType_Json, "Empty JSON object in the case of a success");
+        .AddAnswerType(MimeType_Json, "Empty JSON object in the case of a success")
+        .SetHttpHeader("If-Match", "Revision of the attachment, if this is not the first time this attachment is set.");
       return;
     }
 
@@ -1888,9 +2034,31 @@
     std::string name = call.GetUriComponent("name", "");
 
     FileContentType contentType = StringToContentType(name);
-    if (IsUserContentType(contentType) &&  // It is forbidden to modify internal attachments
-        context.AddAttachment(publicId, StringToContentType(name), call.GetBodyData(), call.GetBodySize()))
+    if (IsUserContentType(contentType))  // It is forbidden to modify internal attachments
     {
+      int64_t oldRevision;
+      bool hasOldRevision = GetRevisionHeader(oldRevision, call, "if-match");
+
+      if (!hasOldRevision)
+      {
+        OrthancConfiguration::ReaderLock lock;
+        if (lock.GetConfiguration().GetBooleanParameter(CHECK_REVISIONS, false))
+        {
+          // "StatelessDatabaseOperations::AddAttachment()" will ignore
+          // the actual value of "oldRevision" if the metadata is
+          // inexistent as expected
+          hasOldRevision = true;
+          oldRevision = -1;  // dummy value
+        }
+      }
+
+      int64_t newRevision;
+      context.AddAttachment(newRevision, publicId, StringToContentType(name), call.GetBodyData(),
+                            call.GetBodySize(), hasOldRevision, oldRevision);
+
+      call.GetOutput().GetLowLevelOutput().
+        AddHeader("ETag", "\"" + boost::lexical_cast<std::string>(newRevision) + "\"");  // New in Orthanc 1.9.2
+      
       call.GetOutput().AnswerBuffer("{}", MimeType_Json);
     }
     else
@@ -1912,7 +2080,9 @@
         .SetDescription("Delete an attachment associated with the given DICOM " + r +
                         ". This call will fail if trying to delete a system attachment (i.e. whose index is < 1024).")
         .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest")
-        .SetUriArgument("name", "The name of the attachment, or its index (cf. `UserContentType` configuration option)");
+        .SetUriArgument("name", "The name of the attachment, or its index (cf. `UserContentType` configuration option)")
+        .SetHttpHeader("If-Match", "Revision of the attachment, to check if its content has not changed and can "
+                       "be deleted. This header is mandatory if `CheckRevision` option is `true`.");
       return;
     }
 
@@ -1947,8 +2117,34 @@
 
     if (allowed) 
     {
-      OrthancRestApi::GetIndex(call).DeleteAttachment(publicId, contentType);
-      call.GetOutput().AnswerBuffer("{}", MimeType_Json);
+      bool found;
+      int64_t revision;
+      if (GetRevisionHeader(revision, call, "if-match"))
+      {
+        found = OrthancRestApi::GetIndex(call).DeleteAttachment(publicId, contentType, true, revision);
+      }
+      else
+      {
+        OrthancConfiguration::ReaderLock lock;
+        if (lock.GetConfiguration().GetBooleanParameter(CHECK_REVISIONS, false))
+        {
+          throw OrthancException(ErrorCode_Revision,
+                                 "HTTP header \"If-Match\" is missing, as \"CheckRevision\" is \"true\"");
+        }
+        else
+        {
+          found = OrthancRestApi::GetIndex(call).DeleteAttachment(publicId, contentType, false, -1 /* dummy value */);
+        }
+      }
+
+      if (found)
+      {
+        call.GetOutput().AnswerBuffer("", MimeType_PlainText);
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_UnknownResource);
+      }
     }
     else
     {
@@ -1990,12 +2186,11 @@
     {
       ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str());
       std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */);
+      AddAttachmentDocumentation(call, r);
       call.GetDocumentation()
         .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */))
         .SetSummary("Is attachment compressed?")
         .SetDescription("Test whether the attachment has been stored as a compressed file on the disk.")
-        .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest")
-        .SetUriArgument("name", "The name of the attachment, or its index (cf. `UserContentType` configuration option)")
         .AddAnswerType(MimeType_PlainText, "`0` if the attachment was stored uncompressed, `1` if it was compressed");
       return;
     }
@@ -2544,11 +2739,10 @@
     for (std::list<std::string>::const_iterator
            it = a.begin(); it != a.end(); ++it)
     {
-      Json::Value item;
-
-      if (OrthancRestApi::GetIndex(call).LookupResource(item, *it, end))
+      Json::Value resource;
+      if (OrthancRestApi::GetIndex(call).ExpandResource(resource, *it, end))
       {
-        result.append(item);
+        result.append(resource);
       }
     }
 
@@ -2654,10 +2848,10 @@
 
     assert(currentType == end);
 
-    Json::Value result;
-    if (index.LookupResource(result, current, end))
+    Json::Value resource;
+    if (OrthancRestApi::GetIndex(call).ExpandResource(resource, current, end))
     {
-      call.GetOutput().AnswerJson(result);
+      call.GetOutput().AnswerJson(resource);
     }
   }
 
@@ -2792,7 +2986,7 @@
       for (std::list<std::string>::const_iterator 
              instance = instances.begin(); instance != instances.end(); ++instance)
       {
-        index.DeleteAttachment(*instance, FileContentType_DicomAsJson);
+        index.DeleteAttachment(*instance, FileContentType_DicomAsJson, false /* no revision checks */, -1 /* dummy */);
       }
     }
 
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp	Tue Apr 20 18:11:29 2021 +0200
@@ -61,29 +61,44 @@
  
   static void GetSystemInformation(RestApiGetCall& call)
   {
+    static const char* const API_VERSION = "ApiVersion";
+    static const char* const CHECK_REVISIONS = "CheckRevisions";
+    static const char* const DATABASE_BACKEND_PLUGIN = "DatabaseBackendPlugin";
+    static const char* const DATABASE_VERSION = "DatabaseVersion";
+    static const char* const DICOM_AET = "DicomAet";
+    static const char* const DICOM_PORT = "DicomPort";
+    static const char* const HTTP_PORT = "HttpPort";
+    static const char* const IS_HTTP_SERVER_SECURE = "IsHttpServerSecure";
+    static const char* const NAME = "Name";
+    static const char* const PLUGINS_ENABLED = "PluginsEnabled";
+    static const char* const STORAGE_AREA_PLUGIN = "StorageAreaPlugin";
+    static const char* const VERSION = "Version";
+    
     if (call.IsDocumentation())
     {
       call.GetDocumentation()
         .SetTag("System")
         .SetSummary("Get system information")
         .SetDescription("Get system information about Orthanc")
-        .SetAnswerField("ApiVersion", RestApiCallDocumentation::Type_Number, "Version of the REST API")
-        .SetAnswerField("Version", RestApiCallDocumentation::Type_String, "Version of Orthanc")
-        .SetAnswerField("DatabaseVersion", RestApiCallDocumentation::Type_Number,
+        .SetAnswerField(API_VERSION, RestApiCallDocumentation::Type_Number, "Version of the REST API")
+        .SetAnswerField(VERSION, RestApiCallDocumentation::Type_String, "Version of Orthanc")
+        .SetAnswerField(DATABASE_VERSION, RestApiCallDocumentation::Type_Number,
                         "Version of the database: https://book.orthanc-server.com/developers/db-versioning.html")
-        .SetAnswerField("IsHttpServerSecure", RestApiCallDocumentation::Type_Boolean,
+        .SetAnswerField(IS_HTTP_SERVER_SECURE, RestApiCallDocumentation::Type_Boolean,
                         "Whether the REST API is properly secured (assuming no reverse proxy is in use): https://book.orthanc-server.com/faq/security.html#securing-the-http-server")
-        .SetAnswerField("StorageAreaPlugin", RestApiCallDocumentation::Type_String,
+        .SetAnswerField(STORAGE_AREA_PLUGIN, RestApiCallDocumentation::Type_String,
                         "Information about the installed storage area plugin (`null` if no such plugin is installed)")
-        .SetAnswerField("DatabaseBackendPlugin", RestApiCallDocumentation::Type_String,
+        .SetAnswerField(DATABASE_BACKEND_PLUGIN, RestApiCallDocumentation::Type_String,
                         "Information about the installed database index plugin (`null` if no such plugin is installed)")
-        .SetAnswerField("DicomAet", RestApiCallDocumentation::Type_String, "The DICOM AET of Orthanc")
-        .SetAnswerField("DicomPort", RestApiCallDocumentation::Type_Number, "The port to the DICOM server of Orthanc")
-        .SetAnswerField("HttpPort", RestApiCallDocumentation::Type_Number, "The port to the HTTP server of Orthanc")
-        .SetAnswerField("Name", RestApiCallDocumentation::Type_String,
+        .SetAnswerField(DICOM_AET, RestApiCallDocumentation::Type_String, "The DICOM AET of Orthanc")
+        .SetAnswerField(DICOM_PORT, RestApiCallDocumentation::Type_Number, "The port to the DICOM server of Orthanc")
+        .SetAnswerField(HTTP_PORT, RestApiCallDocumentation::Type_Number, "The port to the HTTP server of Orthanc")
+        .SetAnswerField(NAME, RestApiCallDocumentation::Type_String,
                         "The name of the Orthanc server, cf. the `Name` configuration option")
-        .SetAnswerField("PluginsEnabled", RestApiCallDocumentation::Type_Boolean,
+        .SetAnswerField(PLUGINS_ENABLED, RestApiCallDocumentation::Type_Boolean,
                         "Whether Orthanc was built with support for plugins")
+        .SetAnswerField(CHECK_REVISIONS, RestApiCallDocumentation::Type_Boolean,
+                        "Whether Orthanc handle revisions of metadata and attachments to deal with multiple writers (new in Orthanc 1.9.2)")
         .SetHttpGetSample("https://demo.orthanc-server.com/system", true);
       return;
     }
@@ -92,39 +107,40 @@
 
     Json::Value result = Json::objectValue;
 
-    result["ApiVersion"] = ORTHANC_API_VERSION;
-    result["Version"] = ORTHANC_VERSION;
-    result["DatabaseVersion"] = OrthancRestApi::GetIndex(call).GetDatabaseVersion();
-    result["IsHttpServerSecure"] = context.IsHttpServerSecure();  // New in Orthanc 1.5.8
+    result[API_VERSION] = ORTHANC_API_VERSION;
+    result[VERSION] = ORTHANC_VERSION;
+    result[DATABASE_VERSION] = OrthancRestApi::GetIndex(call).GetDatabaseVersion();
+    result[IS_HTTP_SERVER_SECURE] = context.IsHttpServerSecure();  // New in Orthanc 1.5.8
 
     {
       OrthancConfiguration::ReaderLock lock;
-      result["DicomAet"] = lock.GetConfiguration().GetOrthancAET();
-      result["DicomPort"] = lock.GetConfiguration().GetUnsignedIntegerParameter("DicomPort", 4242);
-      result["HttpPort"] = lock.GetConfiguration().GetUnsignedIntegerParameter("HttpPort", 8042);
-      result["Name"] = lock.GetConfiguration().GetStringParameter("Name", "");
+      result[DICOM_AET] = lock.GetConfiguration().GetOrthancAET();
+      result[DICOM_PORT] = lock.GetConfiguration().GetUnsignedIntegerParameter(DICOM_PORT, 4242);
+      result[HTTP_PORT] = lock.GetConfiguration().GetUnsignedIntegerParameter(HTTP_PORT, 8042);
+      result[NAME] = lock.GetConfiguration().GetStringParameter(NAME, "");
+      result[CHECK_REVISIONS] = lock.GetConfiguration().GetBooleanParameter(CHECK_REVISIONS, false);  // New in Orthanc 1.9.2
     }
 
-    result["StorageAreaPlugin"] = Json::nullValue;
-    result["DatabaseBackendPlugin"] = Json::nullValue;
+    result[STORAGE_AREA_PLUGIN] = Json::nullValue;
+    result[DATABASE_BACKEND_PLUGIN] = Json::nullValue;
 
 #if ORTHANC_ENABLE_PLUGINS == 1
-    result["PluginsEnabled"] = true;
+    result[PLUGINS_ENABLED] = true;
     const OrthancPlugins& plugins = context.GetPlugins();
 
     if (plugins.HasStorageArea())
     {
       std::string p = plugins.GetStorageAreaLibrary().GetPath();
-      result["StorageAreaPlugin"] = boost::filesystem::canonical(p).string();
+      result[STORAGE_AREA_PLUGIN] = boost::filesystem::canonical(p).string();
     }
 
     if (plugins.HasDatabaseBackend())
     {
       std::string p = plugins.GetDatabaseBackendLibrary().GetPath();
-      result["DatabaseBackendPlugin"] = boost::filesystem::canonical(p).string();     
+      result[DATABASE_BACKEND_PLUGIN] = boost::filesystem::canonical(p).string();     
     }
 #else
-    result["PluginsEnabled"] = false;
+    result[PLUGINS_ENABLED] = false;
 #endif
 
     call.GetOutput().AnswerJson(result);
--- a/OrthancServer/Sources/OrthancWebDav.cpp	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancServer/Sources/OrthancWebDav.cpp	Tue Apr 20 18:11:29 2021 +0200
@@ -50,7 +50,6 @@
 static const char* const BY_DATES = "by-dates";
 static const char* const BY_UIDS = "by-uids";
 static const char* const UPLOADS = "uploads";
-static const char* const MAIN_DICOM_TAGS = "MainDicomTags";
 static const char* const STUDY_INFO = "study.json";
 static const char* const SERIES_INFO = "series.json";
 
@@ -70,7 +69,8 @@
                          MetadataType metadata)
   {
     std::string value;
-    if (context.GetIndex().LookupMetadata(value, publicId, level, metadata))
+    int64_t revision;  // Ignored
+    if (context.GetIndex().LookupMetadata(value, revision, publicId, level, metadata))
     {
       try
       {
@@ -153,7 +153,8 @@
         if (level_ == ResourceType_Instance)
         {
           FileInfo info;
-          if (context_.GetIndex().LookupAttachment(info, publicId, FileContentType_Dicom))
+          int64_t revision;  // Ignored
+          if (context_.GetIndex().LookupAttachment(info, revision, publicId, FileContentType_Dicom))
           {
             std::unique_ptr<File> f(new File(s + ".dcm"));
             f->SetMimeType(MimeType_Dicom);
@@ -268,8 +269,8 @@
                        const DicomMap& mainDicomTags,
                        const Json::Value* dicomAsJson  /* unused (*) */)  ORTHANC_OVERRIDE
     {
-      Json::Value info;
-      if (context_.GetIndex().LookupResource(info, publicId, level_))
+      Json::Value resource;
+      if (context_.GetIndex().ExpandResource(resource, publicId, level_))
       {
         if (success_)
         {
@@ -277,7 +278,7 @@
         }
         else
         {
-          target_ = info.toStyledString();
+          target_ = resource.toStyledString();
 
           // Replace UNIX newlines with DOS newlines 
           boost::replace_all(target_, "\n", "\r\n");
@@ -508,7 +509,8 @@
           LookupTime(time, context_, *it, ResourceType_Instance, MetadataType_Instance_ReceptionDate);
 
           FileInfo info;
-          if (context_.GetIndex().LookupAttachment(info, *it, FileContentType_Dicom))
+          int64_t revision;  // Ignored
+          if (context_.GetIndex().LookupAttachment(info, revision, *it, FileContentType_Dicom))
           {
             std::unique_ptr<File> resource(new File(*it + ".dcm"));
             resource->SetMimeType(MimeType_Dicom);
--- a/OrthancServer/Sources/ServerContext.cpp	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancServer/Sources/ServerContext.cpp	Tue Apr 20 18:11:29 2021 +0200
@@ -215,7 +215,7 @@
     if (loadJobsFromDatabase)
     {
       std::string serialized;
-      if (index_.LookupGlobalProperty(serialized, GlobalProperty_JobsRegistry))
+      if (index_.LookupGlobalProperty(serialized, GlobalProperty_JobsRegistry, false /* not shared */))
       {
         LOG(WARNING) << "Reloading the jobs from the last execution of Orthanc";
 
@@ -261,7 +261,7 @@
         std::string serialized;
         Toolbox::WriteFastJson(serialized, value);
 
-        index_.SetGlobalProperty(GlobalProperty_JobsRegistry, serialized);
+        index_.SetGlobalProperty(GlobalProperty_JobsRegistry, false /* not shared */, serialized);
       }
       catch (OrthancException& e)
       {
@@ -753,13 +753,16 @@
                                        FileContentType content)
   {
     FileInfo attachment;
-    if (!index_.LookupAttachment(attachment, resourceId, content))
+    int64_t revision;
+    if (!index_.LookupAttachment(attachment, revision, resourceId, content))
     {
       throw OrthancException(ErrorCode_UnknownResource);
     }
-
-    StorageAccessor accessor(area_, GetMetricsRegistry());
-    accessor.AnswerFile(output, attachment, GetFileContentMime(content));
+    else
+    {
+      StorageAccessor accessor(area_, GetMetricsRegistry());
+      accessor.AnswerFile(output, attachment, GetFileContentMime(content));
+    }
   }
 
 
@@ -773,7 +776,8 @@
               << compression; 
 
     FileInfo attachment;
-    if (!index_.LookupAttachment(attachment, resourceId, attachmentType))
+    int64_t revision;
+    if (!index_.LookupAttachment(attachment, revision, resourceId, attachmentType))
     {
       throw OrthancException(ErrorCode_UnknownResource);
     }
@@ -794,7 +798,8 @@
 
     try
     {
-      StoreStatus status = index_.AddAttachment(modified, resourceId);
+      int64_t newRevision;  // ignored
+      StoreStatus status = index_.AddAttachment(newRevision, modified, resourceId, true, revision);
       if (status != StoreStatus_Success)
       {
         accessor.Remove(modified);
@@ -833,8 +838,9 @@
      **/
     
     FileInfo attachment;
+    int64_t revision;  // Ignored
 
-    if (index_.LookupAttachment(attachment, instancePublicId, FileContentType_DicomUntilPixelData))
+    if (index_.LookupAttachment(attachment, revision, instancePublicId, FileContentType_DicomUntilPixelData))
     {
       std::string dicom;
 
@@ -860,7 +866,7 @@
 
       {
         std::string s;
-        if (index_.LookupMetadata(s, instancePublicId, ResourceType_Instance,
+        if (index_.LookupMetadata(s, revision, instancePublicId, ResourceType_Instance,
                                   MetadataType_Instance_PixelDataOffset))
         {
           hasPixelDataOffset = false;
@@ -892,7 +898,7 @@
 
       if (hasPixelDataOffset &&
           area_.HasReadRange() &&
-          index_.LookupAttachment(attachment, instancePublicId, FileContentType_Dicom) &&
+          index_.LookupAttachment(attachment, revision, instancePublicId, FileContentType_Dicom) &&
           attachment.GetCompressionType() == CompressionType_None)
       {
         /**
@@ -921,7 +927,7 @@
         }
       }
       else if (ignoreTagLength.empty() &&
-               index_.LookupAttachment(attachment, instancePublicId, FileContentType_DicomAsJson))
+               index_.LookupAttachment(attachment, revision, instancePublicId, FileContentType_DicomAsJson))
       {
         /**
          * CASE 3: This instance was created using Orthanc <=
@@ -971,14 +977,16 @@
           if (DicomStreamReader::LookupPixelDataOffset(pixelDataOffset, dicom) &&
               pixelDataOffset < dicom.size())
           {
-            index_.SetMetadata(instancePublicId, MetadataType_Instance_PixelDataOffset,
-                               boost::lexical_cast<std::string>(pixelDataOffset));
+            index_.OverwriteMetadata(instancePublicId, MetadataType_Instance_PixelDataOffset,
+                                     boost::lexical_cast<std::string>(pixelDataOffset));
 
             if (!area_.HasReadRange() ||
                 compressionEnabled_)
             {
-              AddAttachment(instancePublicId, FileContentType_DicomUntilPixelData,
-                            dicom.empty() ? NULL: dicom.c_str(), pixelDataOffset);
+              int64_t newRevision;
+              AddAttachment(newRevision, instancePublicId, FileContentType_DicomUntilPixelData,
+                            dicom.empty() ? NULL: dicom.c_str(), pixelDataOffset,
+                            false /* no old revision */, -1 /* dummy revision */);
             }
           }
         }
@@ -998,7 +1006,8 @@
   void ServerContext::ReadDicom(std::string& dicom,
                                 const std::string& instancePublicId)
   {
-    ReadAttachment(dicom, instancePublicId, FileContentType_Dicom, true /* uncompress */);
+    int64_t revision;
+    ReadAttachment(dicom, revision, instancePublicId, FileContentType_Dicom, true /* uncompress */);
   }
     
 
@@ -1011,7 +1020,8 @@
     }
     
     FileInfo attachment;
-    if (!index_.LookupAttachment(attachment, instancePublicId, FileContentType_Dicom))
+    int64_t revision;  // Ignored
+    if (!index_.LookupAttachment(attachment, revision, instancePublicId, FileContentType_Dicom))
     {
       throw OrthancException(ErrorCode_InternalError,
                              "Unable to read the DICOM file of instance " + instancePublicId);
@@ -1020,7 +1030,7 @@
     std::string s;
 
     if (attachment.GetCompressionType() == CompressionType_None &&
-        index_.LookupMetadata(s, instancePublicId, ResourceType_Instance,
+        index_.LookupMetadata(s, revision, instancePublicId, ResourceType_Instance,
                               MetadataType_Instance_PixelDataOffset) &&
         !s.empty())
     {
@@ -1044,12 +1054,13 @@
   
 
   void ServerContext::ReadAttachment(std::string& result,
+                                     int64_t& revision,
                                      const std::string& instancePublicId,
                                      FileContentType content,
                                      bool uncompressIfNeeded)
   {
     FileInfo attachment;
-    if (!index_.LookupAttachment(attachment, instancePublicId, content))
+    if (!index_.LookupAttachment(attachment, revision, instancePublicId, content))
     {
       throw OrthancException(ErrorCode_InternalError,
                              "Unable to read attachment " + EnumerationToString(content) +
@@ -1145,10 +1156,13 @@
   }
 
 
-  bool ServerContext::AddAttachment(const std::string& resourceId,
+  bool ServerContext::AddAttachment(int64_t& newRevision,
+                                    const std::string& resourceId,
                                     FileContentType attachmentType,
                                     const void* data,
-                                    size_t size)
+                                    size_t size,
+                                    bool hasOldRevision,
+                                    int64_t oldRevision)
   {
     LOG(INFO) << "Adding attachment " << EnumerationToString(attachmentType) << " to resource " << resourceId;
     
@@ -1158,7 +1172,7 @@
     StorageAccessor accessor(area_, GetMetricsRegistry());
     FileInfo attachment = accessor.Write(data, size, attachmentType, compression, storeMD5_);
 
-    StoreStatus status = index_.AddAttachment(attachment, resourceId);
+    StoreStatus status = index_.AddAttachment(newRevision, attachment, resourceId, hasOldRevision, oldRevision);
     if (status != StoreStatus_Success)
     {
       accessor.Remove(attachment);
@@ -1643,7 +1657,8 @@
     if (metadata == MetadataType_Instance_SopClassUid ||
         metadata == MetadataType_Instance_TransferSyntax)
     {
-      if (index_.LookupMetadata(target, publicId, level, metadata))
+      int64_t revision;  // Ignored
+      if (index_.LookupMetadata(target, revision, publicId, level, metadata))
       {
         return true;
       }
@@ -1685,7 +1700,7 @@
           target = value->GetContent();
 
           // Store for reuse
-          index_.SetMetadata(publicId, metadata, target);
+          index_.OverwriteMetadata(publicId, metadata, target);
           return true;
         }
         else
@@ -1698,7 +1713,8 @@
     else
     {
       // No backward
-      return index_.LookupMetadata(target, publicId, level, metadata);
+      int64_t revision;  // Ignored
+      return index_.LookupMetadata(target, revision, publicId, level, metadata);
     }
   }
 
--- a/OrthancServer/Sources/ServerContext.h	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancServer/Sources/ServerContext.h	Tue Apr 20 18:11:29 2021 +0200
@@ -295,10 +295,13 @@
       return compressionEnabled_;
     }
 
-    bool AddAttachment(const std::string& resourceId,
+    bool AddAttachment(int64_t& newRevision,
+                       const std::string& resourceId,
                        FileContentType attachmentType,
                        const void* data,
-                       size_t size);
+                       size_t size,
+                       bool hasOldRevision,
+                       int64_t oldRevision);
 
     StoreStatus Store(std::string& resultPublicId,
                       DicomInstanceToStore& dicom,
@@ -327,6 +330,7 @@
 
     // This method is for low-level operations on "/instances/.../attachments/..."
     void ReadAttachment(std::string& result,
+                        int64_t& revision,
                         const std::string& instancePublicId,
                         FileContentType content,
                         bool uncompressIfNeeded);
--- a/OrthancServer/Sources/ServerEnumerations.h	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancServer/Sources/ServerEnumerations.h	Tue Apr 20 18:11:29 2021 +0200
@@ -109,6 +109,12 @@
     TransferSyntaxGroup_H265    // New in Orthanc 1.9.0
   };
 
+  enum TransactionType
+  {
+    TransactionType_ReadOnly,
+    TransactionType_ReadWrite
+  };
+
 
   /**
    * WARNING: Do not change the explicit values in the enumerations
@@ -119,7 +125,7 @@
   enum GlobalProperty
   {
     GlobalProperty_DatabaseSchemaVersion = 1,   // Unused in the Orthanc core as of Orthanc 0.9.5
-    GlobalProperty_FlushSleep = 2,
+    GlobalProperty_FlushSleep = 2,              // Unused in the Orthanc core if Orthanc > 1.9.1
     GlobalProperty_AnonymizationSequence = 3,
     GlobalProperty_JobsRegistry = 5,
     GlobalProperty_GetTotalSizeIsFast = 6,      // New in Orthanc 1.5.2
--- a/OrthancServer/Sources/ServerIndex.cpp	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancServer/Sources/ServerIndex.cpp	Tue Apr 20 18:11:29 2021 +0200
@@ -38,45 +38,20 @@
 #define NOMINMAX
 #endif
 
-#include "../../OrthancFramework/Sources/DicomFormat/DicomArray.h"
-#include "../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
-#include "../../OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h"
 #include "../../OrthancFramework/Sources/Logging.h"
 #include "../../OrthancFramework/Sources/Toolbox.h"
 
-#include "Database/ResourcesContent.h"
 #include "OrthancConfiguration.h"
-#include "Search/DatabaseLookup.h"
-#include "Search/DicomTagConstraint.h"
 #include "ServerContext.h"
 #include "ServerIndexChange.h"
 #include "ServerToolbox.h"
 
-#include <boost/lexical_cast.hpp>
-#include <stdio.h>
-#include <stack>
 
 static const uint64_t MEGA_BYTES = 1024 * 1024;
 
 namespace Orthanc
 {
-  static void CopyListToVector(std::vector<std::string>& target,
-                               const std::list<std::string>& source)
-  {
-    target.resize(source.size());
-
-    size_t pos = 0;
-    
-    for (std::list<std::string>::const_iterator
-           it = source.begin(); it != source.end(); ++it)
-    {
-      target[pos] = *it;
-      pos ++;
-    }      
-  }
-
-  
-  class ServerIndex::Listener : public IDatabaseListener
+  class ServerIndex::TransactionContext : public StatelessDatabaseOperations::ITransactionContext
   {
   private:
     struct FileToRemove
@@ -110,41 +85,16 @@
     std::list<FileToRemove> pendingFilesToRemove_;
     std::list<ServerIndexChange> pendingChanges_;
     uint64_t sizeOfFilesToRemove_;
-    bool insideTransaction_;
+    uint64_t sizeOfAddedAttachments_;
 
     void Reset()
     {
       sizeOfFilesToRemove_ = 0;
       hasRemainingLevel_ = false;
+      remainingType_ = ResourceType_Instance;  // dummy initialization
       pendingFilesToRemove_.clear();
       pendingChanges_.clear();
-    }
-
-  public:
-    explicit Listener(ServerContext& context) :
-      context_(context),
-      insideTransaction_(false)      
-    {
-      Reset();
-      assert(ResourceType_Patient < ResourceType_Study &&
-             ResourceType_Study < ResourceType_Series &&
-             ResourceType_Series < ResourceType_Instance);
-    }
-
-    void StartTransaction()
-    {
-      Reset();
-      insideTransaction_ = true;
-    }
-
-    void EndTransaction()
-    {
-      insideTransaction_ = false;
-    }
-
-    uint64_t GetSizeOfFilesToRemove()
-    {
-      return sizeOfFilesToRemove_;
+      sizeOfAddedAttachments_ = 0;
     }
 
     void CommitFilesToRemove()
@@ -175,8 +125,18 @@
       }
     }
 
+  public:
+    explicit TransactionContext(ServerContext& context) :
+      context_(context)
+    {
+      Reset();
+      assert(ResourceType_Patient < ResourceType_Study &&
+             ResourceType_Study < ResourceType_Series &&
+             ResourceType_Series < ResourceType_Instance);
+    }
+
     virtual void SignalRemainingAncestor(ResourceType parentType,
-                                         const std::string& publicId)
+                                         const std::string& publicId) ORTHANC_OVERRIDE
     {
       LOG(TRACE) << "Remaining ancestor \"" << publicId << "\" (" << parentType << ")";
 
@@ -196,99 +156,99 @@
       }        
     }
 
-    virtual void SignalFileDeleted(const FileInfo& info)
+    virtual void SignalAttachmentDeleted(const FileInfo& info) ORTHANC_OVERRIDE
     {
       assert(Toolbox::IsUuid(info.GetUuid()));
       pendingFilesToRemove_.push_back(FileToRemove(info));
       sizeOfFilesToRemove_ += info.GetCompressedSize();
     }
 
-    virtual void SignalChange(const ServerIndexChange& change)
+    virtual void SignalResourceDeleted(ResourceType type,
+                                       const std::string& publicId) ORTHANC_OVERRIDE
+    {
+      SignalChange(ServerIndexChange(ChangeType_Deleted, type, publicId));
+    }
+
+    virtual void SignalChange(const ServerIndexChange& change) ORTHANC_OVERRIDE
     {
       LOG(TRACE) << "Change related to resource " << change.GetPublicId() << " of type " 
                  << EnumerationToString(change.GetResourceType()) << ": " 
                  << EnumerationToString(change.GetChangeType());
 
-      if (insideTransaction_)
+      pendingChanges_.push_back(change);
+    }
+
+    virtual void SignalAttachmentsAdded(uint64_t compressedSize) ORTHANC_OVERRIDE
+    {
+      sizeOfAddedAttachments_ += compressedSize;
+    }
+
+    virtual bool LookupRemainingLevel(std::string& remainingPublicId /* out */,
+                                      ResourceType& remainingLevel   /* out */) ORTHANC_OVERRIDE
+    {
+      if (hasRemainingLevel_)
       {
-        pendingChanges_.push_back(change);
+        remainingPublicId = remainingPublicId_;
+        remainingLevel = remainingType_;
+        return true;
       }
       else
       {
-        context_.SignalChange(change);
-      }
+        return false;
+      }        
+    };
+
+    virtual void MarkAsUnstable(int64_t id,
+                                Orthanc::ResourceType type,
+                                const std::string& publicId) ORTHANC_OVERRIDE
+    {
+      context_.GetIndex().MarkAsUnstable(id, type, publicId);
     }
 
-    bool HasRemainingLevel() const
+    virtual bool IsUnstableResource(int64_t id) ORTHANC_OVERRIDE
     {
-      return hasRemainingLevel_;
-    }
-
-    ResourceType GetRemainingType() const
-    {
-      assert(HasRemainingLevel());
-      return remainingType_;
+      return context_.GetIndex().IsUnstableResource(id);
     }
 
-    const std::string& GetRemainingPublicId() const
+    virtual void Commit() ORTHANC_OVERRIDE
     {
-      assert(HasRemainingLevel());
-      return remainingPublicId_;
-    }                                 
-  };
-
-
-  class ServerIndex::Transaction
-  {
-  private:
-    ServerIndex& index_;
-    std::unique_ptr<IDatabaseWrapper::ITransaction> transaction_;
-    bool isCommitted_;
-    
-  public:
-    explicit Transaction(ServerIndex& index) : 
-      index_(index),
-      isCommitted_(false)
-    {
-      transaction_.reset(index_.db_.StartTransaction());
-      transaction_->Begin();
-
-      index_.listener_->StartTransaction();
+      // We can remove the files once the SQLite transaction has
+      // been successfully committed. Some files might have to be
+      // deleted because of recycling.
+      CommitFilesToRemove();
+      
+      // Send all the pending changes to the Orthanc plugins
+      CommitChanges();
     }
 
-    ~Transaction()
-    {
-      index_.listener_->EndTransaction();
-
-      if (!isCommitted_)
-      {
-        transaction_->Rollback();
-      }
-    }
-
-    void Commit(uint64_t sizeOfAddedFiles)
+    virtual int64_t GetCompressedSizeDelta() ORTHANC_OVERRIDE
     {
-      if (!isCommitted_)
-      {
-        int64_t delta = (static_cast<int64_t>(sizeOfAddedFiles) -
-                         static_cast<int64_t>(index_.listener_->GetSizeOfFilesToRemove()));
-
-        transaction_->Commit(delta);
-
-        // We can remove the files once the SQLite transaction has
-        // been successfully committed. Some files might have to be
-        // deleted because of recycling.
-        index_.listener_->CommitFilesToRemove();
-
-        // Send all the pending changes to the Orthanc plugins
-        index_.listener_->CommitChanges();
-
-        isCommitted_ = true;
-      }
+      return (static_cast<int64_t>(sizeOfAddedAttachments_) -
+              static_cast<int64_t>(sizeOfFilesToRemove_));
     }
   };
 
 
+  class ServerIndex::TransactionContextFactory : public ITransactionContextFactory
+  {
+  private:
+    ServerContext& context_;
+      
+  public:
+    explicit TransactionContextFactory(ServerContext& context) :
+      context_(context)
+    {
+    }
+
+    virtual ITransactionContext* Create()
+    {
+      // There can be concurrent calls to this method, which is not an
+      // issue because we simply create an object
+      return new TransactionContext(context_);
+    }
+  };    
+  
+  
   class ServerIndex::UnstableResourcePayload
   {
   private:
@@ -326,379 +286,71 @@
   };
 
 
-  class ServerIndex::MainDicomTagsRegistry : public boost::noncopyable
+  void ServerIndex::FlushThread(ServerIndex* that,
+                                unsigned int threadSleepGranularityMilliseconds)
   {
-  private:
-    class TagInfo
-    {
-    private:
-      ResourceType  level_;
-      DicomTagType  type_;
-
-    public:
-      TagInfo()
-      {
-      }
-
-      TagInfo(ResourceType level,
-              DicomTagType type) :
-        level_(level),
-        type_(type)
-      {
-      }
-
-      ResourceType GetLevel() const
-      {
-        return level_;
-      }
-
-      DicomTagType GetType() const
-      {
-        return type_;
-      }
-    };
-      
-    typedef std::map<DicomTag, TagInfo>   Registry;
-
+    // By default, wait for 10 seconds before flushing
+    static const unsigned int SLEEP_SECONDS = 10;
 
-    Registry  registry_;
-      
-    void LoadTags(ResourceType level)
+    if (threadSleepGranularityMilliseconds > 1000)
     {
-      {
-        const DicomTag* tags = NULL;
-        size_t size;
-  
-        ServerToolbox::LoadIdentifiers(tags, size, level);
-  
-        for (size_t i = 0; i < size; i++)
-        {
-          if (registry_.find(tags[i]) == registry_.end())
-          {
-            registry_[tags[i]] = TagInfo(level, DicomTagType_Identifier);
-          }
-          else
-          {
-            // These patient-level tags are copied in the study level
-            assert(level == ResourceType_Study &&
-                   (tags[i] == DICOM_TAG_PATIENT_ID ||
-                    tags[i] == DICOM_TAG_PATIENT_NAME ||
-                    tags[i] == DICOM_TAG_PATIENT_BIRTH_DATE));
-          }
-        }
-      }
-
-      {
-        std::set<DicomTag> tags;
-        DicomMap::GetMainDicomTags(tags, level);
-
-        for (std::set<DicomTag>::const_iterator
-               tag = tags.begin(); tag != tags.end(); ++tag)
-        {
-          if (registry_.find(*tag) == registry_.end())
-          {
-            registry_[*tag] = TagInfo(level, DicomTagType_Main);
-          }
-        }
-      }
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
 
-  public:
-    MainDicomTagsRegistry()
-    {
-      LoadTags(ResourceType_Patient);
-      LoadTags(ResourceType_Study);
-      LoadTags(ResourceType_Series);
-      LoadTags(ResourceType_Instance); 
-    }
-
-    void LookupTag(ResourceType& level,
-                   DicomTagType& type,
-                   const DicomTag& tag) const
-    {
-      Registry::const_iterator it = registry_.find(tag);
-
-      if (it == registry_.end())
-      {
-        // Default values
-        level = ResourceType_Instance;
-        type = DicomTagType_Generic;
-      }
-      else
-      {
-        level = it->second.GetLevel();
-        type = it->second.GetType();
-      }
-    }
-  };
-
-
-  bool ServerIndex::DeleteResource(Json::Value& target,
-                                   const std::string& uuid,
-                                   ResourceType expectedType)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    Transaction t(*this);
-
-    int64_t id;
-    ResourceType type;
-    if (!db_.LookupResource(id, type, uuid) ||
-        expectedType != type)
-    {
-      return false;
-    }
-      
-    db_.DeleteResource(id);
-
-    if (listener_->HasRemainingLevel())
-    {
-      ResourceType remainingType = listener_->GetRemainingType();
-      const std::string& remainingUuid = listener_->GetRemainingPublicId();
-
-      target["RemainingAncestor"] = Json::Value(Json::objectValue);
-      target["RemainingAncestor"]["Path"] = GetBasePath(remainingType, remainingUuid);
-      target["RemainingAncestor"]["Type"] = EnumerationToString(remainingType);
-      target["RemainingAncestor"]["ID"] = remainingUuid;
-    }
-    else
-    {
-      target["RemainingAncestor"] = Json::nullValue;
-    }
-
-    t.Commit(0);
-
-    return true;
-  }
-
-
-  void ServerIndex::FlushThread(ServerIndex* that,
-                                unsigned int threadSleep)
-  {
-    // By default, wait for 10 seconds before flushing
-    unsigned int sleep = 10;
-
-    try
-    {
-      boost::mutex::scoped_lock lock(that->mutex_);
-      std::string sleepString;
-
-      if (that->db_.LookupGlobalProperty(sleepString, GlobalProperty_FlushSleep) &&
-          Toolbox::IsInteger(sleepString))
-      {
-        sleep = boost::lexical_cast<unsigned int>(sleepString);
-      }
-    }
-    catch (boost::bad_lexical_cast&)
-    {
-    }
-
-    LOG(INFO) << "Starting the database flushing thread (sleep = " << sleep << ")";
+    LOG(INFO) << "Starting the database flushing thread (sleep = " << SLEEP_SECONDS << " seconds)";
 
     unsigned int count = 0;
+    unsigned int countThreshold = (1000 * SLEEP_SECONDS) / threadSleepGranularityMilliseconds;
 
     while (!that->done_)
     {
-      boost::this_thread::sleep(boost::posix_time::milliseconds(threadSleep));
+      boost::this_thread::sleep(boost::posix_time::milliseconds(threadSleepGranularityMilliseconds));
       count++;
-      if (count < sleep)
+      
+      if (count >= countThreshold)
       {
-        continue;
+        Logging::Flush();
+        that->FlushToDisk();
+        
+        count = 0;
       }
-
-      Logging::Flush();
-
-      boost::mutex::scoped_lock lock(that->mutex_);
-
-      try
-      {
-        that->db_.FlushToDisk();
-      }
-      catch (OrthancException&)
-      {
-        LOG(ERROR) << "Cannot flush the SQLite database to the disk (is your filesystem full?)";
-      }
-          
-      count = 0;
     }
 
     LOG(INFO) << "Stopping the database flushing thread";
   }
 
 
-  static bool ComputeExpectedNumberOfInstances(int64_t& target,
-                                               const DicomMap& dicomSummary)
-  {
-    try
-    {
-      const DicomValue* value;
-      const DicomValue* value2;
-          
-      if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_IMAGES_IN_ACQUISITION)) != NULL &&
-          !value->IsNull() &&
-          !value->IsBinary() &&
-          (value2 = dicomSummary.TestAndGetValue(DICOM_TAG_NUMBER_OF_TEMPORAL_POSITIONS)) != NULL &&
-          !value2->IsNull() &&
-          !value2->IsBinary())
-      {
-        // Patch for series with temporal positions thanks to Will Ryder
-        int64_t imagesInAcquisition = boost::lexical_cast<int64_t>(value->GetContent());
-        int64_t countTemporalPositions = boost::lexical_cast<int64_t>(value2->GetContent());
-        target = imagesInAcquisition * countTemporalPositions;
-        return (target > 0);
-      }
-
-      else if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_NUMBER_OF_SLICES)) != NULL &&
-               !value->IsNull() &&
-               !value->IsBinary() &&
-               (value2 = dicomSummary.TestAndGetValue(DICOM_TAG_NUMBER_OF_TIME_SLICES)) != NULL &&
-               !value2->IsBinary() &&
-               !value2->IsNull())
-      {
-        // Support of Cardio-PET images
-        int64_t numberOfSlices = boost::lexical_cast<int64_t>(value->GetContent());
-        int64_t numberOfTimeSlices = boost::lexical_cast<int64_t>(value2->GetContent());
-        target = numberOfSlices * numberOfTimeSlices;
-        return (target > 0);
-      }
-
-      else if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_CARDIAC_NUMBER_OF_IMAGES)) != NULL &&
-               !value->IsNull() &&
-               !value->IsBinary())
-      {
-        target = boost::lexical_cast<int64_t>(value->GetContent());
-        return (target > 0);
-      }
-    }
-    catch (OrthancException&)
-    {
-    }
-    catch (boost::bad_lexical_cast&)
-    {
-    }
-
-    return false;
-  }
-
-
-
-
-  static bool LookupStringMetadata(std::string& result,
-                                   const std::map<MetadataType, std::string>& metadata,
-                                   MetadataType type)
+  bool ServerIndex::IsUnstableResource(int64_t id)
   {
-    std::map<MetadataType, std::string>::const_iterator found = metadata.find(type);
-
-    if (found == metadata.end())
-    {
-      return false;
-    }
-    else
-    {
-      result = found->second;
-      return true;
-    }
-  }
-
-
-  static bool LookupIntegerMetadata(int64_t& result,
-                                    const std::map<MetadataType, std::string>& metadata,
-                                    MetadataType type)
-  {
-    std::string s;
-    if (!LookupStringMetadata(s, metadata, type))
-    {
-      return false;
-    }
-
-    try
-    {
-      result = boost::lexical_cast<int64_t>(s);
-      return true;
-    }
-    catch (boost::bad_lexical_cast&)
-    {
-      return false;
-    }
+    boost::mutex::scoped_lock lock(monitoringMutex_);
+    return unstableResources_.Contains(id);
   }
 
 
-  void ServerIndex::LogChange(int64_t internalId,
-                              ChangeType changeType,
-                              ResourceType resourceType,
-                              const std::string& publicId)
-  {
-    ServerIndexChange change(changeType, resourceType, publicId);
-
-    if (changeType <= ChangeType_INTERNAL_LastLogged)
-    {
-      db_.LogChange(internalId, change);
-    }
-
-    assert(listener_.get() != NULL);
-    listener_->SignalChange(change);
-  }
-
-
-  uint64_t ServerIndex::IncrementGlobalSequenceInternal(GlobalProperty property)
-  {
-    std::string oldValue;
-
-    if (db_.LookupGlobalProperty(oldValue, property))
-    {
-      uint64_t oldNumber;
-      
-      try
-      {
-        oldNumber = boost::lexical_cast<uint64_t>(oldValue);
-      }
-      catch (boost::bad_lexical_cast&)
-      {
-        LOG(ERROR) << "Cannot read the global sequence "
-                   << boost::lexical_cast<std::string>(property) << ", resetting it";
-        oldNumber = 0;
-      }
-
-      db_.SetGlobalProperty(property, boost::lexical_cast<std::string>(oldNumber + 1));
-      return oldNumber + 1;
-    }
-    else
-    {
-      // Initialize the sequence at "1"
-      db_.SetGlobalProperty(property, "1");
-      return 1;
-    }
-  }
-
-
-
   ServerIndex::ServerIndex(ServerContext& context,
                            IDatabaseWrapper& db,
-                           unsigned int threadSleep) : 
+                           unsigned int threadSleepGranularityMilliseconds) :
+    StatelessDatabaseOperations(db),
     done_(false),
-    db_(db),
     maximumStorageSize_(0),
-    maximumPatients_(0),
-    mainDicomTagsRegistry_(new MainDicomTagsRegistry)
+    maximumPatients_(0)
   {
-    listener_.reset(new Listener(context));
-    db_.SetListener(*listener_);
+    SetTransactionContextFactory(new TransactionContextFactory(context));
 
     // Initial recycling if the parameters have changed since the last
     // execution of Orthanc
-    StandaloneRecycling();
+    StandaloneRecycling(maximumStorageSize_, maximumPatients_);
 
-    if (db.HasFlushToDisk())
+    if (HasFlushToDisk())
     {
-      flushThread_ = boost::thread(FlushThread, this, threadSleep);
+      flushThread_ = boost::thread(FlushThread, this, threadSleepGranularityMilliseconds);
     }
 
     unstableResourcesMonitorThread_ = boost::thread
-      (UnstableResourcesMonitorThread, this, threadSleep);
+      (UnstableResourcesMonitorThread, this, threadSleepGranularityMilliseconds);
   }
 
 
-
   ServerIndex::~ServerIndex()
   {
     if (!done_)
@@ -709,15 +361,13 @@
   }
 
 
-
   void ServerIndex::Stop()
   {
     if (!done_)
     {
       done_ = true;
 
-      if (db_.HasFlushToDisk() &&
-          flushThread_.joinable())
+      if (flushThread_.joinable())
       {
         flushThread_.join();
       }
@@ -730,1373 +380,48 @@
   }
 
 
-  static void SetInstanceMetadata(ResourcesContent& content,
-                                  std::map<MetadataType, std::string>& instanceMetadata,
-                                  int64_t instance,
-                                  MetadataType metadata,
-                                  const std::string& value)
-  {
-    content.AddMetadata(instance, metadata, value);
-    instanceMetadata[metadata] = value;
-  }
-
-
-  void ServerIndex::SignalNewResource(ChangeType changeType,
-                                      ResourceType level,
-                                      const std::string& publicId,
-                                      int64_t internalId)
-  {
-    ServerIndexChange change(changeType, level, publicId);
-    db_.LogChange(internalId, change);
-    
-    assert(listener_.get() != NULL);
-    listener_->SignalChange(change);
-  }
-
-  
-  StoreStatus ServerIndex::Store(std::map<MetadataType, std::string>& instanceMetadata,
-                                 const DicomMap& dicomSummary,
-                                 const Attachments& attachments,
-                                 const MetadataMap& metadata,
-                                 const DicomInstanceOrigin& origin,
-                                 bool overwrite,
-                                 bool hasTransferSyntax,
-                                 DicomTransferSyntax transferSyntax,
-                                 bool hasPixelDataOffset,
-                                 uint64_t pixelDataOffset)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    int64_t expectedInstances;
-    const bool hasExpectedInstances =
-      ComputeExpectedNumberOfInstances(expectedInstances, dicomSummary);
-    
-    instanceMetadata.clear();
-
-    DicomInstanceHasher hasher(dicomSummary);
-    const std::string hashPatient = hasher.HashPatient();
-    const std::string hashStudy = hasher.HashStudy();
-    const std::string hashSeries = hasher.HashSeries();
-    const std::string hashInstance = hasher.HashInstance();
-
-    try
-    {
-      Transaction t(*this);
-
-      IDatabaseWrapper::CreateInstanceResult status;
-      int64_t instanceId;
-
-      // Check whether this instance is already stored
-      if (!db_.CreateInstance(status, instanceId, hashPatient,
-                              hashStudy, hashSeries, hashInstance))
-      {
-        // The instance already exists
-        
-        if (overwrite)
-        {
-          // Overwrite the old instance
-          LOG(INFO) << "Overwriting instance: " << hashInstance;
-          db_.DeleteResource(instanceId);
-
-          // Re-create the instance, now that the old one is removed
-          if (!db_.CreateInstance(status, instanceId, hashPatient,
-                                  hashStudy, hashSeries, hashInstance))
-          {
-            throw OrthancException(ErrorCode_InternalError);
-          }
-        }
-        else
-        {
-          // Do nothing if the instance already exists and overwriting is disabled
-          db_.GetAllMetadata(instanceMetadata, instanceId);
-          return StoreStatus_AlreadyStored;
-        }
-      }
-
-
-      // Warn about the creation of new resources. The order must be
-      // from instance to patient.
-
-      // NB: In theory, could be sped up by grouping the underlying
-      // calls to "db_.LogChange()". However, this would only have an
-      // impact when new patient/study/series get created, which
-      // occurs far less often that creating new instances. The
-      // positive impact looks marginal in practice.
-      SignalNewResource(ChangeType_NewInstance, ResourceType_Instance, hashInstance, instanceId);
-
-      if (status.isNewSeries_)
-      {
-        SignalNewResource(ChangeType_NewSeries, ResourceType_Series, hashSeries, status.seriesId_);
-      }
-      
-      if (status.isNewStudy_)
-      {
-        SignalNewResource(ChangeType_NewStudy, ResourceType_Study, hashStudy, status.studyId_);
-      }
-      
-      if (status.isNewPatient_)
-      {
-        SignalNewResource(ChangeType_NewPatient, ResourceType_Patient, hashPatient, status.patientId_);
-      }
-      
-      
-      // Ensure there is enough room in the storage for the new instance
-      uint64_t instanceSize = 0;
-      for (Attachments::const_iterator it = attachments.begin();
-           it != attachments.end(); ++it)
-      {
-        instanceSize += it->GetCompressedSize();
-      }
-
-      Recycle(instanceSize, hashPatient /* don't consider the current patient for recycling */);
-      
-     
-      // Attach the files to the newly created instance
-      for (Attachments::const_iterator it = attachments.begin();
-           it != attachments.end(); ++it)
-      {
-        db_.AddAttachment(instanceId, *it);
-      }
-
-      
-      {
-        ResourcesContent content;
-      
-        // Populate the tags of the newly-created resources
-
-        content.AddResource(instanceId, ResourceType_Instance, dicomSummary);
-
-        if (status.isNewSeries_)
-        {
-          content.AddResource(status.seriesId_, ResourceType_Series, dicomSummary);
-        }
-
-        if (status.isNewStudy_)
-        {
-          content.AddResource(status.studyId_, ResourceType_Study, dicomSummary);
-        }
-
-        if (status.isNewPatient_)
-        {
-          content.AddResource(status.patientId_, ResourceType_Patient, dicomSummary);
-        }
-
-
-        // Attach the user-specified metadata
-
-        for (MetadataMap::const_iterator 
-               it = metadata.begin(); it != metadata.end(); ++it)
-        {
-          switch (it->first.first)
-          {
-            case ResourceType_Patient:
-              content.AddMetadata(status.patientId_, it->first.second, it->second);
-              break;
-
-            case ResourceType_Study:
-              content.AddMetadata(status.studyId_, it->first.second, it->second);
-              break;
-
-            case ResourceType_Series:
-              content.AddMetadata(status.seriesId_, it->first.second, it->second);
-              break;
-
-            case ResourceType_Instance:
-              SetInstanceMetadata(content, instanceMetadata, instanceId,
-                                  it->first.second, it->second);
-              break;
-
-            default:
-              throw OrthancException(ErrorCode_ParameterOutOfRange);
-          }
-        }
-
-        
-        // Attach the auto-computed metadata for the patient/study/series levels
-        std::string now = SystemToolbox::GetNowIsoString(true /* use UTC time (not local time) */);
-        content.AddMetadata(status.seriesId_, MetadataType_LastUpdate, now);
-        content.AddMetadata(status.studyId_, MetadataType_LastUpdate, now);
-        content.AddMetadata(status.patientId_, MetadataType_LastUpdate, now);
-
-        if (status.isNewSeries_)
-        {
-          if (hasExpectedInstances)
-          {
-            content.AddMetadata(status.seriesId_, MetadataType_Series_ExpectedNumberOfInstances,
-                                boost::lexical_cast<std::string>(expectedInstances));
-          }
-
-          // New in Orthanc 1.9.0
-          content.AddMetadata(status.seriesId_, MetadataType_RemoteAet,
-                              origin.GetRemoteAetC());
-        }
-
-        
-        // Attach the auto-computed metadata for the instance level,
-        // reflecting these additions into the input metadata map
-        SetInstanceMetadata(content, instanceMetadata, instanceId,
-                            MetadataType_Instance_ReceptionDate, now);
-        SetInstanceMetadata(content, instanceMetadata, instanceId, MetadataType_RemoteAet,
-                            origin.GetRemoteAetC());
-        SetInstanceMetadata(content, instanceMetadata, instanceId, MetadataType_Instance_Origin, 
-                            EnumerationToString(origin.GetRequestOrigin()));
-
-
-        if (hasTransferSyntax)
-        {
-          // New in Orthanc 1.2.0
-          SetInstanceMetadata(content, instanceMetadata, instanceId,
-                              MetadataType_Instance_TransferSyntax,
-                              GetTransferSyntaxUid(transferSyntax));
-        }
-
-        {
-          std::string s;
-
-          if (origin.LookupRemoteIp(s))
-          {
-            // New in Orthanc 1.4.0
-            SetInstanceMetadata(content, instanceMetadata, instanceId,
-                                MetadataType_Instance_RemoteIp, s);
-          }
-
-          if (origin.LookupCalledAet(s))
-          {
-            // New in Orthanc 1.4.0
-            SetInstanceMetadata(content, instanceMetadata, instanceId,
-                                MetadataType_Instance_CalledAet, s);
-          }
-
-          if (origin.LookupHttpUsername(s))
-          {
-            // New in Orthanc 1.4.0
-            SetInstanceMetadata(content, instanceMetadata, instanceId,
-                                MetadataType_Instance_HttpUsername, s);
-          }
-        }
-
-        if (hasPixelDataOffset)
-        {
-          // New in Orthanc 1.9.1
-          SetInstanceMetadata(content, instanceMetadata, instanceId,
-                              MetadataType_Instance_PixelDataOffset,
-                              boost::lexical_cast<std::string>(pixelDataOffset));
-        }
-        
-        const DicomValue* value;
-        if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_SOP_CLASS_UID)) != NULL &&
-            !value->IsNull() &&
-            !value->IsBinary())
-        {
-          SetInstanceMetadata(content, instanceMetadata, instanceId,
-                              MetadataType_Instance_SopClassUid, value->GetContent());
-        }
-
-
-        if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_INSTANCE_NUMBER)) != NULL ||
-            (value = dicomSummary.TestAndGetValue(DICOM_TAG_IMAGE_INDEX)) != NULL)
-        {
-          if (!value->IsNull() && 
-              !value->IsBinary())
-          {
-            SetInstanceMetadata(content, instanceMetadata, instanceId,
-                                MetadataType_Instance_IndexInSeries, Toolbox::StripSpaces(value->GetContent()));
-          }
-        }
-
-        
-        db_.SetResourcesContent(content);
-      }
-
-  
-      // Check whether the series of this new instance is now completed
-      int64_t expectedNumberOfInstances;
-      if (ComputeExpectedNumberOfInstances(expectedNumberOfInstances, dicomSummary))
-      {
-        SeriesStatus seriesStatus = GetSeriesStatus(status.seriesId_, expectedNumberOfInstances);
-        if (seriesStatus == SeriesStatus_Complete)
-        {
-          LogChange(status.seriesId_, ChangeType_CompletedSeries, ResourceType_Series, hashSeries);
-        }
-      }
-      
-
-      // Mark the parent resources of this instance as unstable
-      MarkAsUnstable(status.seriesId_, ResourceType_Series, hashSeries);
-      MarkAsUnstable(status.studyId_, ResourceType_Study, hashStudy);
-      MarkAsUnstable(status.patientId_, ResourceType_Patient, hashPatient);
-
-      t.Commit(instanceSize);
-
-      return StoreStatus_Success;
-    }
-    catch (OrthancException& e)
-    {
-      LOG(ERROR) << "EXCEPTION [" << e.What() << "]";
-    }
-
-    return StoreStatus_Failure;
-  }
-
-
-  void ServerIndex::GetGlobalStatistics(/* out */ uint64_t& diskSize,
-                                        /* out */ uint64_t& uncompressedSize,
-                                        /* out */ uint64_t& countPatients, 
-                                        /* out */ uint64_t& countStudies, 
-                                        /* out */ uint64_t& countSeries, 
-                                        /* out */ uint64_t& countInstances)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-    diskSize = db_.GetTotalCompressedSize();
-    uncompressedSize = db_.GetTotalUncompressedSize();
-    countPatients = db_.GetResourceCount(ResourceType_Patient);
-    countStudies = db_.GetResourceCount(ResourceType_Study);
-    countSeries = db_.GetResourceCount(ResourceType_Series);
-    countInstances = db_.GetResourceCount(ResourceType_Instance);
-  }
-
-  
-  SeriesStatus ServerIndex::GetSeriesStatus(int64_t id,
-                                            int64_t expectedNumberOfInstances)
+  void ServerIndex::SetMaximumPatientCount(unsigned int count) 
   {
-    std::list<std::string> values;
-    db_.GetChildrenMetadata(values, id, MetadataType_Instance_IndexInSeries);
-
-    std::set<int64_t> instances;
-
-    for (std::list<std::string>::const_iterator
-           it = values.begin(); it != values.end(); ++it)
     {
-      int64_t index;
-
-      try
-      {
-        index = boost::lexical_cast<int64_t>(*it);
-      }
-      catch (boost::bad_lexical_cast&)
-      {
-        return SeriesStatus_Unknown;
-      }
+      boost::mutex::scoped_lock lock(monitoringMutex_);
+      maximumPatients_ = count;
       
-      if (!(index > 0 && index <= expectedNumberOfInstances))
-      {
-        // Out-of-range instance index
-        return SeriesStatus_Inconsistent;
-      }
-
-      if (instances.find(index) != instances.end())
-      {
-        // Twice the same instance index
-        return SeriesStatus_Inconsistent;
-      }
-
-      instances.insert(index);
-    }
-
-    if (static_cast<int64_t>(instances.size()) == expectedNumberOfInstances)
-    {
-      return SeriesStatus_Complete;
-    }
-    else
-    {
-      return SeriesStatus_Missing;
-    }
-  }
-
-
-  void ServerIndex::MainDicomTagsToJson(Json::Value& target,
-                                        int64_t resourceId,
-                                        ResourceType resourceType)
-  {
-    DicomMap tags;
-    db_.GetMainDicomTags(tags, resourceId);
-
-    if (resourceType == ResourceType_Study)
-    {
-      DicomMap t1, t2;
-      tags.ExtractStudyInformation(t1);
-      tags.ExtractPatientInformation(t2);
-
-      target["MainDicomTags"] = Json::objectValue;
-      FromDcmtkBridge::ToJson(target["MainDicomTags"], t1, true);
-
-      target["PatientMainDicomTags"] = Json::objectValue;
-      FromDcmtkBridge::ToJson(target["PatientMainDicomTags"], t2, true);
-    }
-    else
-    {
-      target["MainDicomTags"] = Json::objectValue;
-      FromDcmtkBridge::ToJson(target["MainDicomTags"], tags, true);
-    }
-  }
-
-  
-  bool ServerIndex::LookupResource(Json::Value& result,
-                                   const std::string& publicId,
-                                   ResourceType expectedType)
-  {
-    result = Json::objectValue;
-
-    boost::mutex::scoped_lock lock(mutex_);
-
-    // Lookup for the requested resource
-    int64_t id;
-    ResourceType type;
-    std::string parent;
-    if (!db_.LookupResourceAndParent(id, type, parent, publicId) ||
-        type != expectedType)
-    {
-      return false;
-    }
-
-    // Set information about the parent resource (if it exists)
-    if (type == ResourceType_Patient)
-    {
-      if (!parent.empty())
-      {
-        throw OrthancException(ErrorCode_DatabasePlugin);
-      }
-    }
-    else
-    {
-      if (parent.empty())
-      {
-        throw OrthancException(ErrorCode_DatabasePlugin);
-      }
-
-      switch (type)
-      {
-        case ResourceType_Study:
-          result["ParentPatient"] = parent;
-          break;
-
-        case ResourceType_Series:
-          result["ParentStudy"] = parent;
-          break;
-
-        case ResourceType_Instance:
-          result["ParentSeries"] = parent;
-          break;
-
-        default:
-          throw OrthancException(ErrorCode_InternalError);
-      }
-    }
-
-    // List the children resources
-    std::list<std::string> children;
-    db_.GetChildrenPublicId(children, id);
-
-    if (type != ResourceType_Instance)
-    {
-      Json::Value c = Json::arrayValue;
-
-      for (std::list<std::string>::const_iterator
-             it = children.begin(); it != children.end(); ++it)
-      {
-        c.append(*it);
-      }
-
-      switch (type)
+      if (count == 0)
       {
-        case ResourceType_Patient:
-          result["Studies"] = c;
-          break;
-
-        case ResourceType_Study:
-          result["Series"] = c;
-          break;
-
-        case ResourceType_Series:
-          result["Instances"] = c;
-          break;
-
-        default:
-          throw OrthancException(ErrorCode_InternalError);
-      }
-    }
-
-    // Extract the metadata
-    std::map<MetadataType, std::string> metadata;
-    db_.GetAllMetadata(metadata, id);
-
-    // Set the resource type
-    switch (type)
-    {
-      case ResourceType_Patient:
-        result["Type"] = "Patient";
-        break;
-
-      case ResourceType_Study:
-        result["Type"] = "Study";
-        break;
-
-      case ResourceType_Series:
-      {
-        result["Type"] = "Series";
-
-        int64_t i;
-        if (LookupIntegerMetadata(i, metadata, MetadataType_Series_ExpectedNumberOfInstances))
-        {
-          result["ExpectedNumberOfInstances"] = static_cast<int>(i);
-          result["Status"] = EnumerationToString(GetSeriesStatus(id, i));
-        }
-        else
-        {
-          result["ExpectedNumberOfInstances"] = Json::nullValue;
-          result["Status"] = EnumerationToString(SeriesStatus_Unknown);
-        }
-
-        break;
-      }
-
-      case ResourceType_Instance:
-      {
-        result["Type"] = "Instance";
-
-        FileInfo attachment;
-        if (!db_.LookupAttachment(attachment, id, FileContentType_Dicom))
-        {
-          throw OrthancException(ErrorCode_InternalError);
-        }
-
-        result["FileSize"] = static_cast<unsigned int>(attachment.GetUncompressedSize());
-        result["FileUuid"] = attachment.GetUuid();
-
-        int64_t i;
-        if (LookupIntegerMetadata(i, metadata, MetadataType_Instance_IndexInSeries))
-        {
-          result["IndexInSeries"] = static_cast<int>(i);
-        }
-        else
-        {
-          result["IndexInSeries"] = Json::nullValue;
-        }
-
-        break;
-      }
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-    }
-
-    // Record the remaining information
-    result["ID"] = publicId;
-    MainDicomTagsToJson(result, id, type);
-
-    std::string tmp;
-
-    if (LookupStringMetadata(tmp, metadata, MetadataType_AnonymizedFrom))
-    {
-      result["AnonymizedFrom"] = tmp;
-    }
-
-    if (LookupStringMetadata(tmp, metadata, MetadataType_ModifiedFrom))
-    {
-      result["ModifiedFrom"] = tmp;
-    }
-
-    if (type == ResourceType_Patient ||
-        type == ResourceType_Study ||
-        type == ResourceType_Series)
-    {
-      result["IsStable"] = !unstableResources_.Contains(id);
-
-      if (LookupStringMetadata(tmp, metadata, MetadataType_LastUpdate))
-      {
-        result["LastUpdate"] = tmp;
-      }
-    }
-
-    return true;
-  }
-
-
-  bool ServerIndex::LookupAttachment(FileInfo& attachment,
-                                     const std::string& instanceUuid,
-                                     FileContentType contentType)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    int64_t id;
-    ResourceType type;
-    if (!db_.LookupResource(id, type, instanceUuid))
-    {
-      throw OrthancException(ErrorCode_UnknownResource);
-    }
-
-    if (db_.LookupAttachment(attachment, id, contentType))
-    {
-      assert(attachment.GetContentType() == contentType);
-      return true;
-    }
-    else
-    {
-      return false;
-    }
-  }
-
-
-
-  void ServerIndex::GetAllUuids(std::list<std::string>& target,
-                                ResourceType resourceType)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-    db_.GetAllPublicIds(target, resourceType);
-  }
-
-
-  void ServerIndex::GetAllUuids(std::list<std::string>& target,
-                                ResourceType resourceType,
-                                size_t since,
-                                size_t limit)
-  {
-    if (limit == 0)
-    {
-      target.clear();
-      return;
-    }
-
-    boost::mutex::scoped_lock lock(mutex_);
-    db_.GetAllPublicIds(target, resourceType, since, limit);
-  }
-
-
-  template <typename T>
-  static void FormatLog(Json::Value& target,
-                        const std::list<T>& log,
-                        const std::string& name,
-                        bool done,
-                        int64_t since,
-                        bool hasLast,
-                        int64_t last)
-  {
-    Json::Value items = Json::arrayValue;
-    for (typename std::list<T>::const_iterator
-           it = log.begin(); it != log.end(); ++it)
-    {
-      Json::Value item;
-      it->Format(item);
-      items.append(item);
-    }
-
-    target = Json::objectValue;
-    target[name] = items;
-    target["Done"] = done;
-
-    if (!hasLast)
-    {
-      // Best-effort guess of the last index in the sequence
-      if (log.empty())
-      {
-        last = since;
+        LOG(WARNING) << "No limit on the number of stored patients";
       }
       else
       {
-        last = log.back().GetSeq();
-      }
-    }
-    
-    target["Last"] = static_cast<int>(last);
-  }
-
-
-  void ServerIndex::GetChanges(Json::Value& target,
-                               int64_t since,                               
-                               unsigned int maxResults)
-  {
-    std::list<ServerIndexChange> changes;
-    bool done;
-    bool hasLast = false;
-    int64_t last = 0;
-
-    {
-      boost::mutex::scoped_lock lock(mutex_);
-
-      // Fix wrt. Orthanc <= 1.3.2: A transaction was missing, as
-      // "GetLastChange()" involves calls to "GetPublicId()"
-      Transaction transaction(*this);
-
-      db_.GetChanges(changes, done, since, maxResults);
-      if (changes.empty())
-      {
-        last = db_.GetLastChangeIndex();
-        hasLast = true;
-      }
-      
-      transaction.Commit(0);
-    }
-
-    FormatLog(target, changes, "Changes", done, since, hasLast, last);
-  }
-
-
-  void ServerIndex::GetLastChange(Json::Value& target)
-  {
-    std::list<ServerIndexChange> changes;
-    bool hasLast = false;
-    int64_t last = 0;
-
-    {
-      boost::mutex::scoped_lock lock(mutex_);
-
-      // Fix wrt. Orthanc <= 1.3.2: A transaction was missing, as
-      // "GetLastChange()" involves calls to "GetPublicId()"
-      Transaction transaction(*this);
-
-      db_.GetLastChange(changes);
-      if (changes.empty())
-      {
-        last = db_.GetLastChangeIndex();
-        hasLast = true;
-      }
-
-      transaction.Commit(0);
-    }
-
-    FormatLog(target, changes, "Changes", true, 0, hasLast, last);
-  }
-
-
-  void ServerIndex::LogExportedResource(const std::string& publicId,
-                                        const std::string& remoteModality)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-    Transaction transaction(*this);
-
-    int64_t id;
-    ResourceType type;
-    if (!db_.LookupResource(id, type, publicId))
-    {
-      throw OrthancException(ErrorCode_InexistentItem);
-    }
-
-    std::string patientId;
-    std::string studyInstanceUid;
-    std::string seriesInstanceUid;
-    std::string sopInstanceUid;
-
-    int64_t currentId = id;
-    ResourceType currentType = type;
-
-    // Iteratively go up inside the patient/study/series/instance hierarchy
-    bool done = false;
-    while (!done)
-    {
-      DicomMap map;
-      db_.GetMainDicomTags(map, currentId);
-
-      switch (currentType)
-      {
-        case ResourceType_Patient:
-          if (map.HasTag(DICOM_TAG_PATIENT_ID))
-          {
-            patientId = map.GetValue(DICOM_TAG_PATIENT_ID).GetContent();
-          }
-          done = true;
-          break;
-
-        case ResourceType_Study:
-          if (map.HasTag(DICOM_TAG_STUDY_INSTANCE_UID))
-          {
-            studyInstanceUid = map.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).GetContent();
-          }
-          currentType = ResourceType_Patient;
-          break;
-
-        case ResourceType_Series:
-          if (map.HasTag(DICOM_TAG_SERIES_INSTANCE_UID))
-          {
-            seriesInstanceUid = map.GetValue(DICOM_TAG_SERIES_INSTANCE_UID).GetContent();
-          }
-          currentType = ResourceType_Study;
-          break;
-
-        case ResourceType_Instance:
-          if (map.HasTag(DICOM_TAG_SOP_INSTANCE_UID))
-          {
-            sopInstanceUid = map.GetValue(DICOM_TAG_SOP_INSTANCE_UID).GetContent();
-          }
-          currentType = ResourceType_Series;
-          break;
-
-        default:
-          throw OrthancException(ErrorCode_InternalError);
-      }
-
-      // If we have not reached the Patient level, find the parent of
-      // the current resource
-      if (!done)
-      {
-        bool ok = db_.LookupParent(currentId, currentId);
-        (void) ok;  // Remove warning about unused variable in release builds
-        assert(ok);
+        LOG(WARNING) << "At most " << count << " patients will be stored";
       }
     }
 
-    ExportedResource resource(-1, 
-                              type,
-                              publicId,
-                              remoteModality,
-                              SystemToolbox::GetNowIsoString(true /* use UTC time (not local time) */),
-                              patientId,
-                              studyInstanceUid,
-                              seriesInstanceUid,
-                              sopInstanceUid);
-
-    db_.LogExportedResource(resource);
-    transaction.Commit(0);
-  }
-
-
-  void ServerIndex::GetExportedResources(Json::Value& target,
-                                         int64_t since,
-                                         unsigned int maxResults)
-  {
-    std::list<ExportedResource> exported;
-    bool done;
-
-    {
-      boost::mutex::scoped_lock lock(mutex_);
-      db_.GetExportedResources(exported, done, since, maxResults);
-    }
-
-    FormatLog(target, exported, "Exports", done, since, false, -1);
-  }
-
-
-  void ServerIndex::GetLastExportedResource(Json::Value& target)
-  {
-    std::list<ExportedResource> exported;
-
-    {
-      boost::mutex::scoped_lock lock(mutex_);
-      db_.GetLastExportedResource(exported);
-    }
-
-    FormatLog(target, exported, "Exports", true, 0, false, -1);
-  }
-
-
-  bool ServerIndex::IsRecyclingNeeded(uint64_t instanceSize)
-  {
-    if (maximumStorageSize_ != 0)
-    {
-      assert(maximumStorageSize_ >= instanceSize);
-      
-      if (db_.IsDiskSizeAbove(maximumStorageSize_ - instanceSize))
-      {
-        return true;
-      }
-    }
-
-    if (maximumPatients_ != 0)
-    {
-      uint64_t patientCount = db_.GetResourceCount(ResourceType_Patient);
-      if (patientCount > maximumPatients_)
-      {
-        return true;
-      }
-    }
-
-    return false;
+    StandaloneRecycling(maximumStorageSize_, maximumPatients_);
   }
 
   
-  void ServerIndex::Recycle(uint64_t instanceSize,
-                            const std::string& newPatientId)
-  {
-    if (!IsRecyclingNeeded(instanceSize))
-    {
-      return;
-    }
-
-    // Check whether other DICOM instances from this patient are
-    // already stored
-    int64_t patientToAvoid;
-    ResourceType type;
-    bool hasPatientToAvoid = db_.LookupResource(patientToAvoid, type, newPatientId);
-
-    if (hasPatientToAvoid && type != ResourceType_Patient)
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-
-    // Iteratively select patient to remove until there is enough
-    // space in the DICOM store
-    int64_t patientToRecycle;
-    while (true)
-    {
-      // If other instances of this patient are already in the store,
-      // we must avoid to recycle them
-      bool ok = hasPatientToAvoid ?
-        db_.SelectPatientToRecycle(patientToRecycle, patientToAvoid) :
-        db_.SelectPatientToRecycle(patientToRecycle);
-        
-      if (!ok)
-      {
-        throw OrthancException(ErrorCode_FullStorage);
-      }
-      
-      LOG(TRACE) << "Recycling one patient";
-      db_.DeleteResource(patientToRecycle);
-
-      if (!IsRecyclingNeeded(instanceSize))
-      {
-        // OK, we're done
-        break;
-      }
-    }
-  }  
-
-  void ServerIndex::SetMaximumPatientCount(unsigned int count) 
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-    maximumPatients_ = count;
-
-    if (count == 0)
-    {
-      LOG(WARNING) << "No limit on the number of stored patients";
-    }
-    else
-    {
-      LOG(WARNING) << "At most " << count << " patients will be stored";
-    }
-
-    StandaloneRecycling();
-  }
-
   void ServerIndex::SetMaximumStorageSize(uint64_t size) 
   {
-    boost::mutex::scoped_lock lock(mutex_);
-    maximumStorageSize_ = size;
-
-    if (size == 0)
     {
-      LOG(WARNING) << "No limit on the size of the storage area";
-    }
-    else
-    {
-      LOG(WARNING) << "At most " << (size / MEGA_BYTES) << "MB will be used for the storage area";
-    }
-
-    StandaloneRecycling();
-  }
-
-
-  void ServerIndex::StandaloneRecycling()
-  {
-    // WARNING: No mutex here, do not include this as a public method
-    Transaction t(*this);
-    Recycle(0, "");
-    t.Commit(0);
-  }
-
-
-  bool ServerIndex::IsProtectedPatient(const std::string& publicId)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    // Lookup for the requested resource
-    int64_t id;
-    ResourceType type;
-    if (!db_.LookupResource(id, type, publicId) ||
-        type != ResourceType_Patient)
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-
-    return db_.IsProtectedPatient(id);
-  }
-     
-
-  void ServerIndex::SetProtectedPatient(const std::string& publicId,
-                                        bool isProtected)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-    Transaction transaction(*this);
-
-    // Lookup for the requested resource
-    int64_t id;
-    ResourceType type;
-    if (!db_.LookupResource(id, type, publicId) ||
-        type != ResourceType_Patient)
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-
-    db_.SetProtectedPatient(id, isProtected);
-    transaction.Commit(0);
-
-    if (isProtected)
-      LOG(INFO) << "Patient " << publicId << " has been protected";
-    else
-      LOG(INFO) << "Patient " << publicId << " has been unprotected";
-  }
-
-
-  void ServerIndex::GetChildren(std::list<std::string>& result,
-                                const std::string& publicId)
-  {
-    result.clear();
-
-    boost::mutex::scoped_lock lock(mutex_);
-
-    ResourceType type;
-    int64_t resource;
-    if (!db_.LookupResource(resource, type, publicId))
-    {
-      throw OrthancException(ErrorCode_UnknownResource);
-    }
-
-    if (type == ResourceType_Instance)
-    {
-      // An instance cannot have a child
-      throw OrthancException(ErrorCode_BadParameterType);
-    }
-
-    std::list<int64_t> tmp;
-    db_.GetChildrenInternalId(tmp, resource);
-
-    for (std::list<int64_t>::const_iterator 
-           it = tmp.begin(); it != tmp.end(); ++it)
-    {
-      result.push_back(db_.GetPublicId(*it));
-    }
-  }
-
-
-  void ServerIndex::GetChildInstances(std::list<std::string>& result,
-                                      const std::string& publicId)
-  {
-    result.clear();
-
-    boost::mutex::scoped_lock lock(mutex_);
-
-    ResourceType type;
-    int64_t top;
-    if (!db_.LookupResource(top, type, publicId))
-    {
-      throw OrthancException(ErrorCode_UnknownResource);
-    }
-
-    if (type == ResourceType_Instance)
-    {
-      // The resource is already an instance: Do not go down the hierarchy
-      result.push_back(publicId);
-      return;
-    }
-
-    std::stack<int64_t> toExplore;
-    toExplore.push(top);
-
-    std::list<int64_t> tmp;
-
-    while (!toExplore.empty())
-    {
-      // Get the internal ID of the current resource
-      int64_t resource = toExplore.top();
-      toExplore.pop();
-
-      if (db_.GetResourceType(resource) == ResourceType_Instance)
+      boost::mutex::scoped_lock lock(monitoringMutex_);
+      maximumStorageSize_ = size;
+      
+      if (size == 0)
       {
-        result.push_back(db_.GetPublicId(resource));
+        LOG(WARNING) << "No limit on the size of the storage area";
       }
       else
       {
-        // Tag all the children of this resource as to be explored
-        db_.GetChildrenInternalId(tmp, resource);
-        for (std::list<int64_t>::const_iterator 
-               it = tmp.begin(); it != tmp.end(); ++it)
-        {
-          toExplore.push(*it);
-        }
-      }
-    }
-  }
-
-
-  void ServerIndex::SetMetadata(const std::string& publicId,
-                                MetadataType type,
-                                const std::string& value)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-    Transaction t(*this);
-
-    ResourceType rtype;
-    int64_t id;
-    if (!db_.LookupResource(id, rtype, publicId))
-    {
-      throw OrthancException(ErrorCode_UnknownResource);
-    }
-
-    db_.SetMetadata(id, type, value);
-
-    if (IsUserMetadata(type))
-    {
-      LogChange(id, ChangeType_UpdatedMetadata, rtype, publicId);
-    }
-
-    t.Commit(0);
-  }
-
-
-  void ServerIndex::DeleteMetadata(const std::string& publicId,
-                                   MetadataType type)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-    Transaction t(*this);
-
-    ResourceType rtype;
-    int64_t id;
-    if (!db_.LookupResource(id, rtype, publicId))
-    {
-      throw OrthancException(ErrorCode_UnknownResource);
-    }
-
-    db_.DeleteMetadata(id, type);
-
-    if (IsUserMetadata(type))
-    {
-      LogChange(id, ChangeType_UpdatedMetadata, rtype, publicId);
-    }
-
-    t.Commit(0);
-  }
-
-
-  bool ServerIndex::LookupMetadata(std::string& target,
-                                   const std::string& publicId,
-                                   ResourceType expectedType,
-                                   MetadataType type)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    ResourceType rtype;
-    int64_t id;
-    if (!db_.LookupResource(id, rtype, publicId) ||
-        rtype != expectedType)
-    {
-      throw OrthancException(ErrorCode_UnknownResource);
-    }
-
-    return db_.LookupMetadata(target, id, type);
-  }
-
-
-  void ServerIndex::GetAllMetadata(std::map<MetadataType, std::string>& target,
-                                   const std::string& publicId,
-                                   ResourceType expectedType)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    ResourceType type;
-    int64_t id;
-    if (!db_.LookupResource(id, type, publicId) ||
-        expectedType != type)
-    {
-      throw OrthancException(ErrorCode_UnknownResource);
-    }
-
-    return db_.GetAllMetadata(target, id);
-  }
-
-
-  void ServerIndex::ListAvailableAttachments(std::set<FileContentType>& target,
-                                             const std::string& publicId,
-                                             ResourceType expectedType)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    ResourceType type;
-    int64_t id;
-    if (!db_.LookupResource(id, type, publicId) ||
-        expectedType != type)
-    {
-      throw OrthancException(ErrorCode_UnknownResource);
-    }
-
-    db_.ListAvailableAttachments(target, id);
-  }
-
-
-  bool ServerIndex::LookupParent(std::string& target,
-                                 const std::string& publicId)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    ResourceType type;
-    int64_t id;
-    if (!db_.LookupResource(id, type, publicId))
-    {
-      throw OrthancException(ErrorCode_UnknownResource);
-    }
-
-    int64_t parentId;
-    if (db_.LookupParent(parentId, id))
-    {
-      target = db_.GetPublicId(parentId);
-      return true;
-    }
-    else
-    {
-      return false;
-    }
-  }
-
-
-  uint64_t ServerIndex::IncrementGlobalSequence(GlobalProperty sequence)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-    Transaction transaction(*this);
-
-    uint64_t seq = IncrementGlobalSequenceInternal(sequence);
-    transaction.Commit(0);
-
-    return seq;
-  }
-
-
-
-  void ServerIndex::LogChange(ChangeType changeType,
-                              const std::string& publicId)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-    Transaction transaction(*this);
-
-    int64_t id;
-    ResourceType type;
-    if (!db_.LookupResource(id, type, publicId))
-    {
-      throw OrthancException(ErrorCode_UnknownResource);
-    }
-
-    LogChange(id, changeType, type, publicId);
-    transaction.Commit(0);
-  }
-
-
-  void ServerIndex::DeleteChanges()
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    Transaction transaction(*this);
-    db_.ClearChanges();
-    transaction.Commit(0);
-  }
-
-  void ServerIndex::DeleteExportedResources()
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    Transaction transaction(*this);
-    db_.ClearExportedResources();
-    transaction.Commit(0);
-  }
-
-
-  void ServerIndex::GetResourceStatistics(/* out */ ResourceType& type,
-                                          /* out */ uint64_t& diskSize, 
-                                          /* out */ uint64_t& uncompressedSize, 
-                                          /* out */ unsigned int& countStudies, 
-                                          /* out */ unsigned int& countSeries, 
-                                          /* out */ unsigned int& countInstances, 
-                                          /* out */ uint64_t& dicomDiskSize, 
-                                          /* out */ uint64_t& dicomUncompressedSize, 
-                                          const std::string& publicId)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    int64_t top;
-    if (!db_.LookupResource(top, type, publicId))
-    {
-      throw OrthancException(ErrorCode_UnknownResource);
-    }
-
-    std::stack<int64_t> toExplore;
-    toExplore.push(top);
-
-    countInstances = 0;
-    countSeries = 0;
-    countStudies = 0;
-    diskSize = 0;
-    uncompressedSize = 0;
-    dicomDiskSize = 0;
-    dicomUncompressedSize = 0;
-
-    while (!toExplore.empty())
-    {
-      // Get the internal ID of the current resource
-      int64_t resource = toExplore.top();
-      toExplore.pop();
-
-      ResourceType thisType = db_.GetResourceType(resource);
-
-      std::set<FileContentType> f;
-      db_.ListAvailableAttachments(f, resource);
-
-      for (std::set<FileContentType>::const_iterator
-             it = f.begin(); it != f.end(); ++it)
-      {
-        FileInfo attachment;
-        if (db_.LookupAttachment(attachment, resource, *it))
-        {
-          if (attachment.GetContentType() == FileContentType_Dicom)
-          {
-            dicomDiskSize += attachment.GetCompressedSize();
-            dicomUncompressedSize += attachment.GetUncompressedSize();
-          }
-          
-          diskSize += attachment.GetCompressedSize();
-          uncompressedSize += attachment.GetUncompressedSize();
-        }
-      }
-
-      if (thisType == ResourceType_Instance)
-      {
-        countInstances++;
-      }
-      else
-      {
-        switch (thisType)
-        {
-          case ResourceType_Study:
-            countStudies++;
-            break;
-
-          case ResourceType_Series:
-            countSeries++;
-            break;
-
-          default:
-            break;
-        }
-
-        // Tag all the children of this resource as to be explored
-        std::list<int64_t> tmp;
-        db_.GetChildrenInternalId(tmp, resource);
-        for (std::list<int64_t>::const_iterator 
-               it = tmp.begin(); it != tmp.end(); ++it)
-        {
-          toExplore.push(*it);
-        }
+        LOG(WARNING) << "At most " << (size / MEGA_BYTES) << "MB will be used for the storage area";
       }
     }
 
-    if (countStudies == 0)
-    {
-      countStudies = 1;
-    }
-
-    if (countSeries == 0)
-    {
-      countSeries = 1;
-    }
+    StandaloneRecycling(maximumStorageSize_, maximumPatients_);
   }
 
 
   void ServerIndex::UnstableResourcesMonitorThread(ServerIndex* that,
-                                                   unsigned int threadSleep)
+                                                   unsigned int threadSleepGranularityMilliseconds)
   {
     int stableAge;
     
@@ -2115,42 +440,62 @@
     while (!that->done_)
     {
       // Check for stable resources each few seconds
-      boost::this_thread::sleep(boost::posix_time::milliseconds(threadSleep));
+      boost::this_thread::sleep(boost::posix_time::milliseconds(threadSleepGranularityMilliseconds));
 
-      boost::mutex::scoped_lock lock(that->mutex_);
-
-      while (!that->unstableResources_.IsEmpty() &&
-             that->unstableResources_.GetOldestPayload().GetAge() > static_cast<unsigned int>(stableAge))
+      for (;;)
       {
-        // This DICOM resource has not received any new instance for
-        // some time. It can be considered as stable.
-          
-        UnstableResourcePayload payload;
-        int64_t id = that->unstableResources_.RemoveOldest(payload);
+        UnstableResourcePayload stableResource;
+        int64_t stableId;
+
+        {      
+          boost::mutex::scoped_lock lock(that->monitoringMutex_);
 
-        // Ensure that the resource is still existing before logging the change
-        if (that->db_.IsExistingResource(id))
+          if (!that->unstableResources_.IsEmpty() &&
+              that->unstableResources_.GetOldestPayload().GetAge() > static_cast<unsigned int>(stableAge))
+          {
+            // This DICOM resource has not received any new instance for
+            // some time. It can be considered as stable.
+            stableId = that->unstableResources_.RemoveOldest(stableResource);
+            //LOG(TRACE) << "Stable resource: " << EnumerationToString(stableResource.GetResourceType()) << " " << stableId;
+          }
+          else
+          {
+            // No more stable DICOM resource, leave the internal loop
+            break;
+          }
+        }
+
+        try
         {
-          switch (payload.GetResourceType())
+          /**
+           * WARNING: Don't protect the calls to "LogChange()" using
+           * "monitoringMutex_", as this could lead to deadlocks in
+           * other threads (typically, if "Store()" is being running in
+           * another thread, which leads to calls to "MarkAsUnstable()",
+           * which leads to two lockings of "monitoringMutex_").
+           **/
+          switch (stableResource.GetResourceType())
           {
             case ResourceType_Patient:
-              that->LogChange(id, ChangeType_StablePatient, ResourceType_Patient, payload.GetPublicId());
+              that->LogChange(stableId, ChangeType_StablePatient, stableResource.GetPublicId(), ResourceType_Patient);
               break;
-
+            
             case ResourceType_Study:
-              that->LogChange(id, ChangeType_StableStudy, ResourceType_Study, payload.GetPublicId());
+              that->LogChange(stableId, ChangeType_StableStudy, stableResource.GetPublicId(), ResourceType_Study);
               break;
-
+            
             case ResourceType_Series:
-              that->LogChange(id, ChangeType_StableSeries, ResourceType_Series, payload.GetPublicId());
+              that->LogChange(stableId, ChangeType_StableSeries, stableResource.GetPublicId(), ResourceType_Series);
               break;
-
+            
             default:
               throw OrthancException(ErrorCode_InternalError);
           }
-
-          //LOG(INFO) << "Stable resource: " << EnumerationToString(payload.type_) << " " << id;
         }
+        catch (OrthancException& e)
+        {
+          LOG(ERROR) << "Cannot log a change about a stable resource into the database";
+        }          
       }
     }
 
@@ -2162,464 +507,61 @@
                                    Orthanc::ResourceType type,
                                    const std::string& publicId)
   {
-    // WARNING: Before calling this method, "mutex_" must be locked.
-
     assert(type == Orthanc::ResourceType_Patient ||
            type == Orthanc::ResourceType_Study ||
            type == Orthanc::ResourceType_Series);
 
-    UnstableResourcePayload payload(type, publicId);
-    unstableResources_.AddOrMakeMostRecent(id, payload);
-    //LOG(INFO) << "Unstable resource: " << EnumerationToString(type) << " " << id;
-
-    LogChange(id, ChangeType_NewChildInstance, type, publicId);
-  }
-
-
-
-  void ServerIndex::LookupIdentifierExact(std::vector<std::string>& result,
-                                          ResourceType level,
-                                          const DicomTag& tag,
-                                          const std::string& value)
-  {
-    assert((level == ResourceType_Patient && tag == DICOM_TAG_PATIENT_ID) ||
-           (level == ResourceType_Study && tag == DICOM_TAG_STUDY_INSTANCE_UID) ||
-           (level == ResourceType_Study && tag == DICOM_TAG_ACCESSION_NUMBER) ||
-           (level == ResourceType_Series && tag == DICOM_TAG_SERIES_INSTANCE_UID) ||
-           (level == ResourceType_Instance && tag == DICOM_TAG_SOP_INSTANCE_UID));
-    
-    result.clear();
-
-    DicomTagConstraint c(tag, ConstraintType_Equal, value, true, true);
-
-    std::vector<DatabaseConstraint> query;
-    query.push_back(c.ConvertToDatabaseConstraint(level, DicomTagType_Identifier));
-
-    std::list<std::string> tmp;
-    
     {
-      boost::mutex::scoped_lock lock(mutex_);
-      db_.ApplyLookupResources(tmp, NULL, query, level, 0);
-    }
-
-    CopyListToVector(result, tmp);
-  }
-
-
-  StoreStatus ServerIndex::AddAttachment(const FileInfo& attachment,
-                                         const std::string& publicId)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    Transaction t(*this);
-
-    ResourceType resourceType;
-    int64_t resourceId;
-    if (!db_.LookupResource(resourceId, resourceType, publicId))
-    {
-      return StoreStatus_Failure;  // Inexistent resource
-    }
-
-    // Remove possible previous attachment
-    db_.DeleteAttachment(resourceId, attachment.GetContentType());
-
-    // Locate the patient of the target resource
-    int64_t patientId = resourceId;
-    for (;;)
-    {
-      int64_t parent;
-      if (db_.LookupParent(parent, patientId))
-      {
-        // We have not reached the patient level yet
-        patientId = parent;
-      }
-      else
-      {
-        // We have reached the patient level
-        break;
-      }
-    }
-
-    // Possibly apply the recycling mechanism while preserving this patient
-    assert(db_.GetResourceType(patientId) == ResourceType_Patient);
-    Recycle(attachment.GetCompressedSize(), db_.GetPublicId(patientId));
-
-    db_.AddAttachment(resourceId, attachment);
-
-    if (IsUserContentType(attachment.GetContentType()))
-    {
-      LogChange(resourceId, ChangeType_UpdatedAttachment, resourceType, publicId);
-    }
-
-    t.Commit(attachment.GetCompressedSize());
-
-    return StoreStatus_Success;
-  }
-
-
-  void ServerIndex::DeleteAttachment(const std::string& publicId,
-                                     FileContentType type)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-    Transaction t(*this);
-
-    ResourceType rtype;
-    int64_t id;
-    if (!db_.LookupResource(id, rtype, publicId))
-    {
-      throw OrthancException(ErrorCode_UnknownResource);
-    }
-
-    db_.DeleteAttachment(id, type);
-
-    if (IsUserContentType(type))
-    {
-      LogChange(id, ChangeType_UpdatedAttachment, rtype, publicId);
-    }
-
-    t.Commit(0);
-  }
-
-
-  void ServerIndex::SetGlobalProperty(GlobalProperty property,
-                                      const std::string& value)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    Transaction transaction(*this);
-    db_.SetGlobalProperty(property, value);
-    transaction.Commit(0);
-  }
-
-
-  bool ServerIndex::LookupGlobalProperty(std::string& value,
-                                         GlobalProperty property)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-    return db_.LookupGlobalProperty(value, property);
-  }
-  
-
-  std::string ServerIndex::GetGlobalProperty(GlobalProperty property,
-                                             const std::string& defaultValue)
-  {
-    std::string value;
-
-    if (LookupGlobalProperty(value, property))
-    {
-      return value;
-    }
-    else
-    {
-      return defaultValue;
+      boost::mutex::scoped_lock lock(monitoringMutex_);
+      UnstableResourcePayload payload(type, publicId);
+      unstableResources_.AddOrMakeMostRecent(id, payload);
+      //LOG(INFO) << "Unstable resource: " << EnumerationToString(type) << " " << id;
     }
   }
 
 
-  bool ServerIndex::GetMainDicomTags(DicomMap& result,
-                                     const std::string& publicId,
-                                     ResourceType expectedType,
-                                     ResourceType levelOfInterest)
+  StoreStatus ServerIndex::Store(std::map<MetadataType, std::string>& instanceMetadata,
+                                 const DicomMap& dicomSummary,
+                                 const ServerIndex::Attachments& attachments,
+                                 const ServerIndex::MetadataMap& metadata,
+                                 const DicomInstanceOrigin& origin,
+                                 bool overwrite,
+                                 bool hasTransferSyntax,
+                                 DicomTransferSyntax transferSyntax,
+                                 bool hasPixelDataOffset,
+                                 uint64_t pixelDataOffset)
   {
-    // Yes, the following test could be shortened, but we wish to make it as clear as possible
-    if (!(expectedType == ResourceType_Patient  && levelOfInterest == ResourceType_Patient) &&
-        !(expectedType == ResourceType_Study    && levelOfInterest == ResourceType_Patient) &&
-        !(expectedType == ResourceType_Study    && levelOfInterest == ResourceType_Study)   &&
-        !(expectedType == ResourceType_Series   && levelOfInterest == ResourceType_Series)  &&
-        !(expectedType == ResourceType_Instance && levelOfInterest == ResourceType_Instance))
+    uint64_t maximumStorageSize;
+    unsigned int maximumPatients;
+    
     {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-
-    result.Clear();
-
-    boost::mutex::scoped_lock lock(mutex_);
-
-    // Lookup for the requested resource
-    int64_t id;
-    ResourceType type;
-    if (!db_.LookupResource(id, type, publicId) ||
-        type != expectedType)
-    {
-      return false;
+      boost::mutex::scoped_lock lock(monitoringMutex_);
+      maximumStorageSize = maximumStorageSize_;
+      maximumPatients = maximumPatients_;
     }
 
-    if (type == ResourceType_Study)
-    {
-      DicomMap tmp;
-      db_.GetMainDicomTags(tmp, id);
-
-      switch (levelOfInterest)
-      {
-        case ResourceType_Patient:
-          tmp.ExtractPatientInformation(result);
-          return true;
-
-        case ResourceType_Study:
-          tmp.ExtractStudyInformation(result);
-          return true;
-
-        default:
-          throw OrthancException(ErrorCode_InternalError);
-      }
-    }
-    else
-    {
-      db_.GetMainDicomTags(result, id);
-      return true;
-    }    
-  }
-
-
-  bool ServerIndex::GetAllMainDicomTags(DicomMap& result,
-                                        const std::string& instancePublicId)
-  {
-    result.Clear();
-    
-    boost::mutex::scoped_lock lock(mutex_);
-
-    // Lookup for the requested resource
-    int64_t instance;
-    ResourceType type;
-    if (!db_.LookupResource(instance, type, instancePublicId) ||
-        type != ResourceType_Instance)
-    {
-      return false;
-    }
-    else
-    {
-      DicomMap tmp;
-
-      db_.GetMainDicomTags(tmp, instance);
-      result.Merge(tmp);
-
-      int64_t series;
-      if (!db_.LookupParent(series, instance))
-      {
-        throw OrthancException(ErrorCode_InternalError);
-      }
-
-      tmp.Clear();
-      db_.GetMainDicomTags(tmp, series);
-      result.Merge(tmp);
-
-      int64_t study;
-      if (!db_.LookupParent(study, series))
-      {
-        throw OrthancException(ErrorCode_InternalError);
-      }
-
-      tmp.Clear();
-      db_.GetMainDicomTags(tmp, study);
-      result.Merge(tmp);
-
-#ifndef NDEBUG
-      {
-        // Sanity test to check that all the main DICOM tags from the
-        // patient level are copied at the study level
-        
-        int64_t patient;
-        if (!db_.LookupParent(patient, study))
-        {
-          throw OrthancException(ErrorCode_InternalError);
-        }
-
-        tmp.Clear();
-        db_.GetMainDicomTags(tmp, study);
-
-        std::set<DicomTag> patientTags;
-        tmp.GetTags(patientTags);
-
-        for (std::set<DicomTag>::const_iterator
-               it = patientTags.begin(); it != patientTags.end(); ++it)
-        {
-          assert(result.HasTag(*it));
-        }
-      }
-#endif
-      
-      return true;
-    }
-  }
-
-
-  bool ServerIndex::LookupResourceType(ResourceType& type,
-                                       const std::string& publicId)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    int64_t id;
-    return db_.LookupResource(id, type, publicId);
-  }
-
-
-  unsigned int ServerIndex::GetDatabaseVersion()
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-    return db_.GetDatabaseVersion();
+    return StatelessDatabaseOperations::Store(
+      instanceMetadata, dicomSummary, attachments, metadata, origin, overwrite, hasTransferSyntax,
+      transferSyntax, hasPixelDataOffset, pixelDataOffset, maximumStorageSize, maximumPatients);
   }
 
-
-  bool ServerIndex::LookupParent(std::string& target,
-                                 const std::string& publicId,
-                                 ResourceType parentType)
+  
+  StoreStatus ServerIndex::AddAttachment(int64_t& newRevision,
+                                         const FileInfo& attachment,
+                                         const std::string& publicId,
+                                         bool hasOldRevision,
+                                         int64_t oldRevision)
   {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    ResourceType type;
-    int64_t id;
-    if (!db_.LookupResource(id, type, publicId))
+    uint64_t maximumStorageSize;
+    unsigned int maximumPatients;
+    
     {
-      throw OrthancException(ErrorCode_UnknownResource);
-    }
-
-    while (type != parentType)
-    {
-      int64_t parentId;
-
-      if (type == ResourceType_Patient ||    // Cannot further go up in hierarchy
-          !db_.LookupParent(parentId, id))
-      {
-        return false;
-      }
-
-      id = parentId;
-      type = GetParentResourceType(type);
+      boost::mutex::scoped_lock lock(monitoringMutex_);
+      maximumStorageSize = maximumStorageSize_;
+      maximumPatients = maximumPatients_;
     }
 
-    target = db_.GetPublicId(id);
-    return true;
-  }
-
-
-  void ServerIndex::ReconstructInstance(const ParsedDicomFile& dicom)
-  {
-    DicomMap summary;
-    OrthancConfiguration::DefaultExtractDicomSummary(summary, dicom);
-
-    DicomInstanceHasher hasher(summary);
-
-    boost::mutex::scoped_lock lock(mutex_);
-
-    try
-    {
-      Transaction t(*this);
-
-      int64_t patient = -1, study = -1, series = -1, instance = -1;
-
-      ResourceType dummy;      
-      if (!db_.LookupResource(patient, dummy, hasher.HashPatient()) ||
-          !db_.LookupResource(study, dummy, hasher.HashStudy()) ||
-          !db_.LookupResource(series, dummy, hasher.HashSeries()) ||
-          !db_.LookupResource(instance, dummy, hasher.HashInstance()) ||
-          patient == -1 ||
-          study == -1 ||
-          series == -1 ||
-          instance == -1)
-      {
-        throw OrthancException(ErrorCode_InternalError);
-      }
-
-      db_.ClearMainDicomTags(patient);
-      db_.ClearMainDicomTags(study);
-      db_.ClearMainDicomTags(series);
-      db_.ClearMainDicomTags(instance);
-
-      {
-        ResourcesContent content;
-        content.AddResource(patient, ResourceType_Patient, summary);
-        content.AddResource(study, ResourceType_Study, summary);
-        content.AddResource(series, ResourceType_Series, summary);
-        content.AddResource(instance, ResourceType_Instance, summary);
-        db_.SetResourcesContent(content);
-      }
-
-      {
-        DicomTransferSyntax s;
-        if (dicom.LookupTransferSyntax(s))
-        {
-          db_.SetMetadata(instance, MetadataType_Instance_TransferSyntax, GetTransferSyntaxUid(s));
-        }
-      }
-
-      const DicomValue* value;
-      if ((value = summary.TestAndGetValue(DICOM_TAG_SOP_CLASS_UID)) != NULL &&
-          !value->IsNull() &&
-          !value->IsBinary())
-      {
-        db_.SetMetadata(instance, MetadataType_Instance_SopClassUid, value->GetContent());
-      }
-
-      t.Commit(0);  // No change in the DB size
-    }
-    catch (OrthancException& e)
-    {
-      LOG(ERROR) << "EXCEPTION [" << e.What() << "]";
-    }
-  }
-
-
-  void ServerIndex::NormalizeLookup(std::vector<DatabaseConstraint>& target,
-                                    const DatabaseLookup& source,
-                                    ResourceType queryLevel) const
-  {
-    assert(mainDicomTagsRegistry_.get() != NULL);
-
-    target.clear();
-    target.reserve(source.GetConstraintsCount());
-
-    for (size_t i = 0; i < source.GetConstraintsCount(); i++)
-    {
-      ResourceType level;
-      DicomTagType type;
-      
-      mainDicomTagsRegistry_->LookupTag(level, type, source.GetConstraint(i).GetTag());
-
-      if (type == DicomTagType_Identifier ||
-          type == DicomTagType_Main)
-      {
-        // Use the fact that patient-level tags are copied at the study level
-        if (level == ResourceType_Patient &&
-            queryLevel != ResourceType_Patient)
-        {
-          level = ResourceType_Study;
-        }
-        
-        target.push_back(source.GetConstraint(i).ConvertToDatabaseConstraint(level, type));
-      }
-    }
-  }
-
-
-  void ServerIndex::ApplyLookupResources(std::vector<std::string>& resourcesId,
-                                         std::vector<std::string>* instancesId,
-                                         const DatabaseLookup& lookup,
-                                         ResourceType queryLevel,
-                                         size_t limit)
-  {
-    std::vector<DatabaseConstraint> normalized;
-    NormalizeLookup(normalized, lookup, queryLevel);
-
-    std::list<std::string> resourcesList, instancesList;
-    
-    {
-      boost::mutex::scoped_lock lock(mutex_);
-
-      if (instancesId == NULL)
-      {
-        db_.ApplyLookupResources(resourcesList, NULL, normalized, queryLevel, limit);
-      }
-      else
-      {
-        db_.ApplyLookupResources(resourcesList, &instancesList, normalized, queryLevel, limit);
-      }
-    }
-
-    CopyListToVector(resourcesId, resourcesList);
-
-    if (instancesId != NULL)
-    { 
-      CopyListToVector(*instancesId, instancesList);
-    }
+    return StatelessDatabaseOperations::AddAttachment(
+      newRevision, attachment, publicId, maximumStorageSize, maximumPatients, hasOldRevision, oldRevision);
   }
 }
--- a/OrthancServer/Sources/ServerIndex.h	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancServer/Sources/ServerIndex.h	Tue Apr 20 18:11:29 2021 +0200
@@ -33,45 +33,31 @@
 
 #pragma once
 
+#include "Database/StatelessDatabaseOperations.h"
 #include "../../OrthancFramework/Sources/Cache/LeastRecentlyUsedIndex.h"
-#include "../../OrthancFramework/Sources/DicomFormat/DicomMap.h"
-
-#include "Database/IDatabaseWrapper.h"
-#include "DicomInstanceOrigin.h"
 
 #include <boost/thread.hpp>
-#include <boost/noncopyable.hpp>
 
 namespace Orthanc
 {
-  class DatabaseLookup;
-  class ParsedDicomFile;
   class ServerContext;
-
-  class ServerIndex : public boost::noncopyable
+  
+  class ServerIndex : public StatelessDatabaseOperations
   {
-  public:
-    typedef std::list<FileInfo> Attachments;
-    typedef std::map<std::pair<ResourceType, MetadataType>, std::string>  MetadataMap;
-
   private:
-    class Listener;
-    class Transaction;
+    class TransactionContext;
+    class TransactionContextFactory;
     class UnstableResourcePayload;
-    class MainDicomTagsRegistry;
 
     bool done_;
-    boost::mutex mutex_;
+    boost::mutex monitoringMutex_;
     boost::thread flushThread_;
     boost::thread unstableResourcesMonitorThread_;
 
-    std::unique_ptr<Listener> listener_;
-    IDatabaseWrapper& db_;
     LeastRecentlyUsedIndex<int64_t, UnstableResourcePayload>  unstableResources_;
 
     uint64_t     maximumStorageSize_;
     unsigned int maximumPatients_;
-    std::unique_ptr<MainDicomTagsRegistry>  mainDicomTagsRegistry_;
 
     static void FlushThread(ServerIndex* that,
                             unsigned int threadSleep);
@@ -79,59 +65,21 @@
     static void UnstableResourcesMonitorThread(ServerIndex* that,
                                                unsigned int threadSleep);
 
-    void MainDicomTagsToJson(Json::Value& result,
-                             int64_t resourceId,
-                             ResourceType resourceType);
-
-    bool IsRecyclingNeeded(uint64_t instanceSize);
-
-    void Recycle(uint64_t instanceSize,
-                 const std::string& newPatientId);
-
-    void StandaloneRecycling();
-
     void MarkAsUnstable(int64_t id,
                         Orthanc::ResourceType type,
                         const std::string& publicId);
 
-    void LogChange(int64_t internalId,
-                   ChangeType changeType,
-                   ResourceType resourceType,
-                   const std::string& publicId);
-
-    void SignalNewResource(ChangeType changeType,
-                           ResourceType level,
-                           const std::string& publicId,
-                           int64_t internalId);
-
-    uint64_t IncrementGlobalSequenceInternal(GlobalProperty property);
-
-    void NormalizeLookup(std::vector<DatabaseConstraint>& target,
-                         const DatabaseLookup& source,
-                         ResourceType level) const;
-
-    SeriesStatus GetSeriesStatus(int64_t id,
-                                 int64_t expectedNumberOfInstances);
+    bool IsUnstableResource(int64_t id);
 
   public:
     ServerIndex(ServerContext& context,
                 IDatabaseWrapper& database,
-                unsigned int threadSleep);
+                unsigned int threadSleepGranularityMilliseconds);
 
     ~ServerIndex();
 
     void Stop();
 
-    uint64_t GetMaximumStorageSize() const
-    {
-      return maximumStorageSize_;
-    }
-
-    uint64_t GetMaximumPatientCount() const
-    {
-      return maximumPatients_;
-    }
-
     // "size == 0" means no limit on the storage size
     void SetMaximumStorageSize(uint64_t size);
 
@@ -149,145 +97,10 @@
                       bool hasPixelDataOffset,
                       uint64_t pixelDataOffset);
 
-    void GetGlobalStatistics(/* out */ uint64_t& diskSize,
-                             /* out */ uint64_t& uncompressedSize,
-                             /* out */ uint64_t& countPatients, 
-                             /* out */ uint64_t& countStudies, 
-                             /* out */ uint64_t& countSeries, 
-                             /* out */ uint64_t& countInstances);
-
-    bool LookupResource(Json::Value& result,
-                        const std::string& publicId,
-                        ResourceType expectedType);
-
-    bool LookupAttachment(FileInfo& attachment,
-                          const std::string& instanceUuid,
-                          FileContentType contentType);
-
-    void GetAllUuids(std::list<std::string>& target,
-                     ResourceType resourceType);
-
-    void GetAllUuids(std::list<std::string>& target,
-                     ResourceType resourceType,
-                     size_t since,
-                     size_t limit);
-
-    bool DeleteResource(Json::Value& target /* out */,
-                        const std::string& uuid,
-                        ResourceType expectedType);
-
-    void GetChanges(Json::Value& target,
-                    int64_t since,
-                    unsigned int maxResults);
-
-    void GetLastChange(Json::Value& target);
-
-    void LogExportedResource(const std::string& publicId,
-                             const std::string& remoteModality);
-
-    void GetExportedResources(Json::Value& target,
-                              int64_t since,
-                              unsigned int maxResults);
-
-    void GetLastExportedResource(Json::Value& target);
-
-    bool IsProtectedPatient(const std::string& publicId);
-
-    void SetProtectedPatient(const std::string& publicId,
-                             bool isProtected);
-
-    void GetChildren(std::list<std::string>& result,
-                     const std::string& publicId);
-
-    void GetChildInstances(std::list<std::string>& result,
-                           const std::string& publicId);
-
-    void SetMetadata(const std::string& publicId,
-                     MetadataType type,
-                     const std::string& value);
-
-    void DeleteMetadata(const std::string& publicId,
-                        MetadataType type);
-
-    void GetAllMetadata(std::map<MetadataType, std::string>& target,
-                        const std::string& publicId,
-                        ResourceType expectedType);
-
-    bool LookupMetadata(std::string& target,
-                        const std::string& publicId,
-                        ResourceType expectedType,
-                        MetadataType type);
-
-    void ListAvailableAttachments(std::set<FileContentType>& target,
-                                  const std::string& publicId,
-                                  ResourceType expectedType);
-
-    bool LookupParent(std::string& target,
-                      const std::string& publicId);
-
-    uint64_t IncrementGlobalSequence(GlobalProperty sequence);
-
-    void LogChange(ChangeType changeType,
-                   const std::string& publicId);
-
-    void DeleteChanges();
-
-    void DeleteExportedResources();
-
-    void GetResourceStatistics(/* out */ ResourceType& type,
-                               /* out */ uint64_t& diskSize, 
-                               /* out */ uint64_t& uncompressedSize, 
-                               /* out */ unsigned int& countStudies, 
-                               /* out */ unsigned int& countSeries, 
-                               /* out */ unsigned int& countInstances, 
-                               /* out */ uint64_t& dicomDiskSize, 
-                               /* out */ uint64_t& dicomUncompressedSize, 
-                               const std::string& publicId);
-
-    void LookupIdentifierExact(std::vector<std::string>& result,
-                               ResourceType level,
-                               const DicomTag& tag,
-                               const std::string& value);
-
-    StoreStatus AddAttachment(const FileInfo& attachment,
-                              const std::string& publicId);
-
-    void DeleteAttachment(const std::string& publicId,
-                          FileContentType type);
-
-    void SetGlobalProperty(GlobalProperty property,
-                           const std::string& value);
-
-    bool LookupGlobalProperty(std::string& value,
-                              GlobalProperty property);
-
-    std::string GetGlobalProperty(GlobalProperty property,
-                                  const std::string& defaultValue);
-
-    bool GetMainDicomTags(DicomMap& result,
-                          const std::string& publicId,
-                          ResourceType expectedType,
-                          ResourceType levelOfInterest);
-
-    // Only applicable at the instance level
-    bool GetAllMainDicomTags(DicomMap& result,
-                             const std::string& instancePublicId);
-
-    bool LookupResourceType(ResourceType& type,
-                            const std::string& publicId);
-
-    unsigned int GetDatabaseVersion();
-
-    bool LookupParent(std::string& target,
-                      const std::string& publicId,
-                      ResourceType parentType);
-
-    void ReconstructInstance(const ParsedDicomFile& dicom);
-
-    void ApplyLookupResources(std::vector<std::string>& resourcesId,
-                              std::vector<std::string>* instancesId,  // Can be NULL if not needed
-                              const DatabaseLookup& lookup,
-                              ResourceType queryLevel,
-                              size_t limit);
+    StoreStatus AddAttachment(int64_t& newRevision /*out*/,
+                              const FileInfo& attachment,
+                              const std::string& publicId,
+                              bool hasOldRevision,
+                              int64_t oldRevision);
   };
 }
--- a/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp	Tue Apr 20 18:11:29 2021 +0200
@@ -229,7 +229,8 @@
       if (level_ == ResourceType_Instance)
       {
         FileInfo tmp;
-        if (index.LookupAttachment(tmp, id, FileContentType_Dicom))
+        int64_t revision;  // ignored
+        if (index.LookupAttachment(tmp, revision, id, FileContentType_Dicom))
         {
           instances_.push_back(Instance(id, tmp.GetUncompressedSize()));
         }
@@ -708,14 +709,11 @@
   {
   private:
     ZipCommands&    commands_;
-    ServerContext&  context_;
     unsigned int    counter_;
 
   public:
-    MediaIndexVisitor(ZipCommands& commands,
-                      ServerContext& context) :
+    explicit MediaIndexVisitor(ZipCommands& commands) :
       commands_(commands),
-      context_(context),
       counter_(0)
     {
     }
@@ -746,7 +744,6 @@
   class ArchiveJob::ZipWriterIterator : public boost::noncopyable
   {
   private:
-    TemporaryFile&                          target_;
     ServerContext&                          context_;
     ZipCommands                             commands_;
     std::unique_ptr<HierarchicalZipWriter>  zip_;
@@ -754,18 +751,17 @@
     bool                                    isMedia_;
 
   public:
-    ZipWriterIterator(TemporaryFile& target,
+    ZipWriterIterator(const TemporaryFile& target,
                       ServerContext& context,
                       ArchiveIndex& archive,
                       bool isMedia,
                       bool enableExtendedSopClass) :
-      target_(target),
       context_(context),
       isMedia_(isMedia)
     {
       if (isMedia)
       {
-        MediaIndexVisitor visitor(commands_, context);
+        MediaIndexVisitor visitor(commands_);
         archive.Expand(context.GetIndex());
 
         commands_.AddOpenDirectory(MEDIA_IMAGES_FOLDER);        
--- a/OrthancServer/Sources/ServerToolbox.cpp	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancServer/Sources/ServerToolbox.cpp	Tue Apr 20 18:11:29 2021 +0200
@@ -34,7 +34,6 @@
 #include "PrecompiledHeadersServer.h"
 #include "ServerToolbox.h"
 
-#include "../../OrthancFramework/Sources/DicomFormat/DicomArray.h"
 #include "../../OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h"
 #include "../../OrthancFramework/Sources/FileStorage/StorageAccessor.h"
 #include "../../OrthancFramework/Sources/Logging.h"
@@ -77,95 +76,10 @@
   };
 
 
-  static void StoreMainDicomTagsInternal(ResourcesContent& target,
-                                         int64_t resource,
-                                         const DicomMap& tags)
-  {
-    DicomArray flattened(tags);
-
-    for (size_t i = 0; i < flattened.GetSize(); i++)
-    {
-      const DicomElement& element = flattened.GetElement(i);
-      const DicomTag& tag = element.GetTag();
-      const DicomValue& value = element.GetValue();
-      if (!value.IsNull() && 
-          !value.IsBinary())
-      {
-        target.AddMainDicomTag(resource, tag, element.GetValue().GetContent());
-      }
-    }
-  }
-
-
-  static void StoreIdentifiers(ResourcesContent& target,
-                               int64_t resource,
-                               ResourceType level,
-                               const DicomMap& map)
-  {
-    const DicomTag* tags;
-    size_t size;
-
-    ServerToolbox::LoadIdentifiers(tags, size, level);
-
-    for (size_t i = 0; i < size; i++)
-    {
-      // The identifiers tags are a subset of the main DICOM tags
-      assert(DicomMap::IsMainDicomTag(tags[i]));
-        
-      const DicomValue* value = map.TestAndGetValue(tags[i]);
-      if (value != NULL &&
-          !value->IsNull() &&
-          !value->IsBinary())
-      {
-        std::string s = ServerToolbox::NormalizeIdentifier(value->GetContent());
-        target.AddIdentifierTag(resource, tags[i], s);
-      }
-    }
-  }
-
-
-  void ResourcesContent::AddResource(int64_t resource,
-                                     ResourceType level,
-                                     const DicomMap& dicomSummary)
-  {
-    StoreIdentifiers(*this, resource, level, dicomSummary);
-
-    DicomMap tags;
-
-    switch (level)
-    {
-      case ResourceType_Patient:
-        dicomSummary.ExtractPatientInformation(tags);
-        break;
-
-      case ResourceType_Study:
-        // Duplicate the patient tags at the study level (new in Orthanc 0.9.5 - db v6)
-        dicomSummary.ExtractPatientInformation(tags);
-        StoreMainDicomTagsInternal(*this, resource, tags);
-
-        dicomSummary.ExtractStudyInformation(tags);
-        break;
-
-      case ResourceType_Series:
-        dicomSummary.ExtractSeriesInformation(tags);
-        break;
-
-      case ResourceType_Instance:
-        dicomSummary.ExtractInstanceInformation(tags);
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-    }
-
-    StoreMainDicomTagsInternal(*this, resource, tags);
-  }
-
-
   namespace ServerToolbox
   {
     bool FindOneChildInstance(int64_t& result,
-                              IDatabaseWrapper& database,
+                              IDatabaseWrapper::ITransaction& transaction,
                               int64_t resource,
                               ResourceType type)
     {
@@ -178,7 +92,7 @@
         }
 
         std::list<int64_t> children;
-        database.GetChildrenInternalId(children, resource);
+        transaction.GetChildrenInternalId(children, resource);
         if (children.empty())
         {
           return false;
@@ -190,7 +104,7 @@
     }
 
 
-    void ReconstructMainDicomTags(IDatabaseWrapper& database,
+    void ReconstructMainDicomTags(IDatabaseWrapper::ITransaction& transaction,
                                   IStorageArea& storageArea,
                                   ResourceType level)
     {
@@ -230,7 +144,7 @@
       LOG(WARNING) << "Upgrade: Reconstructing the main DICOM tags of all the " << plural << "...";
 
       std::list<std::string> resources;
-      database.GetAllPublicIds(resources, level);
+      transaction.GetAllPublicIds(resources, level);
 
       for (std::list<std::string>::const_iterator
              it = resources.begin(); it != resources.end(); ++it)
@@ -239,9 +153,9 @@
         int64_t resource, instance;
         ResourceType tmp;
 
-        if (!database.LookupResource(resource, tmp, *it) ||
+        if (!transaction.LookupResource(resource, tmp, *it) ||
             tmp != level ||
-            !FindOneChildInstance(instance, database, resource, level))
+            !FindOneChildInstance(instance, transaction, resource, level))
         {
           throw OrthancException(ErrorCode_InternalError,
                                  "Cannot find an instance for " +
@@ -251,11 +165,12 @@
 
         // Get the DICOM file attached to some instances in the resource
         FileInfo attachment;
-        if (!database.LookupAttachment(attachment, instance, FileContentType_Dicom))
+        int64_t revision;
+        if (!transaction.LookupAttachment(attachment, revision, instance, FileContentType_Dicom))
         {
           throw OrthancException(ErrorCode_InternalError,
                                  "Cannot retrieve the DICOM file associated with instance " +
-                                 database.GetPublicId(instance));
+                                 transaction.GetPublicId(instance));
         }
 
         try
@@ -272,16 +187,16 @@
           DicomMap dicomSummary;
           OrthancConfiguration::DefaultExtractDicomSummary(dicomSummary, dicom);
 
-          database.ClearMainDicomTags(resource);
+          transaction.ClearMainDicomTags(resource);
 
-          ResourcesContent tags;
+          ResourcesContent tags(false /* prevent the setting of metadata */);
           tags.AddResource(resource, level, dicomSummary);
-          database.SetResourcesContent(tags);
+          transaction.SetResourcesContent(tags);
         }
         catch (OrthancException&)
         {
           LOG(ERROR) << "Cannot decode the DICOM file with UUID " << attachment.GetUuid()
-                     << " associated with instance " << database.GetPublicId(instance);
+                     << " associated with instance " << transaction.GetPublicId(instance);
           throw;
         }
       }
@@ -380,7 +295,8 @@
         ServerContext::DicomCacheLocker locker(context, *it);
 
         // Delay the reconstruction of DICOM-as-JSON to its next access through "ServerContext"
-        context.GetIndex().DeleteAttachment(*it, FileContentType_DicomAsJson);
+        context.GetIndex().DeleteAttachment(
+          *it, FileContentType_DicomAsJson, false /* no revision */, -1 /* dummy revision */);
         
         context.GetIndex().ReconstructInstance(locker.GetDicom());
       }
--- a/OrthancServer/Sources/ServerToolbox.h	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancServer/Sources/ServerToolbox.h	Tue Apr 20 18:11:29 2021 +0200
@@ -33,6 +33,7 @@
 
 #pragma once
 
+#include "Database/IDatabaseWrapper.h"
 #include "ServerEnumerations.h"
 
 #include <boost/noncopyable.hpp>
@@ -41,17 +42,16 @@
 namespace Orthanc
 {
   class ServerContext;
-  class IDatabaseWrapper;
   class IStorageArea;
 
   namespace ServerToolbox
   {
     bool FindOneChildInstance(int64_t& result,
-                              IDatabaseWrapper& database,
+                              IDatabaseWrapper::ITransaction& transaction,
                               int64_t resource,
                               ResourceType type);
 
-    void ReconstructMainDicomTags(IDatabaseWrapper& database,
+    void ReconstructMainDicomTags(IDatabaseWrapper::ITransaction& transaction,
                                   IStorageArea& storageArea,
                                   ResourceType level);
 
--- a/OrthancServer/Sources/SliceOrdering.cpp	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancServer/Sources/SliceOrdering.cpp	Tue Apr 20 18:11:29 2021 +0200
@@ -208,7 +208,8 @@
 
       try
       {
-        if (index.LookupMetadata(s, instanceId, ResourceType_Instance, MetadataType_Instance_IndexInSeries))
+        int64_t revision;  // Ignored
+        if (index.LookupMetadata(s, revision, instanceId, ResourceType_Instance, MetadataType_Instance_IndexInSeries))
         {
           indexInSeries_ = boost::lexical_cast<size_t>(Toolbox::StripSpaces(s));
           hasIndexInSeries_ = true;
--- a/OrthancServer/Sources/main.cpp	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancServer/Sources/main.cpp	Tue Apr 20 18:11:29 2021 +0200
@@ -723,6 +723,8 @@
     PrintErrorCode(ErrorCode_SslInitialization, "Cannot initialize SSL encryption, check out your certificates");
     PrintErrorCode(ErrorCode_DiscontinuedAbi, "Calling a function that has been removed from the Orthanc Framework");
     PrintErrorCode(ErrorCode_BadRange, "Incorrect range request");
+    PrintErrorCode(ErrorCode_DatabaseCannotSerialize, "Database could not serialize access due to concurrent update, the transaction should be retried");
+    PrintErrorCode(ErrorCode_Revision, "A bad revision number was provided, which might indicate conflict between multiple writers");
     PrintErrorCode(ErrorCode_SQLiteNotOpened, "SQLite: The database is not opened");
     PrintErrorCode(ErrorCode_SQLiteAlreadyOpened, "SQLite: Connection is already open");
     PrintErrorCode(ErrorCode_SQLiteCannotOpen, "SQLite: Unable to open the database");
@@ -1352,6 +1354,7 @@
       {
         plugins_->SetServerContext(context_);
         context_.SetPlugins(*plugins_);
+        context_.GetIndex().SetMaxDatabaseRetries(plugins_->GetMaxDatabaseRetries());
       }
 #endif
     }
@@ -1486,6 +1489,25 @@
                            ": Please run Orthanc with the \"--upgrade\" argument");
   }
 
+  {
+    static const char* const CHECK_REVISIONS = "CheckRevisions";
+    
+    OrthancConfiguration::ReaderLock lock;
+    if (lock.GetConfiguration().GetBooleanParameter(CHECK_REVISIONS, false))
+    {
+      if (database.HasRevisionsSupport())
+      {
+        LOG(INFO) << "Handling of revisions is enabled, and the custom database back-end *has* "
+                  << "support for revisions of metadata and attachments";
+      }
+      else
+      {
+        LOG(WARNING) << "The custom database back-end has *no* support for revisions of metadata and attachments, "
+                     << "but configuration option \"" << CHECK_REVISIONS << "\" is set to \"true\"";
+      }
+    }
+  }
+
   bool success = ConfigureServerContext
     (database, storageArea, plugins, loadJobsFromDatabase);
 
@@ -1504,7 +1526,13 @@
   std::unique_ptr<IStorageArea>  storage;
 
 #if ORTHANC_ENABLE_PLUGINS == 1
-  OrthancPlugins plugins;
+  std::string databaseServerIdentifier;
+  {
+    OrthancConfiguration::ReaderLock lock;
+    databaseServerIdentifier = lock.GetConfiguration().GetDatabaseServerIdentifier();
+  }
+  
+  OrthancPlugins plugins(databaseServerIdentifier);
   plugins.SetCommandLineArguments(argc, argv);
   LoadPlugins(plugins);
 
--- a/OrthancServer/UnitTestsSources/ServerIndexTests.cpp	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancServer/UnitTestsSources/ServerIndexTests.cpp	Tue Apr 20 18:11:29 2021 +0200
@@ -75,23 +75,18 @@
       ancestorType_ = type;
     }
 
-    virtual void SignalFileDeleted(const FileInfo& info) ORTHANC_OVERRIDE
+    virtual void SignalAttachmentDeleted(const FileInfo& info) ORTHANC_OVERRIDE
     {
       const std::string fileUuid = info.GetUuid();
       deletedFiles_.push_back(fileUuid);
       LOG(INFO) << "A file must be removed: " << fileUuid;
     }       
 
-    virtual void SignalChange(const ServerIndexChange& change) ORTHANC_OVERRIDE
+    virtual void SignalResourceDeleted(ResourceType type,
+                                       const std::string& publicId) ORTHANC_OVERRIDE
     {
-      if (change.GetChangeType() == ChangeType_Deleted)
-      {
-        deletedResources_.push_back(change.GetPublicId());        
-      }
-
-      LOG(INFO) << "Change related to resource " << change.GetPublicId() << " of type " 
-                << EnumerationToString(change.GetResourceType()) << ": " 
-                << EnumerationToString(change.GetChangeType());
+      LOG(INFO) << "Deleted resource " << publicId << " of type " << EnumerationToString(type);
+      deletedResources_.push_back(publicId);
     }
   };
 
@@ -101,6 +96,7 @@
   protected:
     std::unique_ptr<TestDatabaseListener>  listener_;
     std::unique_ptr<SQLiteDatabaseWrapper> index_;
+    std::unique_ptr<SQLiteDatabaseWrapper::UnitTestsTransaction>  transaction_;
 
   public:
     DatabaseWrapperTest()
@@ -111,12 +107,16 @@
     {
       listener_.reset(new TestDatabaseListener);
       index_.reset(new SQLiteDatabaseWrapper);
-      index_->SetListener(*listener_);
       index_->Open();
+      transaction_.reset(dynamic_cast<SQLiteDatabaseWrapper::UnitTestsTransaction*>(
+                           index_->StartTransaction(TransactionType_ReadWrite, *listener_)));
     }
 
     virtual void TearDown() ORTHANC_OVERRIDE
     {
+      transaction_->Commit(0);
+      transaction_.reset();
+      
       index_->Close();
       index_.reset(NULL);
       listener_.reset(NULL);
@@ -124,33 +124,33 @@
 
     void CheckTableRecordCount(uint32_t expected, const char* table)
     {
-      ASSERT_EQ(expected, index_->GetTableRecordCount(table));
+      ASSERT_EQ(expected, transaction_->GetTableRecordCount(table));
     }
 
     void CheckNoParent(int64_t id)
     {
       std::string s;
-      ASSERT_FALSE(index_->GetParentPublicId(s, id));
+      ASSERT_FALSE(transaction_->GetParentPublicId(s, id));
     }
 
     void CheckParentPublicId(const char* expected, int64_t id)
     {
       std::string s;
-      ASSERT_TRUE(index_->GetParentPublicId(s, id));
+      ASSERT_TRUE(transaction_->GetParentPublicId(s, id));
       ASSERT_EQ(expected, s);
     }
 
     void CheckNoChild(int64_t id)
     {
       std::list<std::string> j;
-      index_->GetChildren(j, id);
+      transaction_->GetChildren(j, id);
       ASSERT_EQ(0u, j.size());
     }
 
     void CheckOneChild(const char* expected, int64_t id)
     {
       std::list<std::string> j;
-      index_->GetChildren(j, id);
+      transaction_->GetChildren(j, id);
       ASSERT_EQ(1u, j.size());
       ASSERT_EQ(expected, j.front());
     }
@@ -160,7 +160,7 @@
                           int64_t id)
     {
       std::list<std::string> j;
-      index_->GetChildren(j, id);
+      transaction_->GetChildren(j, id);
       ASSERT_EQ(2u, j.size());
       ASSERT_TRUE((expected1 == j.front() && expected2 == j.back()) ||
                   (expected1 == j.back() && expected2 == j.front()));                    
@@ -179,7 +179,7 @@
       std::vector<DatabaseConstraint> lookup;
       lookup.push_back(c.ConvertToDatabaseConstraint(level, DicomTagType_Identifier));
       
-      index_->ApplyLookupResources(result, NULL, lookup, level, 0 /* no limit */);
+      transaction_->ApplyLookupResources(result, NULL, lookup, level, 0 /* no limit */);
     }    
 
     void DoLookupIdentifier2(std::list<std::string>& result,
@@ -199,7 +199,7 @@
       lookup.push_back(c1.ConvertToDatabaseConstraint(level, DicomTagType_Identifier));
       lookup.push_back(c2.ConvertToDatabaseConstraint(level, DicomTagType_Identifier));
       
-      index_->ApplyLookupResources(result, NULL, lookup, level, 0 /* no limit */);
+      transaction_->ApplyLookupResources(result, NULL, lookup, level, 0 /* no limit */);
     }
   };
 }
@@ -208,65 +208,65 @@
 TEST_F(DatabaseWrapperTest, Simple)
 {
   int64_t a[] = {
-    index_->CreateResource("a", ResourceType_Patient),   // 0
-    index_->CreateResource("b", ResourceType_Study),     // 1
-    index_->CreateResource("c", ResourceType_Series),    // 2
-    index_->CreateResource("d", ResourceType_Instance),  // 3
-    index_->CreateResource("e", ResourceType_Instance),  // 4
-    index_->CreateResource("f", ResourceType_Instance),  // 5
-    index_->CreateResource("g", ResourceType_Study)      // 6
+    transaction_->CreateResource("a", ResourceType_Patient),   // 0
+    transaction_->CreateResource("b", ResourceType_Study),     // 1
+    transaction_->CreateResource("c", ResourceType_Series),    // 2
+    transaction_->CreateResource("d", ResourceType_Instance),  // 3
+    transaction_->CreateResource("e", ResourceType_Instance),  // 4
+    transaction_->CreateResource("f", ResourceType_Instance),  // 5
+    transaction_->CreateResource("g", ResourceType_Study)      // 6
   };
 
-  ASSERT_EQ("a", index_->GetPublicId(a[0]));
-  ASSERT_EQ("b", index_->GetPublicId(a[1]));
-  ASSERT_EQ("c", index_->GetPublicId(a[2]));
-  ASSERT_EQ("d", index_->GetPublicId(a[3]));
-  ASSERT_EQ("e", index_->GetPublicId(a[4]));
-  ASSERT_EQ("f", index_->GetPublicId(a[5]));
-  ASSERT_EQ("g", index_->GetPublicId(a[6]));
+  ASSERT_EQ("a", transaction_->GetPublicId(a[0]));
+  ASSERT_EQ("b", transaction_->GetPublicId(a[1]));
+  ASSERT_EQ("c", transaction_->GetPublicId(a[2]));
+  ASSERT_EQ("d", transaction_->GetPublicId(a[3]));
+  ASSERT_EQ("e", transaction_->GetPublicId(a[4]));
+  ASSERT_EQ("f", transaction_->GetPublicId(a[5]));
+  ASSERT_EQ("g", transaction_->GetPublicId(a[6]));
 
-  ASSERT_EQ(ResourceType_Patient, index_->GetResourceType(a[0]));
-  ASSERT_EQ(ResourceType_Study, index_->GetResourceType(a[1]));
-  ASSERT_EQ(ResourceType_Series, index_->GetResourceType(a[2]));
-  ASSERT_EQ(ResourceType_Instance, index_->GetResourceType(a[3]));
-  ASSERT_EQ(ResourceType_Instance, index_->GetResourceType(a[4]));
-  ASSERT_EQ(ResourceType_Instance, index_->GetResourceType(a[5]));
-  ASSERT_EQ(ResourceType_Study, index_->GetResourceType(a[6]));
+  ASSERT_EQ(ResourceType_Patient, transaction_->GetResourceType(a[0]));
+  ASSERT_EQ(ResourceType_Study, transaction_->GetResourceType(a[1]));
+  ASSERT_EQ(ResourceType_Series, transaction_->GetResourceType(a[2]));
+  ASSERT_EQ(ResourceType_Instance, transaction_->GetResourceType(a[3]));
+  ASSERT_EQ(ResourceType_Instance, transaction_->GetResourceType(a[4]));
+  ASSERT_EQ(ResourceType_Instance, transaction_->GetResourceType(a[5]));
+  ASSERT_EQ(ResourceType_Study, transaction_->GetResourceType(a[6]));
 
   {
     std::list<std::string> t;
-    index_->GetAllPublicIds(t, ResourceType_Patient);
+    transaction_->GetAllPublicIds(t, ResourceType_Patient);
 
     ASSERT_EQ(1u, t.size());
     ASSERT_EQ("a", t.front());
 
-    index_->GetAllPublicIds(t, ResourceType_Series);
+    transaction_->GetAllPublicIds(t, ResourceType_Series);
     ASSERT_EQ(1u, t.size());
     ASSERT_EQ("c", t.front());
 
-    index_->GetAllPublicIds(t, ResourceType_Study);
+    transaction_->GetAllPublicIds(t, ResourceType_Study);
     ASSERT_EQ(2u, t.size());
 
-    index_->GetAllPublicIds(t, ResourceType_Instance);
+    transaction_->GetAllPublicIds(t, ResourceType_Instance);
     ASSERT_EQ(3u, t.size());
   }
 
-  index_->SetGlobalProperty(GlobalProperty_FlushSleep, "World");
+  transaction_->SetGlobalProperty(GlobalProperty_FlushSleep, true, "World");
 
-  index_->AttachChild(a[0], a[1]);
-  index_->AttachChild(a[1], a[2]);
-  index_->AttachChild(a[2], a[3]);
-  index_->AttachChild(a[2], a[4]);
-  index_->AttachChild(a[6], a[5]);
+  transaction_->AttachChild(a[0], a[1]);
+  transaction_->AttachChild(a[1], a[2]);
+  transaction_->AttachChild(a[2], a[3]);
+  transaction_->AttachChild(a[2], a[4]);
+  transaction_->AttachChild(a[6], a[5]);
 
   int64_t parent;
-  ASSERT_FALSE(index_->LookupParent(parent, a[0]));
-  ASSERT_TRUE(index_->LookupParent(parent, a[1])); ASSERT_EQ(a[0], parent);
-  ASSERT_TRUE(index_->LookupParent(parent, a[2])); ASSERT_EQ(a[1], parent);
-  ASSERT_TRUE(index_->LookupParent(parent, a[3])); ASSERT_EQ(a[2], parent);
-  ASSERT_TRUE(index_->LookupParent(parent, a[4])); ASSERT_EQ(a[2], parent);
-  ASSERT_TRUE(index_->LookupParent(parent, a[5])); ASSERT_EQ(a[6], parent);
-  ASSERT_FALSE(index_->LookupParent(parent, a[6]));
+  ASSERT_FALSE(transaction_->LookupParent(parent, a[0]));
+  ASSERT_TRUE(transaction_->LookupParent(parent, a[1])); ASSERT_EQ(a[0], parent);
+  ASSERT_TRUE(transaction_->LookupParent(parent, a[2])); ASSERT_EQ(a[1], parent);
+  ASSERT_TRUE(transaction_->LookupParent(parent, a[3])); ASSERT_EQ(a[2], parent);
+  ASSERT_TRUE(transaction_->LookupParent(parent, a[4])); ASSERT_EQ(a[2], parent);
+  ASSERT_TRUE(transaction_->LookupParent(parent, a[5])); ASSERT_EQ(a[6], parent);
+  ASSERT_FALSE(transaction_->LookupParent(parent, a[6]));
 
   std::string s;
 
@@ -279,14 +279,14 @@
   CheckParentPublicId("g", a[5]);
 
   std::list<std::string> l;
-  index_->GetChildrenPublicId(l, a[0]); ASSERT_EQ(1u, l.size()); ASSERT_EQ("b", l.front());
-  index_->GetChildrenPublicId(l, a[1]); ASSERT_EQ(1u, l.size()); ASSERT_EQ("c", l.front());
-  index_->GetChildrenPublicId(l, a[3]); ASSERT_EQ(0u, l.size()); 
-  index_->GetChildrenPublicId(l, a[4]); ASSERT_EQ(0u, l.size()); 
-  index_->GetChildrenPublicId(l, a[5]); ASSERT_EQ(0u, l.size()); 
-  index_->GetChildrenPublicId(l, a[6]); ASSERT_EQ(1u, l.size()); ASSERT_EQ("f", l.front());
+  transaction_->GetChildrenPublicId(l, a[0]); ASSERT_EQ(1u, l.size()); ASSERT_EQ("b", l.front());
+  transaction_->GetChildrenPublicId(l, a[1]); ASSERT_EQ(1u, l.size()); ASSERT_EQ("c", l.front());
+  transaction_->GetChildrenPublicId(l, a[3]); ASSERT_EQ(0u, l.size()); 
+  transaction_->GetChildrenPublicId(l, a[4]); ASSERT_EQ(0u, l.size()); 
+  transaction_->GetChildrenPublicId(l, a[5]); ASSERT_EQ(0u, l.size()); 
+  transaction_->GetChildrenPublicId(l, a[6]); ASSERT_EQ(1u, l.size()); ASSERT_EQ("f", l.front());
 
-  index_->GetChildrenPublicId(l, a[2]); ASSERT_EQ(2u, l.size()); 
+  transaction_->GetChildrenPublicId(l, a[2]); ASSERT_EQ(2u, l.size()); 
   if (l.front() == "d")
   {
     ASSERT_EQ("e", l.back());
@@ -298,64 +298,72 @@
   }
 
   std::map<MetadataType, std::string> md;
-  index_->GetAllMetadata(md, a[4]);
+  transaction_->GetAllMetadata(md, a[4]);
   ASSERT_EQ(0u, md.size());
 
-  index_->AddAttachment(a[4], FileInfo("my json file", FileContentType_DicomAsJson, 42, "md5", 
-                                       CompressionType_ZlibWithSize, 21, "compressedMD5"));
-  index_->AddAttachment(a[4], FileInfo("my dicom file", FileContentType_Dicom, 42, "md5"));
-  index_->AddAttachment(a[6], FileInfo("world", FileContentType_Dicom, 44, "md5"));
-  index_->SetMetadata(a[4], MetadataType_RemoteAet, "PINNACLE");
+  transaction_->AddAttachment(a[4], FileInfo("my json file", FileContentType_DicomAsJson, 42, "md5", 
+                                             CompressionType_ZlibWithSize, 21, "compressedMD5"), 42);
+  transaction_->AddAttachment(a[4], FileInfo("my dicom file", FileContentType_Dicom, 42, "md5"), 43);
+  transaction_->AddAttachment(a[6], FileInfo("world", FileContentType_Dicom, 44, "md5"), 44);
   
-  index_->GetAllMetadata(md, a[4]);
+  // TODO - REVISIONS - "42" is revision number, that is not currently stored (*)
+  transaction_->SetMetadata(a[4], MetadataType_RemoteAet, "PINNACLE", 42);
+  
+  transaction_->GetAllMetadata(md, a[4]);
   ASSERT_EQ(1u, md.size());
   ASSERT_EQ("PINNACLE", md[MetadataType_RemoteAet]);
-  index_->SetMetadata(a[4], MetadataType_ModifiedFrom, "TUTU");
-  index_->GetAllMetadata(md, a[4]);
+  transaction_->SetMetadata(a[4], MetadataType_ModifiedFrom, "TUTU", 10);
+  transaction_->GetAllMetadata(md, a[4]);
   ASSERT_EQ(2u, md.size());
 
   std::map<MetadataType, std::string> md2;
-  index_->GetAllMetadata(md2, a[4]);
+  transaction_->GetAllMetadata(md2, a[4]);
   ASSERT_EQ(2u, md2.size());
   ASSERT_EQ("TUTU", md2[MetadataType_ModifiedFrom]);
   ASSERT_EQ("PINNACLE", md2[MetadataType_RemoteAet]);
 
-  index_->DeleteMetadata(a[4], MetadataType_ModifiedFrom);
-  index_->GetAllMetadata(md, a[4]);
+  transaction_->DeleteMetadata(a[4], MetadataType_ModifiedFrom);
+  transaction_->GetAllMetadata(md, a[4]);
   ASSERT_EQ(1u, md.size());
   ASSERT_EQ("PINNACLE", md[MetadataType_RemoteAet]);
 
-  index_->GetAllMetadata(md2, a[4]);
+  transaction_->GetAllMetadata(md2, a[4]);
   ASSERT_EQ(1u, md2.size());
   ASSERT_EQ("PINNACLE", md2[MetadataType_RemoteAet]);
 
 
-  ASSERT_EQ(21u + 42u + 44u, index_->GetTotalCompressedSize());
-  ASSERT_EQ(42u + 42u + 44u, index_->GetTotalUncompressedSize());
+  ASSERT_EQ(21u + 42u + 44u, transaction_->GetTotalCompressedSize());
+  ASSERT_EQ(42u + 42u + 44u, transaction_->GetTotalUncompressedSize());
 
-  index_->SetMainDicomTag(a[3], DicomTag(0x0010, 0x0010), "PatientName");
+  transaction_->SetMainDicomTag(a[3], DicomTag(0x0010, 0x0010), "PatientName");
 
   int64_t b;
   ResourceType t;
-  ASSERT_TRUE(index_->LookupResource(b, t, "g"));
+  ASSERT_TRUE(transaction_->LookupResource(b, t, "g"));
   ASSERT_EQ(7, b);
   ASSERT_EQ(ResourceType_Study, t);
 
-  ASSERT_TRUE(index_->LookupMetadata(s, a[4], MetadataType_RemoteAet));
-  ASSERT_FALSE(index_->LookupMetadata(s, a[4], MetadataType_Instance_IndexInSeries));
+  int64_t revision;
+  ASSERT_TRUE(transaction_->LookupMetadata(s, revision, a[4], MetadataType_RemoteAet));
+  ASSERT_EQ(0, revision);   // "0" instead of "42" because of (*)
+  ASSERT_FALSE(transaction_->LookupMetadata(s, revision, a[4], MetadataType_Instance_IndexInSeries));
+  ASSERT_EQ(0, revision);
   ASSERT_EQ("PINNACLE", s);
 
   std::string u;
-  ASSERT_TRUE(index_->LookupMetadata(u, a[4], MetadataType_RemoteAet));
+  ASSERT_TRUE(transaction_->LookupMetadata(u, revision, a[4], MetadataType_RemoteAet));
+  ASSERT_EQ(0, revision);
   ASSERT_EQ("PINNACLE", u);
-  ASSERT_FALSE(index_->LookupMetadata(u, a[4], MetadataType_Instance_IndexInSeries));
+  ASSERT_FALSE(transaction_->LookupMetadata(u, revision, a[4], MetadataType_Instance_IndexInSeries));
+  ASSERT_EQ(0, revision);
 
-  ASSERT_TRUE(index_->LookupGlobalProperty(s, GlobalProperty_FlushSleep));
-  ASSERT_FALSE(index_->LookupGlobalProperty(s, static_cast<GlobalProperty>(42)));
+  ASSERT_TRUE(transaction_->LookupGlobalProperty(s, GlobalProperty_FlushSleep, true));
+  ASSERT_FALSE(transaction_->LookupGlobalProperty(s, static_cast<GlobalProperty>(42), true));
   ASSERT_EQ("World", s);
 
   FileInfo att;
-  ASSERT_TRUE(index_->LookupAttachment(att, a[4], FileContentType_DicomAsJson));
+  ASSERT_TRUE(transaction_->LookupAttachment(att, revision, a[4], FileContentType_DicomAsJson));
+  ASSERT_EQ(0, revision);  // "0" instead of "42" because of (*)
   ASSERT_EQ("my json file", att.GetUuid());
   ASSERT_EQ(21u, att.GetCompressedSize());
   ASSERT_EQ("md5", att.GetUncompressedMD5());
@@ -363,7 +371,8 @@
   ASSERT_EQ(42u, att.GetUncompressedSize());
   ASSERT_EQ(CompressionType_ZlibWithSize, att.GetCompressionType());
 
-  ASSERT_TRUE(index_->LookupAttachment(att, a[6], FileContentType_Dicom));
+  ASSERT_TRUE(transaction_->LookupAttachment(att, revision, a[6], FileContentType_Dicom));
+  ASSERT_EQ(0, revision);  // "0" instead of "42" because of (*)
   ASSERT_EQ("world", att.GetUuid());
   ASSERT_EQ(44u, att.GetCompressedSize());
   ASSERT_EQ("md5", att.GetUncompressedMD5());
@@ -379,7 +388,7 @@
   CheckTableRecordCount(1, "Metadata");
   CheckTableRecordCount(1, "MainDicomTags");
 
-  index_->DeleteResource(a[0]);
+  transaction_->DeleteResource(a[0]);
   ASSERT_EQ(5u, listener_->deletedResources_.size());
   ASSERT_EQ(2u, listener_->deletedFiles_.size());
   ASSERT_FALSE(std::find(listener_->deletedFiles_.begin(), 
@@ -394,7 +403,7 @@
   CheckTableRecordCount(1, "AttachedFiles");
   CheckTableRecordCount(0, "MainDicomTags");
 
-  index_->DeleteResource(a[5]);
+  transaction_->DeleteResource(a[5]);
   ASSERT_EQ(7u, listener_->deletedResources_.size());
 
   CheckTableRecordCount(0, "Resources");
@@ -402,11 +411,11 @@
   CheckTableRecordCount(3, "GlobalProperties");
 
   std::string tmp;
-  ASSERT_TRUE(index_->LookupGlobalProperty(tmp, GlobalProperty_DatabaseSchemaVersion));
+  ASSERT_TRUE(transaction_->LookupGlobalProperty(tmp, GlobalProperty_DatabaseSchemaVersion, true));
   ASSERT_EQ("6", tmp);
-  ASSERT_TRUE(index_->LookupGlobalProperty(tmp, GlobalProperty_FlushSleep));
+  ASSERT_TRUE(transaction_->LookupGlobalProperty(tmp, GlobalProperty_FlushSleep, true));
   ASSERT_EQ("World", tmp);
-  ASSERT_TRUE(index_->LookupGlobalProperty(tmp, GlobalProperty_GetTotalSizeIsFast));
+  ASSERT_TRUE(transaction_->LookupGlobalProperty(tmp, GlobalProperty_GetTotalSizeIsFast, true));
   ASSERT_EQ("1", tmp);
 
   ASSERT_EQ(3u, listener_->deletedFiles_.size());
@@ -419,23 +428,23 @@
 TEST_F(DatabaseWrapperTest, Upward)
 {
   int64_t a[] = {
-    index_->CreateResource("a", ResourceType_Patient),   // 0
-    index_->CreateResource("b", ResourceType_Study),     // 1
-    index_->CreateResource("c", ResourceType_Series),    // 2
-    index_->CreateResource("d", ResourceType_Instance),  // 3
-    index_->CreateResource("e", ResourceType_Instance),  // 4
-    index_->CreateResource("f", ResourceType_Study),     // 5
-    index_->CreateResource("g", ResourceType_Series),    // 6
-    index_->CreateResource("h", ResourceType_Series)     // 7
+    transaction_->CreateResource("a", ResourceType_Patient),   // 0
+    transaction_->CreateResource("b", ResourceType_Study),     // 1
+    transaction_->CreateResource("c", ResourceType_Series),    // 2
+    transaction_->CreateResource("d", ResourceType_Instance),  // 3
+    transaction_->CreateResource("e", ResourceType_Instance),  // 4
+    transaction_->CreateResource("f", ResourceType_Study),     // 5
+    transaction_->CreateResource("g", ResourceType_Series),    // 6
+    transaction_->CreateResource("h", ResourceType_Series)     // 7
   };
 
-  index_->AttachChild(a[0], a[1]);
-  index_->AttachChild(a[1], a[2]);
-  index_->AttachChild(a[2], a[3]);
-  index_->AttachChild(a[2], a[4]);
-  index_->AttachChild(a[1], a[6]);
-  index_->AttachChild(a[0], a[5]);
-  index_->AttachChild(a[5], a[7]);
+  transaction_->AttachChild(a[0], a[1]);
+  transaction_->AttachChild(a[1], a[2]);
+  transaction_->AttachChild(a[2], a[3]);
+  transaction_->AttachChild(a[2], a[4]);
+  transaction_->AttachChild(a[1], a[6]);
+  transaction_->AttachChild(a[0], a[5]);
+  transaction_->AttachChild(a[5], a[7]);
 
   CheckTwoChildren("b", "f", a[0]);
   CheckTwoChildren("c", "g", a[1]);
@@ -447,22 +456,22 @@
   CheckNoChild(a[7]);
 
   listener_->Reset();
-  index_->DeleteResource(a[3]);
+  transaction_->DeleteResource(a[3]);
   ASSERT_EQ("c", listener_->ancestorId_);
   ASSERT_EQ(ResourceType_Series, listener_->ancestorType_);
 
   listener_->Reset();
-  index_->DeleteResource(a[4]);
+  transaction_->DeleteResource(a[4]);
   ASSERT_EQ("b", listener_->ancestorId_);
   ASSERT_EQ(ResourceType_Study, listener_->ancestorType_);
 
   listener_->Reset();
-  index_->DeleteResource(a[7]);
+  transaction_->DeleteResource(a[7]);
   ASSERT_EQ("a", listener_->ancestorId_);
   ASSERT_EQ(ResourceType_Patient, listener_->ancestorType_);
 
   listener_->Reset();
-  index_->DeleteResource(a[6]);
+  transaction_->DeleteResource(a[6]);
   ASSERT_EQ("", listener_->ancestorId_);  // No more ancestor
 }
 
@@ -473,10 +482,10 @@
   for (int i = 0; i < 10; i++)
   {
     std::string p = "Patient " + boost::lexical_cast<std::string>(i);
-    patients.push_back(index_->CreateResource(p, ResourceType_Patient));
-    index_->AddAttachment(patients[i], FileInfo(p, FileContentType_Dicom, i + 10, 
-                                                "md5-" + boost::lexical_cast<std::string>(i)));
-    ASSERT_FALSE(index_->IsProtectedPatient(patients[i]));
+    patients.push_back(transaction_->CreateResource(p, ResourceType_Patient));
+    transaction_->AddAttachment(patients[i], FileInfo(p, FileContentType_Dicom, i + 10, 
+                                                      "md5-" + boost::lexical_cast<std::string>(i)), 42);
+    ASSERT_FALSE(transaction_->IsProtectedPatient(patients[i]));
   }
 
   CheckTableRecordCount(10u, "Resources");
@@ -485,8 +494,8 @@
   listener_->Reset();
   ASSERT_EQ(0u, listener_->deletedResources_.size());
 
-  index_->DeleteResource(patients[5]);
-  index_->DeleteResource(patients[0]);
+  transaction_->DeleteResource(patients[5]);
+  transaction_->DeleteResource(patients[0]);
   ASSERT_EQ(2u, listener_->deletedResources_.size());
 
   CheckTableRecordCount(8u, "Resources");
@@ -497,28 +506,28 @@
   ASSERT_EQ("Patient 0", listener_->deletedFiles_[1]);
 
   int64_t p;
-  ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[1]);
-  index_->DeleteResource(p);
+  ASSERT_TRUE(transaction_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[1]);
+  transaction_->DeleteResource(p);
   ASSERT_EQ(3u, listener_->deletedResources_.size());
-  ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[2]);
-  index_->DeleteResource(p);
+  ASSERT_TRUE(transaction_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[2]);
+  transaction_->DeleteResource(p);
   ASSERT_EQ(4u, listener_->deletedResources_.size());
-  ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[3]);
-  index_->DeleteResource(p);
+  ASSERT_TRUE(transaction_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[3]);
+  transaction_->DeleteResource(p);
   ASSERT_EQ(5u, listener_->deletedResources_.size());
-  ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[4]);
-  index_->DeleteResource(p);
+  ASSERT_TRUE(transaction_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[4]);
+  transaction_->DeleteResource(p);
   ASSERT_EQ(6u, listener_->deletedResources_.size());
-  ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[6]);
-  index_->DeleteResource(p);
-  index_->DeleteResource(patients[8]);
+  ASSERT_TRUE(transaction_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[6]);
+  transaction_->DeleteResource(p);
+  transaction_->DeleteResource(patients[8]);
   ASSERT_EQ(8u, listener_->deletedResources_.size());
-  ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[7]);
-  index_->DeleteResource(p);
+  ASSERT_TRUE(transaction_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[7]);
+  transaction_->DeleteResource(p);
   ASSERT_EQ(9u, listener_->deletedResources_.size());
-  ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[9]);
-  index_->DeleteResource(p);
-  ASSERT_FALSE(index_->SelectPatientToRecycle(p));
+  ASSERT_TRUE(transaction_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[9]);
+  transaction_->DeleteResource(p);
+  ASSERT_FALSE(transaction_->SelectPatientToRecycle(p));
   ASSERT_EQ(10u, listener_->deletedResources_.size());
 
   ASSERT_EQ(10u, listener_->deletedFiles_.size());
@@ -534,39 +543,39 @@
   for (int i = 0; i < 5; i++)
   {
     std::string p = "Patient " + boost::lexical_cast<std::string>(i);
-    patients.push_back(index_->CreateResource(p, ResourceType_Patient));
-    index_->AddAttachment(patients[i], FileInfo(p, FileContentType_Dicom, i + 10,
-                                                "md5-" + boost::lexical_cast<std::string>(i)));
-    ASSERT_FALSE(index_->IsProtectedPatient(patients[i]));
+    patients.push_back(transaction_->CreateResource(p, ResourceType_Patient));
+    transaction_->AddAttachment(patients[i], FileInfo(p, FileContentType_Dicom, i + 10,
+                                                      "md5-" + boost::lexical_cast<std::string>(i)), 42);
+    ASSERT_FALSE(transaction_->IsProtectedPatient(patients[i]));
   }
 
   CheckTableRecordCount(5, "Resources");
   CheckTableRecordCount(5, "PatientRecyclingOrder");
 
-  ASSERT_FALSE(index_->IsProtectedPatient(patients[2]));
-  index_->SetProtectedPatient(patients[2], true);
-  ASSERT_TRUE(index_->IsProtectedPatient(patients[2]));
+  ASSERT_FALSE(transaction_->IsProtectedPatient(patients[2]));
+  transaction_->SetProtectedPatient(patients[2], true);
+  ASSERT_TRUE(transaction_->IsProtectedPatient(patients[2]));
   CheckTableRecordCount(5, "Resources");
   CheckTableRecordCount(4, "PatientRecyclingOrder");
 
-  index_->SetProtectedPatient(patients[2], true);
-  ASSERT_TRUE(index_->IsProtectedPatient(patients[2]));
+  transaction_->SetProtectedPatient(patients[2], true);
+  ASSERT_TRUE(transaction_->IsProtectedPatient(patients[2]));
   CheckTableRecordCount(4, "PatientRecyclingOrder");
-  index_->SetProtectedPatient(patients[2], false);
-  ASSERT_FALSE(index_->IsProtectedPatient(patients[2]));
+  transaction_->SetProtectedPatient(patients[2], false);
+  ASSERT_FALSE(transaction_->IsProtectedPatient(patients[2]));
   CheckTableRecordCount(5, "PatientRecyclingOrder");
-  index_->SetProtectedPatient(patients[2], false);
-  ASSERT_FALSE(index_->IsProtectedPatient(patients[2]));
+  transaction_->SetProtectedPatient(patients[2], false);
+  ASSERT_FALSE(transaction_->IsProtectedPatient(patients[2]));
   CheckTableRecordCount(5, "PatientRecyclingOrder");
   CheckTableRecordCount(5, "Resources");
-  index_->SetProtectedPatient(patients[2], true);
-  ASSERT_TRUE(index_->IsProtectedPatient(patients[2]));
+  transaction_->SetProtectedPatient(patients[2], true);
+  ASSERT_TRUE(transaction_->IsProtectedPatient(patients[2]));
   CheckTableRecordCount(4, "PatientRecyclingOrder");
-  index_->SetProtectedPatient(patients[2], false);
-  ASSERT_FALSE(index_->IsProtectedPatient(patients[2]));
+  transaction_->SetProtectedPatient(patients[2], false);
+  ASSERT_FALSE(transaction_->IsProtectedPatient(patients[2]));
   CheckTableRecordCount(5, "PatientRecyclingOrder");
-  index_->SetProtectedPatient(patients[3], true);
-  ASSERT_TRUE(index_->IsProtectedPatient(patients[3]));
+  transaction_->SetProtectedPatient(patients[3], true);
+  ASSERT_TRUE(transaction_->IsProtectedPatient(patients[3]));
   CheckTableRecordCount(4, "PatientRecyclingOrder");
 
   CheckTableRecordCount(5, "Resources");
@@ -575,33 +584,33 @@
   // Unprotecting a patient puts it at the last position in the recycling queue
   int64_t p;
   ASSERT_EQ(0u, listener_->deletedResources_.size());
-  ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[0]);
-  index_->DeleteResource(p);
+  ASSERT_TRUE(transaction_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[0]);
+  transaction_->DeleteResource(p);
   ASSERT_EQ(1u, listener_->deletedResources_.size());
-  ASSERT_TRUE(index_->SelectPatientToRecycle(p, patients[1])); ASSERT_EQ(p, patients[4]);
-  ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[1]);
-  index_->DeleteResource(p);
+  ASSERT_TRUE(transaction_->SelectPatientToRecycle(p, patients[1])); ASSERT_EQ(p, patients[4]);
+  ASSERT_TRUE(transaction_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[1]);
+  transaction_->DeleteResource(p);
   ASSERT_EQ(2u, listener_->deletedResources_.size());
-  ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[4]);
-  index_->DeleteResource(p);
+  ASSERT_TRUE(transaction_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[4]);
+  transaction_->DeleteResource(p);
   ASSERT_EQ(3u, listener_->deletedResources_.size());
-  ASSERT_FALSE(index_->SelectPatientToRecycle(p, patients[2]));
-  ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[2]);
-  index_->DeleteResource(p);
+  ASSERT_FALSE(transaction_->SelectPatientToRecycle(p, patients[2]));
+  ASSERT_TRUE(transaction_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[2]);
+  transaction_->DeleteResource(p);
   ASSERT_EQ(4u, listener_->deletedResources_.size());
   // "patients[3]" is still protected
-  ASSERT_FALSE(index_->SelectPatientToRecycle(p));
+  ASSERT_FALSE(transaction_->SelectPatientToRecycle(p));
 
   ASSERT_EQ(4u, listener_->deletedFiles_.size());
   CheckTableRecordCount(1, "Resources");
   CheckTableRecordCount(0, "PatientRecyclingOrder");
 
-  index_->SetProtectedPatient(patients[3], false);
+  transaction_->SetProtectedPatient(patients[3], false);
   CheckTableRecordCount(1, "PatientRecyclingOrder");
-  ASSERT_FALSE(index_->SelectPatientToRecycle(p, patients[3]));
-  ASSERT_TRUE(index_->SelectPatientToRecycle(p, patients[2]));
-  ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[3]);
-  index_->DeleteResource(p);
+  ASSERT_FALSE(transaction_->SelectPatientToRecycle(p, patients[3]));
+  ASSERT_TRUE(transaction_->SelectPatientToRecycle(p, patients[2]));
+  ASSERT_TRUE(transaction_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[3]);
+  transaction_->DeleteResource(p);
   ASSERT_EQ(5u, listener_->deletedResources_.size());
 
   ASSERT_EQ(5u, listener_->deletedFiles_.size());
@@ -623,10 +632,10 @@
 
   ServerIndex& index = context.GetIndex();
 
-  ASSERT_EQ(1u, index.IncrementGlobalSequence(GlobalProperty_AnonymizationSequence));
-  ASSERT_EQ(2u, index.IncrementGlobalSequence(GlobalProperty_AnonymizationSequence));
-  ASSERT_EQ(3u, index.IncrementGlobalSequence(GlobalProperty_AnonymizationSequence));
-  ASSERT_EQ(4u, index.IncrementGlobalSequence(GlobalProperty_AnonymizationSequence));
+  ASSERT_EQ(1u, index.IncrementGlobalSequence(GlobalProperty_AnonymizationSequence, true));
+  ASSERT_EQ(2u, index.IncrementGlobalSequence(GlobalProperty_AnonymizationSequence, true));
+  ASSERT_EQ(3u, index.IncrementGlobalSequence(GlobalProperty_AnonymizationSequence, true));
+  ASSERT_EQ(4u, index.IncrementGlobalSequence(GlobalProperty_AnonymizationSequence, true));
 
   context.Stop();
   db.Close();
@@ -636,16 +645,16 @@
 TEST_F(DatabaseWrapperTest, LookupIdentifier)
 {
   int64_t a[] = {
-    index_->CreateResource("a", ResourceType_Study),   // 0
-    index_->CreateResource("b", ResourceType_Study),   // 1
-    index_->CreateResource("c", ResourceType_Study),   // 2
-    index_->CreateResource("d", ResourceType_Series)   // 3
+    transaction_->CreateResource("a", ResourceType_Study),   // 0
+    transaction_->CreateResource("b", ResourceType_Study),   // 1
+    transaction_->CreateResource("c", ResourceType_Study),   // 2
+    transaction_->CreateResource("d", ResourceType_Series)   // 3
   };
 
-  index_->SetIdentifierTag(a[0], DICOM_TAG_STUDY_INSTANCE_UID, "0");
-  index_->SetIdentifierTag(a[1], DICOM_TAG_STUDY_INSTANCE_UID, "1");
-  index_->SetIdentifierTag(a[2], DICOM_TAG_STUDY_INSTANCE_UID, "0");
-  index_->SetIdentifierTag(a[3], DICOM_TAG_SERIES_INSTANCE_UID, "0");
+  transaction_->SetIdentifierTag(a[0], DICOM_TAG_STUDY_INSTANCE_UID, "0");
+  transaction_->SetIdentifierTag(a[1], DICOM_TAG_STUDY_INSTANCE_UID, "1");
+  transaction_->SetIdentifierTag(a[2], DICOM_TAG_STUDY_INSTANCE_UID, "0");
+  transaction_->SetIdentifierTag(a[3], DICOM_TAG_SERIES_INSTANCE_UID, "0");
 
   std::list<std::string> s;
 
@@ -776,7 +785,9 @@
   for (size_t i = 0; i < ids.size(); i++)
   {
     FileInfo info(Toolbox::GenerateUuid(), FileContentType_Dicom, 1, "md5");
-    index.AddAttachment(info, ids[i]);
+    int64_t revision = -1;
+    index.AddAttachment(revision, info, ids[i], false /* no previous revision */, -1);
+    ASSERT_EQ(0, revision);
 
     index.GetGlobalStatistics(diskSize, uncompressedSize, countPatients, 
                               countStudies, countSeries, countInstances);
@@ -854,12 +865,17 @@
 
     {
       FileInfo nope;
-      ASSERT_FALSE(context.GetIndex().LookupAttachment(nope, id, FileContentType_DicomAsJson));
+      int64_t revision;
+      ASSERT_FALSE(context.GetIndex().LookupAttachment(nope, revision, id, FileContentType_DicomAsJson));
     }
 
     FileInfo dicom1, pixelData1;
-    ASSERT_TRUE(context.GetIndex().LookupAttachment(dicom1, id, FileContentType_Dicom));
-    ASSERT_TRUE(context.GetIndex().LookupAttachment(pixelData1, id, FileContentType_DicomUntilPixelData));
+    int64_t revision;
+    ASSERT_TRUE(context.GetIndex().LookupAttachment(dicom1, revision, id, FileContentType_Dicom));
+    ASSERT_EQ(0, revision);
+    revision = -1;
+    ASSERT_TRUE(context.GetIndex().LookupAttachment(pixelData1, revision, id, FileContentType_DicomUntilPixelData));
+    ASSERT_EQ(0, revision);
 
     context.GetIndex().GetGlobalStatistics(diskSize, uncompressedSize, countPatients, 
                                            countStudies, countSeries, countInstances);
@@ -899,12 +915,16 @@
 
     {
       FileInfo nope;
-      ASSERT_FALSE(context.GetIndex().LookupAttachment(nope, id, FileContentType_DicomAsJson));
+      int64_t revision;
+      ASSERT_FALSE(context.GetIndex().LookupAttachment(nope, revision, id, FileContentType_DicomAsJson));
     }
 
     FileInfo dicom2, pixelData2;
-    ASSERT_TRUE(context.GetIndex().LookupAttachment(dicom2, id, FileContentType_Dicom));
-    ASSERT_TRUE(context.GetIndex().LookupAttachment(pixelData2, id, FileContentType_DicomUntilPixelData));
+    ASSERT_TRUE(context.GetIndex().LookupAttachment(dicom2, revision, id, FileContentType_Dicom));
+    ASSERT_EQ(0, revision);
+    revision = -1;
+    ASSERT_TRUE(context.GetIndex().LookupAttachment(pixelData2, revision, id, FileContentType_DicomUntilPixelData));
+    ASSERT_EQ(0, revision);
 
     context.GetIndex().GetGlobalStatistics(diskSize, uncompressedSize, countPatients, 
                                            countStudies, countSeries, countInstances);
@@ -1008,12 +1028,14 @@
       }
 
       std::string s;
-      bool found = context.GetIndex().LookupMetadata(s, id, ResourceType_Instance,
+      int64_t revision;
+      bool found = context.GetIndex().LookupMetadata(s, revision, id, ResourceType_Instance,
                                                      MetadataType_Instance_PixelDataOffset);
       
       if (withPixelData)
       {
         ASSERT_TRUE(found);
+        ASSERT_EQ(0, revision);
         ASSERT_GT(boost::lexical_cast<int>(s), 128 /* length of the DICOM preamble */);
         ASSERT_LT(boost::lexical_cast<size_t>(s), dicomSize);
       }
--- a/OrthancServer/UnitTestsSources/SizeOfTests.cpp	Mon Apr 19 10:28:24 2021 +0200
+++ b/OrthancServer/UnitTestsSources/SizeOfTests.cpp	Tue Apr 20 18:11:29 2021 +0200
@@ -129,7 +129,6 @@
 #include "../../OrthancFramework/Sources/Images/ImageBuffer.h"
 #include "../../OrthancFramework/Sources/Images/ImageProcessing.h"
 #include "../../OrthancFramework/Sources/Images/ImageTraits.h"
-#include "../../OrthancFramework/Sources/Images/JpegErrorManager.h"
 #include "../../OrthancFramework/Sources/Images/JpegReader.h"
 #include "../../OrthancFramework/Sources/Images/JpegWriter.h"
 #include "../../OrthancFramework/Sources/Images/PamReader.h"
--- a/TODO	Mon Apr 19 10:28:24 2021 +0200
+++ b/TODO	Tue Apr 20 18:11:29 2021 +0200
@@ -97,6 +97,13 @@
 * Check out rapidjson: https://github.com/miloyip/nativejson-benchmark
 
 
+========
+Database
+========
+
+* Integration test searching for "\" and "%" in PatientName, PatientID...
+
+
 =======
 Plugins
 =======