changeset 92:2e4f73786199 db-changes

integration mainline->db-changes
author Sebastien Jodogne <s.jodogne@gmail.com>
date Thu, 17 Jan 2019 11:42:42 +0100
parents e61587582cef (diff) 1bd538a5a783 (current diff)
children 5571a6554db0
files
diffstat 24 files changed, 1511 insertions(+), 148 deletions(-) [+]
line wrap: on
line diff
--- a/Framework/Common/DatabaseManager.cpp	Thu Jan 17 11:42:24 2019 +0100
+++ b/Framework/Common/DatabaseManager.cpp	Thu Jan 17 11:42:42 2019 +0100
@@ -279,34 +279,6 @@
   }
 
 
-  IResult& DatabaseManager::CachedStatement::GetResult() const
-  {
-    if (result_.get() == NULL)
-    {
-      LOG(ERROR) << "Accessing the results of a statement without having executed it";
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
-    }
-
-    return *result_;
-  }
-
-
-  void DatabaseManager::CachedStatement::Setup(const char* sql)
-  {
-    statement_ = manager_.LookupCachedStatement(location_);
-
-    if (statement_ == NULL)
-    {
-      query_.reset(new Query(sql));
-    }
-    else
-    {
-      LOG(TRACE) << "Reusing cached statement from "
-                 << location_.GetFile() << ":" << location_.GetLine();
-    }
-  }
-
-
   DatabaseManager::Transaction::Transaction(DatabaseManager& manager) :
     lock_(manager.mutex_),
     manager_(manager),
@@ -347,40 +319,72 @@
     }
   }
 
-  
-  DatabaseManager::CachedStatement::CachedStatement(const StatementLocation& location,
-                                                    DatabaseManager& manager,
-                                                    const char* sql) :
-    manager_(manager),
-    lock_(manager_.mutex_),
-    database_(manager_.GetDatabase()),
-    location_(location),
-    transaction_(manager_.GetTransaction())
+
+  IResult& DatabaseManager::StatementBase::GetResult() const
   {
-    Setup(sql);
-  }
+    if (result_.get() == NULL)
+    {
+      LOG(ERROR) << "Accessing the results of a statement without having executed it";
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
 
-      
-  DatabaseManager::CachedStatement::CachedStatement(const StatementLocation& location,
-                                                    Transaction& transaction,
-                                                    const char* sql) :
-    manager_(transaction.GetManager()),
-    lock_(manager_.mutex_),
-    database_(manager_.GetDatabase()),
-    location_(location),
-    transaction_(manager_.GetTransaction())
-  {
-    Setup(sql);
+    return *result_;
   }
 
 
-  DatabaseManager::CachedStatement::~CachedStatement()
+  void DatabaseManager::StatementBase::SetQuery(Query* query)
+  {
+    std::auto_ptr<Query> protection(query);
+    
+    if (query_.get() != NULL)
+    {
+      LOG(ERROR) << "Cannot set twice a query";
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+
+    if (query == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+
+    query_.reset(protection.release());
+  }
+
+  
+  void DatabaseManager::StatementBase::SetResult(IResult* result)
+  {
+    std::auto_ptr<IResult> protection(result);
+    
+    if (result_.get() != NULL)
+    {
+      LOG(ERROR) << "Cannot execute twice a statement";
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+
+    if (result == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+
+    result_.reset(protection.release());
+  }
+
+  
+  DatabaseManager::StatementBase::StatementBase(DatabaseManager& manager) :
+    manager_(manager),
+    lock_(manager_.mutex_),
+    transaction_(manager_.GetTransaction())
+  {
+  }
+
+
+  DatabaseManager::StatementBase::~StatementBase()
   {
     manager_.ReleaseImplicitTransaction();
   }
+
   
-      
-  void DatabaseManager::CachedStatement::SetReadOnly(bool readOnly)
+  void DatabaseManager::StatementBase::SetReadOnly(bool readOnly)
   {
     if (query_.get() != NULL)
     {
@@ -389,8 +393,8 @@
   }
 
 
-  void DatabaseManager::CachedStatement::SetParameterType(const std::string& parameter,
-                                                          ValueType type)
+  void DatabaseManager::StatementBase::SetParameterType(const std::string& parameter,
+                                                        ValueType type)
   {
     if (query_.get() != NULL)
     {
@@ -398,44 +402,7 @@
     }
   }
       
-      
-  void DatabaseManager::CachedStatement::Execute()
-  {
-    Dictionary parameters;
-    Execute(parameters);
-  }
-
-
-  void DatabaseManager::CachedStatement::Execute(const Dictionary& parameters)
-  {
-    if (result_.get() != NULL)
-    {
-      LOG(ERROR) << "Cannot execute twice a statement";
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
-    }
-
-    try
-    {
-      if (query_.get() != NULL)
-      {
-        // Register the newly-created statement
-        assert(statement_ == NULL);
-        statement_ = &manager_.CacheStatement(location_, *query_);
-        query_.reset(NULL);
-      }
-        
-      assert(statement_ != NULL);
-      result_.reset(transaction_.Execute(*statement_, parameters));
-    }
-    catch (Orthanc::OrthancException& e)
-    {
-      manager_.CloseIfUnavailable(e.GetErrorCode());
-      throw;
-    }
-  }
-
-
-  bool DatabaseManager::CachedStatement::IsDone() const
+  bool DatabaseManager::StatementBase::IsDone() const
   {
     try
     {
@@ -449,7 +416,7 @@
   }
 
 
-  void DatabaseManager::CachedStatement::Next()
+  void DatabaseManager::StatementBase::Next()
   {
     try
     {
@@ -463,7 +430,7 @@
   }
 
 
-  size_t DatabaseManager::CachedStatement::GetResultFieldsCount() const
+  size_t DatabaseManager::StatementBase::GetResultFieldsCount() const
   {
     try
     {
@@ -477,8 +444,8 @@
   }
 
 
-  void DatabaseManager::CachedStatement::SetResultFieldType(size_t field,
-                                                            ValueType type)
+  void DatabaseManager::StatementBase::SetResultFieldType(size_t field,
+                                                          ValueType type)
   {
     try
     {
@@ -495,7 +462,7 @@
   }
 
 
-  const IValue& DatabaseManager::CachedStatement::GetResultField(size_t index) const
+  const IValue& DatabaseManager::StatementBase::GetResultField(size_t index) const
   {
     try
     {
@@ -506,5 +473,88 @@
       manager_.CloseIfUnavailable(e.GetErrorCode());
       throw;
     }
+  }  
+  
+  
+  DatabaseManager::CachedStatement::CachedStatement(const StatementLocation& location,
+                                                    DatabaseManager& manager,
+                                                    const std::string& sql) :
+    StatementBase(manager),
+    location_(location)
+  {
+    statement_ = GetManager().LookupCachedStatement(location_);
+
+    if (statement_ == NULL)
+    {
+      SetQuery(new Query(sql));
+    }
+    else
+    {
+      LOG(TRACE) << "Reusing cached statement from "
+                 << location_.GetFile() << ":" << location_.GetLine();
+    }
+  }
+
+      
+  void DatabaseManager::CachedStatement::Execute(const Dictionary& parameters)
+  {
+    try
+    {
+      std::auto_ptr<Query> query(ReleaseQuery());
+      
+      if (query.get() != NULL)
+      {
+        // Register the newly-created statement
+        assert(statement_ == NULL);
+        statement_ = &GetManager().CacheStatement(location_, *query);
+      }
+        
+      assert(statement_ != NULL);
+      SetResult(GetTransaction().Execute(*statement_, parameters));
+    }
+    catch (Orthanc::OrthancException& e)
+    {
+      GetManager().CloseIfUnavailable(e.GetErrorCode());
+      throw;
+    }
+  }
+  
+  
+  DatabaseManager::StandaloneStatement::StandaloneStatement(DatabaseManager& manager,
+                                                            const std::string& sql) :
+    StatementBase(manager)
+  {
+    SetQuery(new Query(sql));
+  }
+
+      
+  DatabaseManager::StandaloneStatement::~StandaloneStatement()
+  {
+    // The result must be removed before the statement, cf. (*)
+    ClearResult();
+    statement_.reset();
+  }
+
+
+  void DatabaseManager::StandaloneStatement::Execute(const Dictionary& parameters)
+  {
+    try
+    {
+      std::auto_ptr<Query> query(ReleaseQuery());
+      assert(query.get() != NULL);
+
+      // The "statement_" object must be kept as long as the "IResult"
+      // is not destroyed, as the "IResult" can make calls to the
+      // statement (this is the case for SQLite and MySQL) - (*)
+      statement_.reset(GetManager().GetDatabase().Compile(*query));
+      assert(statement_.get() != NULL);
+
+      SetResult(GetTransaction().Execute(*statement_, parameters));
+    }
+    catch (Orthanc::OrthancException& e)
+    {
+      GetManager().CloseIfUnavailable(e.GetErrorCode());
+      throw;
+    }
   }
 }
--- a/Framework/Common/DatabaseManager.h	Thu Jan 17 11:42:24 2019 +0100
+++ b/Framework/Common/DatabaseManager.h	Thu Jan 17 11:42:42 2019 +0100
@@ -111,36 +111,51 @@
     };
 
 
-    class CachedStatement : public boost::noncopyable
+    class StatementBase : public boost::noncopyable
     {
     private:
       DatabaseManager&                     manager_;
       boost::recursive_mutex::scoped_lock  lock_;
-      IDatabase&                           database_;
-      StatementLocation                    location_;
       ITransaction&                        transaction_;
-      IPrecompiledStatement*               statement_;
       std::auto_ptr<Query>                 query_;
       std::auto_ptr<IResult>               result_;
 
-      void Setup(const char* sql);
-
       IResult& GetResult() const;
 
-    public:
-      CachedStatement(const StatementLocation& location,
-                      DatabaseManager& manager,
-                      const char* sql);
+    protected:
+      DatabaseManager& GetManager() const
+      {
+        return manager_;
+      }
+
+      ITransaction& GetTransaction() const
+      {
+        return transaction_;
+      }
+      
+      void SetQuery(Query* query);
+
+      void SetResult(IResult* result);
 
-      CachedStatement(const StatementLocation& location,
-                      Transaction& transaction,
-                      const char* sql);
+      void ClearResult()
+      {
+        result_.reset();
+      }
 
-      ~CachedStatement();
+      Query* ReleaseQuery()
+      {
+        return query_.release();
+      }
 
+    public:
+      StatementBase(DatabaseManager& manager);
+
+      virtual ~StatementBase();
+
+      // Used only by SQLite
       IDatabase& GetDatabase()
       {
-        return database_;
+        return manager_.GetDatabase();
       }
 
       void SetReadOnly(bool readOnly);
@@ -148,10 +163,6 @@
       void SetParameterType(const std::string& parameter,
                             ValueType type);
       
-      void Execute();
-
-      void Execute(const Dictionary& parameters);
-
       bool IsDone() const;
       
       void Next();
@@ -163,5 +174,47 @@
       
       const IValue& GetResultField(size_t index) const;
     };
+
+
+    class CachedStatement : public StatementBase
+    {
+    private:
+      StatementLocation       location_;
+      IPrecompiledStatement*  statement_;
+
+    public:
+      CachedStatement(const StatementLocation& location,
+                      DatabaseManager& manager,
+                      const std::string& sql);
+
+      void Execute()
+      {
+        Dictionary parameters;
+        Execute(parameters);
+      }
+
+      void Execute(const Dictionary& parameters);
+    };
+
+
+    class StandaloneStatement : public StatementBase
+    {
+    private:
+      std::auto_ptr<IPrecompiledStatement>  statement_;
+      
+    public:
+      StandaloneStatement(DatabaseManager& manager,
+                          const std::string& sql);
+
+      virtual ~StandaloneStatement();
+
+      void Execute()
+      {
+        Dictionary parameters;
+        Execute(parameters);
+      }
+
+      void Execute(const Dictionary& parameters);
+    };
   };
 }
--- a/Framework/Common/Query.cpp	Thu Jan 17 11:42:24 2019 +0100
+++ b/Framework/Common/Query.cpp	Thu Jan 17 11:42:42 2019 +0100
@@ -125,8 +125,8 @@
 
     if (found == parameters_.end())
     {
-      LOG(ERROR) << "Inexistent parameter in a SQL query: " << parameter;
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentItem);
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentItem,
+                                      "Inexistent parameter in a SQL query: " + parameter);
     }
     else
     {
@@ -142,8 +142,8 @@
 
     if (found == parameters_.end())
     {
-      LOG(ERROR) << "Ignoring inexistent parameter in a SQL query: " << parameter;
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentItem,
+                                      "Inexistent parameter in a SQL query: " + parameter);
     }
     else
     {
--- a/Framework/Plugins/IndexBackend.cpp	Thu Jan 17 11:42:24 2019 +0100
+++ b/Framework/Plugins/IndexBackend.cpp	Thu Jan 17 11:42:42 2019 +0100
@@ -29,6 +29,7 @@
 #include <Core/Logging.h>
 #include <Core/OrthancException.h>
 #include <OrthancServer/ServerEnumerations.h>
+#include <OrthancServer/Search/ISqlLookupFormatter.h>
 
 
 namespace OrthancDatabases
@@ -55,7 +56,7 @@
   }
 
   
-  int64_t IndexBackend::ReadInteger64(const DatabaseManager::CachedStatement& statement,
+  int64_t IndexBackend::ReadInteger64(const DatabaseManager::StatementBase& statement,
                                       size_t field)
   {
     if (statement.IsDone())
@@ -77,7 +78,7 @@
   }
 
 
-  int32_t IndexBackend::ReadInteger32(const DatabaseManager::CachedStatement& statement,
+  int32_t IndexBackend::ReadInteger32(const DatabaseManager::StatementBase& statement,
                                       size_t field)
   {
     if (statement.IsDone())
@@ -99,11 +100,11 @@
   }
 
     
-  std::string IndexBackend::ReadString(const DatabaseManager::CachedStatement& statement,
+  std::string IndexBackend::ReadString(const DatabaseManager::StatementBase& statement,
                                        size_t field)
   {
     const IValue& value = statement.GetResultField(field);
-      
+
     switch (value.GetType())
     {
       case ValueType_BinaryString:
@@ -704,14 +705,14 @@
 
       case Dialect_PostgreSQL:
         statement.reset(new DatabaseManager::CachedStatement(
-                        STATEMENT_FROM_HERE, GetManager(),
-                        "SELECT CAST(COUNT(*) AS BIGINT) FROM Resources WHERE resourceType=${type}"));
+                          STATEMENT_FROM_HERE, GetManager(),
+                          "SELECT CAST(COUNT(*) AS BIGINT) FROM Resources WHERE resourceType=${type}"));
         break;
 
       case Dialect_SQLite:
         statement.reset(new DatabaseManager::CachedStatement(
-                        STATEMENT_FROM_HERE, GetManager(),
-                        "SELECT COUNT(*) FROM Resources WHERE resourceType=${type}"));
+                          STATEMENT_FROM_HERE, GetManager(),
+                          "SELECT COUNT(*) FROM Resources WHERE resourceType=${type}"));
         break;
 
       default:
@@ -1579,4 +1580,375 @@
 
     ReadListOfStrings(childrenPublicIds, statement, args);
   }
+
+
+#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
+  class IndexBackend::LookupFormatter : public Orthanc::ISqlLookupFormatter
+  {
+  private:
+    Dialect     dialect_;
+    size_t      count_;
+    Dictionary  dictionary_;
+
+    static std::string FormatParameter(size_t index)
+    {
+      return "p" + boost::lexical_cast<std::string>(index);
+    }
+    
+  public:
+    LookupFormatter(Dialect dialect) :
+      dialect_(dialect),
+      count_(0)
+    {
+    }
+
+    virtual std::string GenerateParameter(const std::string& value)
+    {
+      const std::string key = FormatParameter(count_);
+
+      count_ ++;
+      dictionary_.SetUtf8Value(key, value);
+
+      return "${" + key + "}";
+    }
+
+    virtual std::string FormatResourceType(Orthanc::ResourceType level)
+    {
+      return boost::lexical_cast<std::string>(Orthanc::Plugins::Convert(level));
+    }
+
+    virtual std::string FormatWildcardEscape()
+    {
+      switch (dialect_)
+      {
+        case Dialect_SQLite:
+        case Dialect_PostgreSQL:
+          return "ESCAPE '\\'";
+
+        case Dialect_MySQL:
+          return "ESCAPE '\\\\'";
+
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+      }
+    }
+
+    void PrepareStatement(DatabaseManager::StandaloneStatement& statement) const
+    {
+      statement.SetReadOnly(true);
+      
+      for (size_t i = 0; i < count_; i++)
+      {
+        statement.SetParameterType(FormatParameter(i), ValueType_Utf8String);
+      }
+    }
+
+    const Dictionary& GetDictionary() const
+    {
+      return dictionary_;
+    }
+  };
+#endif
+
+  
+#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
+  // New primitive since Orthanc 1.5.2
+  void IndexBackend::LookupResources(const std::vector<Orthanc::DatabaseConstraint>& lookup,
+                                     OrthancPluginResourceType queryLevel,
+                                     uint32_t limit,
+                                     bool requestSomeInstance)
+  {
+    LookupFormatter formatter(manager_.GetDialect());
+
+    std::string sql;
+    Orthanc::ISqlLookupFormatter::Apply(sql, formatter, lookup,
+                                        Orthanc::Plugins::Convert(queryLevel), limit);
+
+    if (requestSomeInstance)
+    {
+      // Composite query to find some instance if requested
+      switch (queryLevel)
+      {
+        case OrthancPluginResourceType_Patient:
+          sql = ("SELECT patients.publicId, MIN(instances.publicId) FROM (" + sql + ") patients "
+                 "INNER JOIN Resources studies   ON studies.parentId   = patients.internalId "
+                 "INNER JOIN Resources series    ON series.parentId    = studies.internalId "
+                 "INNER JOIN Resources instances ON instances.parentId = series.internalId "
+                 "GROUP BY patients.publicId");
+          break;
+
+        case OrthancPluginResourceType_Study:
+          sql = ("SELECT studies.publicId, MIN(instances.publicId) FROM (" + sql + ") studies "
+                 "INNER JOIN Resources series    ON series.parentId    = studies.internalId "
+                 "INNER JOIN Resources instances ON instances.parentId = series.internalId "
+                 "GROUP BY studies.publicId");                 
+          break;
+
+        case OrthancPluginResourceType_Series:
+          sql = ("SELECT series.publicId, MIN(instances.publicId) FROM (" + sql + ") series "
+                 "INNER JOIN Resources instances ON instances.parentId = series.internalId "
+                 "GROUP BY series.publicId");
+          break;
+
+        case OrthancPluginResourceType_Instance:
+          sql = ("SELECT instances.publicId, instances.publicId FROM (" + sql + ") instances");
+          break;
+
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+    }
+
+    DatabaseManager::StandaloneStatement statement(GetManager(), sql);
+    formatter.PrepareStatement(statement);
+
+    statement.Execute(formatter.GetDictionary());
+
+    while (!statement.IsDone())
+    {
+      if (requestSomeInstance)
+      {
+        GetOutput().AnswerMatchingResource(ReadString(statement, 0), ReadString(statement, 1));
+      }
+      else
+      {
+        GetOutput().AnswerMatchingResource(ReadString(statement, 0));
+      }
+
+      statement.Next();
+    }    
+  }
+#endif
+
+
+#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
+  static void ExecuteSetResourcesContentTags(
+    DatabaseManager& manager,
+    const std::string& table,
+    const std::string& variablePrefix,
+    uint32_t count,
+    const OrthancPluginResourcesContentTags* tags)
+  {
+    std::string sql;
+    Dictionary args;
+    
+    for (uint32_t i = 0; i < count; i++)
+    {
+      std::string name = variablePrefix + boost::lexical_cast<std::string>(i);
+
+      args.SetUtf8Value(name, tags[i].value);
+      
+      std::string insert = ("(" + boost::lexical_cast<std::string>(tags[i].resource) + ", " +
+                           boost::lexical_cast<std::string>(tags[i].group) + ", " +
+                           boost::lexical_cast<std::string>(tags[i].element) + ", " +
+                           "${" + name + "})");
+
+      if (sql.empty())
+      {
+        sql = "INSERT INTO " + table + " VALUES " + insert;
+      }
+      else
+      {
+        sql += ", " + insert;
+      }
+    }
+
+    if (!sql.empty())
+    {
+      DatabaseManager::StandaloneStatement statement(manager, sql);
+
+      for (uint32_t i = 0; i < count; i++)
+      {
+        statement.SetParameterType(variablePrefix + boost::lexical_cast<std::string>(i),
+                                   ValueType_Utf8String);
+      }
+
+      statement.Execute(args);
+    }
+  }
+#endif
+  
+
+#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
+  static void ExecuteSetResourcesContentMetadata(
+    DatabaseManager& manager,
+    uint32_t count,
+    const OrthancPluginResourcesContentMetadata* metadata)
+  {
+    std::string sqlRemove;  // To overwrite    
+    std::string sqlInsert;
+    Dictionary args;
+    
+    for (uint32_t i = 0; i < count; i++)
+    {
+      std::string name = "m" + boost::lexical_cast<std::string>(i);
+
+      args.SetUtf8Value(name, metadata[i].value);
+      
+      std::string insert = ("(" + boost::lexical_cast<std::string>(metadata[i].resource) + ", " +
+                            boost::lexical_cast<std::string>(metadata[i].metadata) + ", " +
+                           "${" + name + "})");
+
+      std::string remove = ("(id=" + boost::lexical_cast<std::string>(metadata[i].resource) +
+                            " AND type=" + boost::lexical_cast<std::string>(metadata[i].metadata)
+                            + ")");
+
+      if (sqlInsert.empty())
+      {
+        sqlInsert = "INSERT INTO Metadata VALUES " + insert;
+      }
+      else
+      {
+        sqlInsert += ", " + insert;
+      }
+
+      if (sqlRemove.empty())
+      {
+        sqlRemove = "DELETE FROM Metadata WHERE " + remove;
+      }
+      else
+      {
+        sqlRemove += " OR " + remove;
+      }
+    }
+
+    if (!sqlRemove.empty())
+    {
+      DatabaseManager::StandaloneStatement statement(manager, sqlRemove);
+      statement.Execute();
+    }
+    
+    if (!sqlInsert.empty())
+    {
+      DatabaseManager::StandaloneStatement statement(manager, sqlInsert);
+
+      for (uint32_t i = 0; i < count; i++)
+      {
+        statement.SetParameterType("m" + boost::lexical_cast<std::string>(i),
+                                   ValueType_Utf8String);
+      }
+
+      statement.Execute(args);
+    }
+  }
+#endif
+  
+
+#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
+  // New primitive since Orthanc 1.5.2
+  void IndexBackend::SetResourcesContent(
+    uint32_t countIdentifierTags,
+    const OrthancPluginResourcesContentTags* identifierTags,
+    uint32_t countMainDicomTags,
+    const OrthancPluginResourcesContentTags* mainDicomTags,
+    uint32_t countMetadata,
+    const OrthancPluginResourcesContentMetadata* metadata)
+  {
+    /**
+     * TODO - PostgreSQL doesn't allow multiple commands in a prepared
+     * statement, so we execute 3 separate commands (for identifiers,
+     * main tags and metadata). Maybe MySQL does not suffer from the
+     * same limitation, to check.
+     **/
+    
+    ExecuteSetResourcesContentTags(GetManager(), "DicomIdentifiers", "i",
+                                   countIdentifierTags, identifierTags);
+
+    ExecuteSetResourcesContentTags(GetManager(), "MainDicomTags", "t",
+                                   countMainDicomTags, mainDicomTags);
+
+    ExecuteSetResourcesContentMetadata(GetManager(), countMetadata, metadata);
+  }
+#endif
+
+
+  // New primitive since Orthanc 1.5.2
+  void IndexBackend::GetChildrenMetadata(std::list<std::string>& target,
+                                         int64_t resourceId,
+                                         int32_t metadata)
+  {
+    DatabaseManager::CachedStatement statement(
+      STATEMENT_FROM_HERE, manager_,
+      "SELECT value FROM Metadata WHERE type=${metadata} AND "
+      "id IN (SELECT internalId FROM Resources WHERE parentId=${id})");
+      
+    statement.SetReadOnly(true);
+    statement.SetParameterType("id", ValueType_Integer64);
+    statement.SetParameterType("metadata", ValueType_Integer64);
+
+    Dictionary args;
+    args.SetIntegerValue("id", static_cast<int>(resourceId));
+    args.SetIntegerValue("metadata", static_cast<int>(metadata));
+
+    ReadListOfStrings(target, statement, args);
+  }
+
+
+  // New primitive since Orthanc 1.5.2
+  void IndexBackend::TagMostRecentPatient(int64_t patient)
+  {
+    int64_t seq;
+    
+    {
+      DatabaseManager::CachedStatement statement(
+        STATEMENT_FROM_HERE, manager_,
+        "SELECT * FROM PatientRecyclingOrder WHERE seq >= "
+        "(SELECT seq FROM PatientRecyclingOrder WHERE patientid=${id}) ORDER BY seq LIMIT 2");
+
+      statement.SetReadOnly(true);
+      statement.SetParameterType("id", ValueType_Integer64);
+
+      Dictionary args;
+      args.SetIntegerValue("id", patient);
+
+      statement.Execute(args);
+      
+      if (statement.IsDone())
+      {
+        // The patient is protected, don't add it to the recycling order
+        return;
+      }
+
+      seq = ReadInteger64(statement, 0);
+
+      statement.Next();
+
+      if (statement.IsDone())
+      {
+        // The patient is already at the end of the recycling order
+        // (because of the "LIMIT 2" above), no need to modify the table
+        return;
+      }
+    }
+
+    // Delete the old position of the patient in the recycling order
+
+    {
+      DatabaseManager::CachedStatement statement(
+        STATEMENT_FROM_HERE, manager_,
+        "DELETE FROM PatientRecyclingOrder WHERE seq=${seq}");
+        
+      statement.SetParameterType("seq", ValueType_Integer64);
+        
+      Dictionary args;
+      args.SetIntegerValue("seq", seq);
+        
+      statement.Execute(args);
+    }
+
+    // Add the patient to the end of the recycling order
+
+    {
+      DatabaseManager::CachedStatement statement(
+        STATEMENT_FROM_HERE, manager_,
+        "INSERT INTO PatientRecyclingOrder VALUES(${}, ${id})");
+        
+      statement.SetParameterType("id", ValueType_Integer64);
+        
+      Dictionary args;
+      args.SetIntegerValue("id", patient);
+        
+      statement.Execute(args);
+    }
+  }
 }
--- a/Framework/Plugins/IndexBackend.h	Thu Jan 17 11:42:24 2019 +0100
+++ b/Framework/Plugins/IndexBackend.h	Thu Jan 17 11:42:42 2019 +0100
@@ -30,6 +30,8 @@
   class IndexBackend : public OrthancPlugins::IDatabaseBackend
   {
   private:
+    class LookupFormatter;
+    
     DatabaseManager   manager_;
 
   protected:
@@ -38,13 +40,13 @@
       return manager_;
     }
     
-    static int64_t ReadInteger64(const DatabaseManager::CachedStatement& statement,
+    static int64_t ReadInteger64(const DatabaseManager::StatementBase& statement,
                                  size_t field);
 
-    static int32_t ReadInteger32(const DatabaseManager::CachedStatement& statement,
+    static int32_t ReadInteger32(const DatabaseManager::StatementBase& statement,
                                  size_t field);
     
-    static std::string ReadString(const DatabaseManager::CachedStatement& statement,
+    static std::string ReadString(const DatabaseManager::StatementBase& statement,
                                   size_t field);
     
     template <typename T>
@@ -242,7 +244,6 @@
     
     virtual void ClearMainDicomTags(int64_t internalId);
 
-
     // For unit testing only!
     virtual uint64_t GetResourcesCount();
 
@@ -256,5 +257,31 @@
     // For unit tests only!
     virtual void GetChildren(std::list<std::string>& childrenPublicIds,
                              int64_t id);
+
+#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
+    // New primitive since Orthanc 1.5.2
+    virtual void LookupResources(const std::vector<Orthanc::DatabaseConstraint>& lookup,
+                                 OrthancPluginResourceType queryLevel,
+                                 uint32_t limit,
+                                 bool requestSomeInstance);
+#endif
+
+#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
+    // New primitive since Orthanc 1.5.2
+    virtual void SetResourcesContent(
+      uint32_t countIdentifierTags,
+      const OrthancPluginResourcesContentTags* identifierTags,
+      uint32_t countMainDicomTags,
+      const OrthancPluginResourcesContentTags* mainDicomTags,
+      uint32_t countMetadata,
+      const OrthancPluginResourcesContentMetadata* metadata);
+#endif
+
+    // New primitive since Orthanc 1.5.2
+    virtual void GetChildrenMetadata(std::list<std::string>& target,
+                                     int64_t resourceId,
+                                     int32_t metadata);
+
+    virtual void TagMostRecentPatient(int64_t patient);
   };
 }
--- a/Framework/Plugins/OrthancCppDatabasePlugin.h	Thu Jan 17 11:42:24 2019 +0100
+++ b/Framework/Plugins/OrthancCppDatabasePlugin.h	Thu Jan 17 11:42:42 2019 +0100
@@ -32,9 +32,14 @@
 #  error HAS_ORTHANC_EXCEPTION must be set to 1
 #endif
 
+#if ORTHANC_ENABLE_PLUGINS != 1
+#  error ORTHANC_ENABLE_PLUGINS must be set to 1
+#endif
 
-#include <orthanc/OrthancCDatabasePlugin.h>
+
 #include <Core/OrthancException.h>
+#include <OrthancServer/Search/DatabaseConstraint.h>
+
 
 
 #define ORTHANC_PLUGINS_DATABASE_CATCH                            \
@@ -75,7 +80,9 @@
       AllowedAnswers_Attachment,
       AllowedAnswers_Change,
       AllowedAnswers_DicomTag,
-      AllowedAnswers_ExportedResource
+      AllowedAnswers_ExportedResource,
+      AllowedAnswers_MatchingResource,
+      AllowedAnswers_String
     };
 
     OrthancPluginContext*         context_;
@@ -243,6 +250,43 @@
 
       OrthancPluginDatabaseAnswerExportedResource(context_, database_, &exported);
     }
+
+
+#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
+    void AnswerMatchingResource(const std::string& resourceId)
+    {
+      if (allowedAnswers_ != AllowedAnswers_All &&
+          allowedAnswers_ != AllowedAnswers_MatchingResource)
+      {
+        throw std::runtime_error("Cannot answer with an exported resource in the current state");
+      }
+
+      OrthancPluginMatchingResource match;
+      match.resourceId = resourceId.c_str();
+      match.someInstanceId = NULL;
+
+      OrthancPluginDatabaseAnswerMatchingResource(context_, database_, &match);
+    }
+#endif
+
+
+#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
+    void AnswerMatchingResource(const std::string& resourceId,
+                                const std::string& someInstanceId)
+    {
+      if (allowedAnswers_ != AllowedAnswers_All &&
+          allowedAnswers_ != AllowedAnswers_MatchingResource)
+      {
+        throw std::runtime_error("Cannot answer with an exported resource in the current state");
+      }
+
+      OrthancPluginMatchingResource match;
+      match.resourceId = resourceId.c_str();
+      match.someInstanceId = someInstanceId.c_str();
+
+      OrthancPluginDatabaseAnswerMatchingResource(context_, database_, &match);
+    }
+#endif
   };
 
 
@@ -447,6 +491,49 @@
                                  OrthancPluginStorageArea* storageArea) = 0;
 
     virtual void ClearMainDicomTags(int64_t internalId) = 0;
+
+    virtual bool HasCreateInstance() const
+    {
+      return false;
+    }
+
+#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
+    virtual void LookupResources(const std::vector<Orthanc::DatabaseConstraint>& lookup,
+                                 OrthancPluginResourceType queryLevel,
+                                 uint32_t limit,
+                                 bool requestSomeInstance) = 0;
+#endif
+
+#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
+    virtual void CreateInstance(OrthancPluginCreateInstanceResult& result,
+                                const char* hashPatient,
+                                const char* hashStudy,
+                                const char* hashSeries,
+                                const char* hashInstance)
+    {
+      throw std::runtime_error("Not implemented");
+    }
+#endif
+
+
+#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
+    virtual void SetResourcesContent(
+      uint32_t countIdentifierTags,
+      const OrthancPluginResourcesContentTags* identifierTags,
+      uint32_t countMainDicomTags,
+      const OrthancPluginResourcesContentTags* mainDicomTags,
+      uint32_t countMetadata,
+      const OrthancPluginResourcesContentMetadata* metadata) = 0;
+#endif
+
+    
+    virtual void GetChildrenMetadata(std::list<std::string>& target,
+                                     int64_t resourceId,
+                                     int32_t metadata) = 0;
+
+    virtual int64_t GetLastChangeIndex() = 0;
+
+    virtual void TagMostRecentPatient(int64_t patientId) = 0;
   };
 
 
@@ -1383,6 +1470,7 @@
                                                      void* payload)
     {
       IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
       
       try
       {
@@ -1398,6 +1486,7 @@
                                                   OrthancPluginStorageArea* storageArea)
     {
       IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
       
       try
       {
@@ -1412,6 +1501,7 @@
                                                      int64_t internalId)
     {
       IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
       
       try
       {
@@ -1421,7 +1511,145 @@
       ORTHANC_PLUGINS_DATABASE_CATCH
     }
 
+
+#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
+    /* Use GetOutput().AnswerResource() */
+    static OrthancPluginErrorCode LookupResources(
+      OrthancPluginDatabaseContext* context,
+      void* payload,
+      uint32_t constraintsCount,
+      const OrthancPluginDatabaseConstraint* constraints,
+      OrthancPluginResourceType queryLevel,
+      uint32_t limit,
+      uint8_t requestSomeInstance)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_MatchingResource);
+
+      try
+      {
+        std::vector<Orthanc::DatabaseConstraint> lookup;
+        lookup.reserve(constraintsCount);
+
+        for (uint32_t i = 0; i < constraintsCount; i++)
+        {
+          lookup.push_back(Orthanc::DatabaseConstraint(constraints[i]));
+        }
+        
+        backend->LookupResources(lookup, queryLevel, limit, (requestSomeInstance != 0));
+        return OrthancPluginErrorCode_Success;
+      }
+      ORTHANC_PLUGINS_DATABASE_CATCH
+    }
+#endif
+
+
+#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
+    static OrthancPluginErrorCode CreateInstance(OrthancPluginCreateInstanceResult* output,
+                                                 void* payload,
+                                                 const char* hashPatient,
+                                                 const char* hashStudy,
+                                                 const char* hashSeries,
+                                                 const char* hashInstance)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
+
+      try
+      {
+        backend->CreateInstance(*output, hashPatient, hashStudy, hashSeries, hashInstance);
+        return OrthancPluginErrorCode_Success;
+      }
+      ORTHANC_PLUGINS_DATABASE_CATCH      
+    }
+#endif
+
+
+#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
+    static OrthancPluginErrorCode SetResourcesContent(
+      void* payload,
+      uint32_t countIdentifierTags,
+      const OrthancPluginResourcesContentTags* identifierTags,
+      uint32_t countMainDicomTags,
+      const OrthancPluginResourcesContentTags* mainDicomTags,
+      uint32_t countMetadata,
+      const OrthancPluginResourcesContentMetadata* metadata)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
+
+      try
+      {
+        backend->SetResourcesContent(countIdentifierTags, identifierTags,
+                                     countMainDicomTags, mainDicomTags,
+                                     countMetadata, metadata);
+        return OrthancPluginErrorCode_Success;
+      }
+      ORTHANC_PLUGINS_DATABASE_CATCH      
+    }
+#endif    
+
     
+    // New primitive since Orthanc 1.5.2
+    static OrthancPluginErrorCode GetChildrenMetadata(OrthancPluginDatabaseContext* context,
+                                                      void* payload,
+                                                      int64_t resourceId,
+                                                      int32_t metadata)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
+
+      try
+      {
+        std::list<std::string> values;
+        backend->GetChildrenMetadata(values, resourceId, metadata);
+
+        for (std::list<std::string>::const_iterator
+               it = values.begin(); it != values.end(); ++it)
+        {
+          OrthancPluginDatabaseAnswerString(backend->GetOutput().context_,
+                                            backend->GetOutput().database_,
+                                            it->c_str());
+        }
+
+        return OrthancPluginErrorCode_Success;
+      }
+      ORTHANC_PLUGINS_DATABASE_CATCH      
+    }
+
+
+    // New primitive since Orthanc 1.5.2
+    static OrthancPluginErrorCode GetLastChangeIndex(int64_t* result,
+                                                     void* payload)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
+
+      try
+      {
+        *result = backend->GetLastChangeIndex();
+        return OrthancPluginErrorCode_Success;
+      }
+      ORTHANC_PLUGINS_DATABASE_CATCH      
+    }
+
+
+    // New primitive since Orthanc 1.5.2
+    static OrthancPluginErrorCode TagMostRecentPatient(void* payload,
+                                                       int64_t patientId)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
+
+      try
+      {
+        backend->TagMostRecentPatient(patientId);
+        return OrthancPluginErrorCode_Success;
+      }
+      ORTHANC_PLUGINS_DATABASE_CATCH      
+    }
+   
+
   public:
     /**
      * Register a custom database back-end written in C++.
@@ -1468,7 +1696,7 @@
       params.logExportedResource = LogExportedResource;
       params.lookupAttachment = LookupAttachment;
       params.lookupGlobalProperty = LookupGlobalProperty;
-      params.lookupIdentifier = NULL;   // Unused starting with Orthanc 0.9.5 (db v6)
+      params.lookupIdentifier = NULL;    // Unused starting with Orthanc 0.9.5 (db v6)
       params.lookupIdentifier2 = NULL;   // Unused starting with Orthanc 0.9.5 (db v6)
       params.lookupMetadata = LookupMetadata;
       params.lookupParent = LookupParent;
@@ -1490,25 +1718,40 @@
       extensions.getDatabaseVersion = GetDatabaseVersion;
       extensions.upgradeDatabase = UpgradeDatabase;
       extensions.clearMainDicomTags = ClearMainDicomTags;
-      extensions.getAllInternalIds = GetAllInternalIds;    // New in Orthanc 0.9.5 (db v6)
-      extensions.lookupIdentifier3 = LookupIdentifier3;    // New in Orthanc 0.9.5 (db v6)
+      extensions.getAllInternalIds = GetAllInternalIds;     // New in Orthanc 0.9.5 (db v6)
+      extensions.lookupIdentifier3 = LookupIdentifier3;     // New in Orthanc 0.9.5 (db v6)
 
       bool performanceWarning = true;
 
 #if defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE)         // Macro introduced in Orthanc 1.3.1
 #  if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 4, 0)
       extensions.lookupIdentifierRange = LookupIdentifierRange;    // New in Orthanc 1.4.0
-      performanceWarning = false;
 #  endif
 #endif
 
+#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
+      // Optimizations brought by Orthanc 1.5.2
+      extensions.lookupResources = LookupResources;          // Fast lookup
+      extensions.setResourcesContent = SetResourcesContent;  // Fast setting tags/metadata
+      extensions.getChildrenMetadata = GetChildrenMetadata;
+      extensions.getLastChangeIndex = GetLastChangeIndex;
+      extensions.tagMostRecentPatient = TagMostRecentPatient;
+
+      if (backend.HasCreateInstance())
+      {
+        extensions.createInstance = CreateInstance;          // Fast creation of resources
+      }
+      
+      performanceWarning = false;
+#endif      
+
       if (performanceWarning)
       {
         char info[1024];
         sprintf(info, 
                 "Performance warning: The database index plugin was compiled "
                 "against an old version of the Orthanc SDK (%d.%d.%d): "
-                "Consider upgrading to version 1.4.0 of the Orthanc SDK",
+                "Consider upgrading to version 1.5.2 of the Orthanc SDK",
                 ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER,
                 ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER,
                 ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER);
--- a/MySQL/CMakeLists.txt	Thu Jan 17 11:42:24 2019 +0100
+++ b/MySQL/CMakeLists.txt	Thu Jan 17 11:42:42 2019 +0100
@@ -6,6 +6,7 @@
 if (ORTHANC_PLUGIN_VERSION STREQUAL "mainline")
   set(ORTHANC_FRAMEWORK_VERSION "mainline")
   set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "hg")
+  set(ORTHANC_FRAMEWORK_BRANCH "db-changes")  # TODO - Remove this once out of "db-changes" branch
 else()
   set(ORTHANC_FRAMEWORK_VERSION "1.4.0")
   set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "web")
@@ -51,7 +52,8 @@
 
 
 EmbedResources(
-  MYSQL_PREPARE_INDEX ${CMAKE_SOURCE_DIR}/Plugins/PrepareIndex.sql
+  MYSQL_PREPARE_INDEX          ${CMAKE_SOURCE_DIR}/Plugins/PrepareIndex.sql
+  MYSQL_GET_LAST_CHANGE_INDEX  ${CMAKE_SOURCE_DIR}/Plugins/GetLastChangeIndex.sql
   )
 
 add_library(OrthancMySQLIndex SHARED
@@ -78,7 +80,6 @@
 
 add_definitions(
   -DORTHANC_PLUGIN_VERSION="${ORTHANC_PLUGIN_VERSION}"
-  -DHAS_ORTHANC_EXCEPTION=1
   )
 
 set_target_properties(OrthancMySQLStorage PROPERTIES 
--- a/MySQL/NEWS	Thu Jan 17 11:42:24 2019 +0100
+++ b/MySQL/NEWS	Thu Jan 17 11:42:42 2019 +0100
@@ -1,6 +1,7 @@
 Pending changes in the mainline
 ===============================
 
+* Database optimizations by implementing new primitives of Orthanc SDK 1.5.2
 * Characters "$" and "_" are allowed in MySQL database identifiers
 * Fix serialization of jobs if many of them
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MySQL/Plugins/GetLastChangeIndex.sql	Thu Jan 17 11:42:42 2019 +0100
@@ -0,0 +1,16 @@
+CREATE TABLE GlobalIntegers(
+       property INTEGER PRIMARY KEY,
+       value BIGINT
+       );
+
+
+INSERT INTO GlobalIntegers
+SELECT 0, COALESCE(MAX(seq), 0) FROM Changes;
+
+
+CREATE TRIGGER ChangeAdded
+AFTER INSERT ON Changes
+FOR EACH ROW
+BEGIN
+  UPDATE GlobalIntegers SET value = new.seq WHERE property = 0@
+END;
--- a/MySQL/Plugins/MySQLIndex.cpp	Thu Jan 17 11:42:24 2019 +0100
+++ b/MySQL/Plugins/MySQLIndex.cpp	Thu Jan 17 11:42:42 2019 +0100
@@ -128,7 +128,19 @@
         SetGlobalIntegerProperty(*db, t, Orthanc::GlobalProperty_DatabasePatchLevel, revision);
       }
 
-      if (revision != 2)
+      if (revision == 2)
+      {
+        std::string query;
+
+        Orthanc::EmbeddedResources::GetFileResource
+          (query, Orthanc::EmbeddedResources::MYSQL_GET_LAST_CHANGE_INDEX);
+        db->Execute(query, true);
+        
+        revision = 3;
+        SetGlobalIntegerProperty(*db, t, Orthanc::GlobalProperty_DatabasePatchLevel, revision);
+      }
+
+      if (revision != 3)
       {
         LOG(ERROR) << "MySQL plugin is incompatible with database schema revision: " << revision;
         throw Orthanc::OrthancException(Orthanc::ErrorCode_Database);        
@@ -261,4 +273,17 @@
 
     SignalDeletedFiles();
   }
+
+  
+  int64_t MySQLIndex::GetLastChangeIndex()
+  {
+    DatabaseManager::CachedStatement statement(
+      STATEMENT_FROM_HERE, GetManager(),
+      "SELECT value FROM GlobalIntegers WHERE property = 0");
+    
+    statement.SetReadOnly(true);
+    statement.Execute();
+
+    return ReadInteger64(statement, 0);
+  }
 }
--- a/MySQL/Plugins/MySQLIndex.h	Thu Jan 17 11:42:24 2019 +0100
+++ b/MySQL/Plugins/MySQLIndex.h	Thu Jan 17 11:42:42 2019 +0100
@@ -74,5 +74,7 @@
                                    OrthancPluginResourceType type);
 
     virtual void DeleteResource(int64_t id);
+
+    virtual int64_t GetLastChangeIndex();
   };
 }
--- a/PostgreSQL/CMakeLists.txt	Thu Jan 17 11:42:24 2019 +0100
+++ b/PostgreSQL/CMakeLists.txt	Thu Jan 17 11:42:42 2019 +0100
@@ -6,6 +6,7 @@
 if (ORTHANC_PLUGIN_VERSION STREQUAL "mainline")
   set(ORTHANC_FRAMEWORK_VERSION "mainline")
   set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "hg")
+  set(ORTHANC_FRAMEWORK_BRANCH "db-changes")  # TODO - Remove this once out of "db-changes" branch
 else()
   set(ORTHANC_FRAMEWORK_VERSION "1.4.0")
   set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "web")
@@ -51,7 +52,11 @@
 
 
 EmbedResources(
-  POSTGRESQL_PREPARE_INDEX ${CMAKE_SOURCE_DIR}/Plugins/PrepareIndex.sql
+  POSTGRESQL_PREPARE_INDEX          ${CMAKE_SOURCE_DIR}/Plugins/PrepareIndex.sql
+  POSTGRESQL_CREATE_INSTANCE        ${CMAKE_SOURCE_DIR}/Plugins/CreateInstance.sql
+  POSTGRESQL_FAST_TOTAL_SIZE        ${CMAKE_SOURCE_DIR}/Plugins/FastTotalSize.sql
+  POSTGRESQL_FAST_COUNT_RESOURCES   ${CMAKE_SOURCE_DIR}/Plugins/FastCountResources.sql
+  POSTGRESQL_GET_LAST_CHANGE_INDEX  ${CMAKE_SOURCE_DIR}/Plugins/GetLastChangeIndex.sql
   )
 
 add_library(OrthancPostgreSQLIndex SHARED
@@ -78,7 +83,6 @@
 
 add_definitions(
   -DORTHANC_PLUGIN_VERSION="${ORTHANC_PLUGIN_VERSION}"
-  -DHAS_ORTHANC_EXCEPTION=1
   )
 
 set_target_properties(OrthancPostgreSQLStorage PROPERTIES 
--- a/PostgreSQL/NEWS	Thu Jan 17 11:42:24 2019 +0100
+++ b/PostgreSQL/NEWS	Thu Jan 17 11:42:42 2019 +0100
@@ -2,6 +2,7 @@
 ===============================
 
 * New configuration option: "EnableSsl"
+* Database optimizations by implementing new primitives of Orthanc SDK 1.5.2
 * Fix issue 105 (Unable to connect to PostgreSQL database using SSL)
 * Fix Debian issue #906771 (Uncaught exception prevents db intialization
   (likely related to pg_trgm))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/PostgreSQL/Plugins/CreateInstance.sql	Thu Jan 17 11:42:42 2019 +0100
@@ -0,0 +1,85 @@
+CREATE FUNCTION CreateInstance(
+  IN patient TEXT,
+  IN study TEXT,
+  IN series TEXT,
+  IN instance TEXT,
+  OUT isNewPatient BIGINT,
+  OUT isNewStudy BIGINT,
+  OUT isNewSeries BIGINT,
+  OUT isNewInstance BIGINT,
+  OUT patientKey BIGINT,
+  OUT studyKey BIGINT,
+  OUT seriesKey BIGINT,
+  OUT instanceKey BIGINT) AS $body$
+
+DECLARE
+  patientSeq BIGINT;
+  countRecycling BIGINT;
+
+BEGIN
+  SELECT internalId FROM Resources INTO instanceKey WHERE publicId = instance AND resourceType = 3;
+
+  IF NOT (instanceKey IS NULL) THEN
+    -- This instance already exists, stop here
+    isNewInstance := 0;
+  ELSE
+    SELECT internalId FROM Resources INTO patientKey WHERE publicId = patient AND resourceType = 0;
+    SELECT internalId FROM Resources INTO studyKey WHERE publicId = study AND resourceType = 1;
+    SELECT internalId FROM Resources INTO seriesKey WHERE publicId = series AND resourceType = 2;
+
+    IF patientKey IS NULL THEN
+      -- Must create a new patient
+      ASSERT studyKey IS NULL;
+      ASSERT seriesKey IS NULL;
+      ASSERT instanceKey IS NULL;
+      INSERT INTO Resources VALUES (DEFAULT, 0, patient, NULL) RETURNING internalId INTO patientKey;
+      isNewPatient := 1;
+    ELSE
+      isNewPatient := 0;
+    END IF;
+  
+    ASSERT NOT patientKey IS NULL;
+
+    IF studyKey IS NULL THEN
+      -- Must create a new study
+      ASSERT seriesKey IS NULL;
+      ASSERT instanceKey IS NULL;
+      INSERT INTO Resources VALUES (DEFAULT, 1, study, patientKey) RETURNING internalId INTO studyKey;
+      isNewStudy := 1;
+    ELSE
+      isNewStudy := 0;
+    END IF;
+
+    ASSERT NOT studyKey IS NULL;
+    
+    IF seriesKey IS NULL THEN
+      -- Must create a new series
+      ASSERT instanceKey IS NULL;
+      INSERT INTO Resources VALUES (DEFAULT, 2, series, studyKey) RETURNING internalId INTO seriesKey;
+      isNewSeries := 1;
+    ELSE
+      isNewSeries := 0;
+    END IF;
+  
+    ASSERT NOT seriesKey IS NULL;
+    ASSERT instanceKey IS NULL;
+
+    INSERT INTO Resources VALUES (DEFAULT, 3, instance, seriesKey) RETURNING internalId INTO instanceKey;
+    isNewInstance := 1;
+
+    -- Move the patient to the end of the recycling order
+    SELECT seq FROM PatientRecyclingOrder WHERE patientId = patientKey INTO patientSeq;
+
+    IF NOT (patientSeq IS NULL) THEN
+       -- The patient is not protected
+       SELECT COUNT(*) FROM (SELECT * FROM PatientRecyclingOrder WHERE seq >= patientSeq LIMIT 2) AS tmp INTO countRecycling;
+       IF countRecycling = 2 THEN
+          -- The patient was not at the end of the recycling order
+          DELETE FROM PatientRecyclingOrder WHERE seq = patientSeq;
+          INSERT INTO PatientRecyclingOrder VALUES(DEFAULT, patientKey);
+       END IF;
+    END IF;
+  END IF;  
+END;
+
+$body$ LANGUAGE plpgsql;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/PostgreSQL/Plugins/FastCountResources.sql	Thu Jan 17 11:42:42 2019 +0100
@@ -0,0 +1,33 @@
+-- https://wiki.postgresql.org/wiki/Count_estimate
+
+INSERT INTO GlobalIntegers
+SELECT 2, CAST(COALESCE(COUNT(*), 0) AS BIGINT) FROM Resources WHERE resourceType = 0;  -- Count patients
+
+INSERT INTO GlobalIntegers
+SELECT 3, CAST(COALESCE(COUNT(*), 0) AS BIGINT) FROM Resources WHERE resourceType = 1;  -- Count studies
+
+INSERT INTO GlobalIntegers
+SELECT 4, CAST(COALESCE(COUNT(*), 0) AS BIGINT) FROM Resources WHERE resourceType = 2;  -- Count series
+
+INSERT INTO GlobalIntegers
+SELECT 5, CAST(COALESCE(COUNT(*), 0) AS BIGINT) FROM Resources WHERE resourceType = 3;  -- Count instances
+
+
+CREATE OR REPLACE FUNCTION CountResourcesTrackerFunc()
+RETURNS TRIGGER AS $$
+BEGIN
+  IF TG_OP = 'INSERT' THEN
+    UPDATE GlobalIntegers SET value = value + 1 WHERE key = new.resourceType + 2;
+    RETURN new;
+  ELSIF TG_OP = 'DELETE' THEN
+    UPDATE GlobalIntegers SET value = value - 1 WHERE key = old.resourceType + 2;
+    RETURN old;
+  END IF;
+END;
+$$ LANGUAGE plpgsql;
+
+
+CREATE TRIGGER CountResourcesTracker
+AFTER INSERT OR DELETE ON Resources
+FOR EACH ROW
+EXECUTE PROCEDURE CountResourcesTrackerFunc();
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/PostgreSQL/Plugins/FastTotalSize.sql	Thu Jan 17 11:42:42 2019 +0100
@@ -0,0 +1,41 @@
+CREATE TABLE GlobalIntegers(
+       key INTEGER PRIMARY KEY,
+       value BIGINT);
+
+INSERT INTO GlobalIntegers
+SELECT 0, CAST(COALESCE(SUM(compressedSize), 0) AS BIGINT) FROM AttachedFiles;
+
+INSERT INTO GlobalIntegers
+SELECT 1, CAST(COALESCE(SUM(uncompressedSize), 0) AS BIGINT) FROM AttachedFiles;
+
+
+
+CREATE FUNCTION AttachedFileIncrementSizeFunc() 
+RETURNS TRIGGER AS $body$
+BEGIN
+  UPDATE GlobalIntegers SET value = value + new.compressedSize WHERE key = 0;
+  UPDATE GlobalIntegers SET value = value + new.uncompressedSize WHERE key = 1;
+  RETURN NULL;
+END;
+$body$ LANGUAGE plpgsql;
+
+CREATE FUNCTION AttachedFileDecrementSizeFunc() 
+RETURNS TRIGGER AS $body$
+BEGIN
+  UPDATE GlobalIntegers SET value = value - old.compressedSize WHERE key = 0;
+  UPDATE GlobalIntegers SET value = value - old.uncompressedSize WHERE key = 1;
+  RETURN NULL;
+END;
+$body$ LANGUAGE plpgsql;
+
+
+
+CREATE TRIGGER AttachedFileIncrementSize
+AFTER INSERT ON AttachedFiles
+FOR EACH ROW
+EXECUTE PROCEDURE AttachedFileIncrementSizeFunc();
+
+CREATE TRIGGER AttachedFileDecrementSize
+AFTER DELETE ON AttachedFiles
+FOR EACH ROW
+EXECUTE PROCEDURE AttachedFileDecrementSizeFunc();
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/PostgreSQL/Plugins/GetLastChangeIndex.sql	Thu Jan 17 11:42:42 2019 +0100
@@ -0,0 +1,27 @@
+-- In PostgreSQL, the most straightforward query would be to run:
+
+--   SELECT currval(pg_get_serial_sequence('Changes', 'seq'))".
+
+-- Unfortunately, this raises the error message "currval of sequence
+-- "changes_seq_seq" is not yet defined in this session" if no change
+-- has been inserted before the SELECT. We thus track the sequence
+-- index with a trigger.
+-- http://www.neilconway.org/docs/sequences/
+
+INSERT INTO GlobalIntegers
+SELECT 6, CAST(COALESCE(MAX(seq), 0) AS BIGINT) FROM Changes;
+
+
+CREATE FUNCTION InsertedChangeFunc() 
+RETURNS TRIGGER AS $body$
+BEGIN
+  UPDATE GlobalIntegers SET value = new.seq WHERE key = 6;
+  RETURN NULL;
+END;
+$body$ LANGUAGE plpgsql;
+
+
+CREATE TRIGGER InsertedChange
+AFTER INSERT ON Changes
+FOR EACH ROW
+EXECUTE PROCEDURE InsertedChangeFunc();
--- a/PostgreSQL/Plugins/PostgreSQLIndex.cpp	Thu Jan 17 11:42:24 2019 +0100
+++ b/PostgreSQL/Plugins/PostgreSQLIndex.cpp	Thu Jan 17 11:42:42 2019 +0100
@@ -35,6 +35,9 @@
 {
   // Some aliases for internal properties
   static const GlobalProperty GlobalProperty_HasTrigramIndex = GlobalProperty_DatabaseInternal0;
+  static const GlobalProperty GlobalProperty_HasCreateInstance = GlobalProperty_DatabaseInternal1;
+  static const GlobalProperty GlobalProperty_HasFastCountResources = GlobalProperty_DatabaseInternal2;
+  static const GlobalProperty GlobalProperty_GetLastChangeIndex = GlobalProperty_DatabaseInternal3;
 }
 
 
@@ -126,7 +129,8 @@
       PostgreSQLTransaction t(*db);
 
       int hasTrigram = 0;
-      if (!LookupGlobalIntegerProperty(hasTrigram, *db, t, Orthanc::GlobalProperty_HasTrigramIndex) ||
+      if (!LookupGlobalIntegerProperty(hasTrigram, *db, t,
+                                       Orthanc::GlobalProperty_HasTrigramIndex) ||
           hasTrigram != 1)
       {
         /**
@@ -162,6 +166,89 @@
                        << "PostgreSQL server, e.g. on Debian: sudo apt install postgresql-contrib";
         }
       }
+      else
+      {
+        t.Commit();
+      }
+    }
+
+    {
+      PostgreSQLTransaction t(*db);
+
+      int property = 0;
+      if (!LookupGlobalIntegerProperty(property, *db, t,
+                                       Orthanc::GlobalProperty_HasCreateInstance) ||
+          property != 2)
+      {
+        LOG(INFO) << "Installing the CreateInstance extension";
+
+        if (property == 1)
+        {
+          // Drop older, experimental versions of this extension
+          db->Execute("DROP FUNCTION CreateInstance("
+                      "IN patient TEXT, IN study TEXT, IN series TEXT, in instance TEXT)");
+        }
+        
+        std::string query;
+        Orthanc::EmbeddedResources::GetFileResource
+          (query, Orthanc::EmbeddedResources::POSTGRESQL_CREATE_INSTANCE);
+        db->Execute(query);
+
+        SetGlobalIntegerProperty(*db, t, Orthanc::GlobalProperty_HasCreateInstance, 2);
+      }
+
+      
+      if (!LookupGlobalIntegerProperty(property, *db, t,
+                                       Orthanc::GlobalProperty_GetTotalSizeIsFast) ||
+          property != 1)
+      {
+        LOG(INFO) << "Installing the FastTotalSize extension";
+
+        std::string query;
+        Orthanc::EmbeddedResources::GetFileResource
+          (query, Orthanc::EmbeddedResources::POSTGRESQL_FAST_TOTAL_SIZE);
+        db->Execute(query);
+
+        SetGlobalIntegerProperty(*db, t, Orthanc::GlobalProperty_GetTotalSizeIsFast, 1);
+      }
+
+
+      // Installing this extension requires the "GlobalIntegers" table
+      // created by the "FastTotalSize" extension
+      property = 0;
+      if (!LookupGlobalIntegerProperty(property, *db, t,
+                                       Orthanc::GlobalProperty_HasFastCountResources) ||
+          property != 1)
+      {
+        LOG(INFO) << "Installing the FastCountResources extension";
+
+        std::string query;
+        Orthanc::EmbeddedResources::GetFileResource
+          (query, Orthanc::EmbeddedResources::POSTGRESQL_FAST_COUNT_RESOURCES);
+        db->Execute(query);
+
+        SetGlobalIntegerProperty(*db, t, Orthanc::GlobalProperty_HasFastCountResources, 1);
+      }
+
+
+      // Installing this extension requires the "GlobalIntegers" table
+      // created by the "GetLastChangeIndex" extension
+      property = 0;
+      if (!LookupGlobalIntegerProperty(property, *db, t,
+                                       Orthanc::GlobalProperty_GetLastChangeIndex) ||
+          property != 1)
+      {
+        LOG(INFO) << "Installing the GetLastChangeIndex extension";
+
+        std::string query;
+        Orthanc::EmbeddedResources::GetFileResource
+          (query, Orthanc::EmbeddedResources::POSTGRESQL_GET_LAST_CHANGE_INDEX);
+        db->Execute(query);
+
+        SetGlobalIntegerProperty(*db, t, Orthanc::GlobalProperty_GetLastChangeIndex, 1);
+      }
+
+      t.Commit();
     }
 
     return db.release();
@@ -195,4 +282,152 @@
 
     return ReadInteger64(statement, 0);
   }
+
+
+  uint64_t PostgreSQLIndex::GetTotalCompressedSize()
+  {
+    // Fast version if extension "./FastTotalSize.sql" is installed
+    uint64_t result;
+
+    {
+      DatabaseManager::CachedStatement statement(
+        STATEMENT_FROM_HERE, GetManager(),
+        "SELECT value FROM GlobalIntegers WHERE key = 0");
+
+      statement.SetReadOnly(true);
+      statement.Execute();
+
+      result = static_cast<uint64_t>(ReadInteger64(statement, 0));
+    }
+    
+    assert(result == IndexBackend::GetTotalCompressedSize());
+    return result;
+  }
+
+  
+  uint64_t PostgreSQLIndex::GetTotalUncompressedSize()
+  {
+    // Fast version if extension "./FastTotalSize.sql" is installed
+    uint64_t result;
+
+    {
+      DatabaseManager::CachedStatement statement(
+        STATEMENT_FROM_HERE, GetManager(),
+        "SELECT value FROM GlobalIntegers WHERE key = 1");
+
+      statement.SetReadOnly(true);
+      statement.Execute();
+
+      result = static_cast<uint64_t>(ReadInteger64(statement, 0));
+    }
+    
+    assert(result == IndexBackend::GetTotalUncompressedSize());
+    return result;
+  }
+
+
+#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
+  void PostgreSQLIndex::CreateInstance(OrthancPluginCreateInstanceResult& result,
+                                       const char* hashPatient,
+                                       const char* hashStudy,
+                                       const char* hashSeries,
+                                       const char* hashInstance)
+  {
+    DatabaseManager::CachedStatement statement(
+      STATEMENT_FROM_HERE, GetManager(),
+      "SELECT * FROM CreateInstance(${patient}, ${study}, ${series}, ${instance})");
+
+    statement.SetParameterType("patient", ValueType_Utf8String);
+    statement.SetParameterType("study", ValueType_Utf8String);
+    statement.SetParameterType("series", ValueType_Utf8String);
+    statement.SetParameterType("instance", ValueType_Utf8String);
+
+    Dictionary args;
+    args.SetUtf8Value("patient", hashPatient);
+    args.SetUtf8Value("study", hashStudy);
+    args.SetUtf8Value("series", hashSeries);
+    args.SetUtf8Value("instance", hashInstance);
+    
+    statement.Execute(args);
+
+    if (statement.IsDone() ||
+        statement.GetResultFieldsCount() != 8)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_Database);
+    }
+
+    for (size_t i = 0; i < 8; i++)
+    {
+      statement.SetResultFieldType(i, ValueType_Integer64);
+    }
+
+    result.isNewInstance = (ReadInteger64(statement, 3) == 1);
+    result.instanceId = ReadInteger64(statement, 7);
+
+    if (result.isNewInstance)
+    {
+      result.isNewPatient = (ReadInteger64(statement, 0) == 1);
+      result.isNewStudy = (ReadInteger64(statement, 1) == 1);
+      result.isNewSeries = (ReadInteger64(statement, 2) == 1);
+      result.patientId = ReadInteger64(statement, 4);
+      result.studyId = ReadInteger64(statement, 5);
+      result.seriesId = ReadInteger64(statement, 6);
+    }
+  }
+#endif
+
+
+  uint64_t PostgreSQLIndex::GetResourceCount(OrthancPluginResourceType resourceType)
+  {
+    // Optimized version thanks to the "FastCountResources.sql" extension
+
+    assert(OrthancPluginResourceType_Patient == 0 &&
+           OrthancPluginResourceType_Study == 1 &&
+           OrthancPluginResourceType_Series == 2 &&
+           OrthancPluginResourceType_Instance == 3);
+
+    uint64_t result;
+    
+    {
+      DatabaseManager::CachedStatement statement(
+        STATEMENT_FROM_HERE, GetManager(),
+        "SELECT value FROM GlobalIntegers WHERE key = ${key}");
+
+      statement.SetParameterType("key", ValueType_Integer64);
+
+      Dictionary args;
+
+      // For an explanation of the "+ 2" below, check out "FastCountResources.sql"
+      args.SetIntegerValue("key", static_cast<int>(resourceType + 2));
+
+      statement.SetReadOnly(true);
+      statement.Execute(args);
+
+      result = static_cast<uint64_t>(ReadInteger64(statement, 0));
+    }
+      
+    assert(result == IndexBackend::GetResourceCount(resourceType));
+    return result;
+  }
+
+
+  int64_t PostgreSQLIndex::GetLastChangeIndex()
+  {
+    DatabaseManager::CachedStatement statement(
+      STATEMENT_FROM_HERE, GetManager(),
+      "SELECT value FROM GlobalIntegers WHERE key = 6");
+
+    statement.SetReadOnly(true);
+    statement.Execute();
+
+    return ReadInteger64(statement, 0);
+  }
+
+
+  void PostgreSQLIndex::TagMostRecentPatient(int64_t patient)
+  {
+    // This behavior is implemented in "CreateInstance()", and no
+    // backward compatibility is necessary
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_Database);
+  }
 }
--- a/PostgreSQL/Plugins/PostgreSQLIndex.h	Thu Jan 17 11:42:24 2019 +0100
+++ b/PostgreSQL/Plugins/PostgreSQLIndex.h	Thu Jan 17 11:42:42 2019 +0100
@@ -71,6 +71,32 @@
     }
 
     virtual int64_t CreateResource(const char* publicId,
-                                   OrthancPluginResourceType type);
+                                   OrthancPluginResourceType type)
+      ORTHANC_OVERRIDE;
+
+    virtual uint64_t GetTotalCompressedSize() ORTHANC_OVERRIDE;
+
+    virtual uint64_t GetTotalUncompressedSize() ORTHANC_OVERRIDE;
+    
+    virtual bool HasCreateInstance() const  ORTHANC_OVERRIDE
+    {
+      return true;
+    }
+
+#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
+    virtual void CreateInstance(OrthancPluginCreateInstanceResult& result,
+                                const char* hashPatient,
+                                const char* hashStudy,
+                                const char* hashSeries,
+                                const char* hashInstance)
+      ORTHANC_OVERRIDE;
+#endif
+
+    virtual uint64_t GetResourceCount(OrthancPluginResourceType resourceType)
+      ORTHANC_OVERRIDE;
+
+    virtual int64_t GetLastChangeIndex() ORTHANC_OVERRIDE;
+
+    virtual void TagMostRecentPatient(int64_t patient) ORTHANC_OVERRIDE;
   };
 }
--- a/PostgreSQL/UnitTests/PostgreSQLTests.cpp	Thu Jan 17 11:42:24 2019 +0100
+++ b/PostgreSQL/UnitTests/PostgreSQLTests.cpp	Thu Jan 17 11:42:42 2019 +0100
@@ -34,6 +34,7 @@
 #  undef S_IXOTH
 #endif
 
+#include "../Plugins/PostgreSQLIndex.h"
 #include "../Plugins/PostgreSQLStorageArea.h"
 #include "../../Framework/PostgreSQL/PostgreSQLTransaction.h"
 #include "../../Framework/PostgreSQL/PostgreSQLResult.h"
@@ -437,3 +438,78 @@
   ASSERT_TRUE(db->DoesTableExist("test2"));
 }
 
+
+#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
+TEST(PostgreSQLIndex, CreateInstance)
+{
+  OrthancDatabases::PostgreSQLIndex db(globalParameters_);
+  db.SetClearAll(true);
+  db.Open();
+
+  std::string s;
+  ASSERT_TRUE(db.LookupGlobalProperty(s, Orthanc::GlobalProperty_DatabaseInternal1));
+  ASSERT_EQ("1", s);
+
+  OrthancPluginCreateInstanceResult r1, r2;
+  
+  memset(&r1, 0, sizeof(r1));
+  db.CreateInstance(r1, "a", "b", "c", "d");
+  ASSERT_TRUE(r1.isNewInstance);
+  ASSERT_TRUE(r1.isNewSeries);
+  ASSERT_TRUE(r1.isNewStudy);
+  ASSERT_TRUE(r1.isNewPatient);
+
+  memset(&r2, 0, sizeof(r2));
+  db.CreateInstance(r2, "a", "b", "c", "d");
+  ASSERT_FALSE(r2.isNewInstance);
+  ASSERT_EQ(r1.instanceId, r2.instanceId);
+
+  // Breaking the hierarchy
+  memset(&r2, 0, sizeof(r2));
+  ASSERT_THROW(db.CreateInstance(r2, "a", "e", "c", "f"), Orthanc::OrthancException);
+
+  memset(&r2, 0, sizeof(r2));
+  db.CreateInstance(r2, "a", "b", "c", "e");
+  ASSERT_TRUE(r2.isNewInstance);
+  ASSERT_FALSE(r2.isNewSeries);
+  ASSERT_FALSE(r2.isNewStudy);
+  ASSERT_FALSE(r2.isNewPatient);
+  ASSERT_EQ(r1.patientId, r2.patientId);
+  ASSERT_EQ(r1.studyId, r2.studyId);
+  ASSERT_EQ(r1.seriesId, r2.seriesId);
+  ASSERT_NE(r1.instanceId, r2.instanceId);
+
+  memset(&r2, 0, sizeof(r2));
+  db.CreateInstance(r2, "a", "b", "f", "g");
+  ASSERT_TRUE(r2.isNewInstance);
+  ASSERT_TRUE(r2.isNewSeries);
+  ASSERT_FALSE(r2.isNewStudy);
+  ASSERT_FALSE(r2.isNewPatient);
+  ASSERT_EQ(r1.patientId, r2.patientId);
+  ASSERT_EQ(r1.studyId, r2.studyId);
+  ASSERT_NE(r1.seriesId, r2.seriesId);
+  ASSERT_NE(r1.instanceId, r2.instanceId);
+
+  memset(&r2, 0, sizeof(r2));
+  db.CreateInstance(r2, "a", "h", "i", "j");
+  ASSERT_TRUE(r2.isNewInstance);
+  ASSERT_TRUE(r2.isNewSeries);
+  ASSERT_TRUE(r2.isNewStudy);
+  ASSERT_FALSE(r2.isNewPatient);
+  ASSERT_EQ(r1.patientId, r2.patientId);
+  ASSERT_NE(r1.studyId, r2.studyId);
+  ASSERT_NE(r1.seriesId, r2.seriesId);
+  ASSERT_NE(r1.instanceId, r2.instanceId);
+
+  memset(&r2, 0, sizeof(r2));
+  db.CreateInstance(r2, "k", "l", "m", "n");
+  ASSERT_TRUE(r2.isNewInstance);
+  ASSERT_TRUE(r2.isNewSeries);
+  ASSERT_TRUE(r2.isNewStudy);
+  ASSERT_TRUE(r2.isNewPatient);
+  ASSERT_NE(r1.patientId, r2.patientId);
+  ASSERT_NE(r1.studyId, r2.studyId);
+  ASSERT_NE(r1.seriesId, r2.seriesId);
+  ASSERT_NE(r1.instanceId, r2.instanceId);
+}
+#endif
--- a/Resources/CMake/DatabasesPluginConfiguration.cmake	Thu Jan 17 11:42:24 2019 +0100
+++ b/Resources/CMake/DatabasesPluginConfiguration.cmake	Thu Jan 17 11:42:42 2019 +0100
@@ -40,10 +40,20 @@
 endif()
 
 
+add_definitions(
+  -DHAS_ORTHANC_EXCEPTION=1
+  -DORTHANC_ENABLE_PLUGINS=1
+  )
+
+
 list(APPEND DATABASES_SOURCES
   ${ORTHANC_CORE_SOURCES}
   ${ORTHANC_DATABASES_ROOT}/Framework/Plugins/GlobalProperties.cpp
   ${ORTHANC_DATABASES_ROOT}/Framework/Plugins/IndexBackend.cpp
   ${ORTHANC_DATABASES_ROOT}/Framework/Plugins/StorageBackend.cpp
   ${ORTHANC_ROOT}/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp
+
+  # New from "db-changes"
+  ${ORTHANC_ROOT}/OrthancServer/Search/DatabaseConstraint.cpp
+  ${ORTHANC_ROOT}/OrthancServer/Search/ISqlLookupFormatter.cpp
   )
--- a/SQLite/CMakeLists.txt	Thu Jan 17 11:42:24 2019 +0100
+++ b/SQLite/CMakeLists.txt	Thu Jan 17 11:42:42 2019 +0100
@@ -6,6 +6,7 @@
 if (ORTHANC_PLUGIN_VERSION STREQUAL "mainline")
   set(ORTHANC_FRAMEWORK_VERSION "mainline")
   set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "hg")
+  set(ORTHANC_FRAMEWORK_BRANCH "db-changes")  # TODO - Remove this once out of "db-changes" branch
 else()
   set(ORTHANC_FRAMEWORK_VERSION "1.4.0")
   set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "web")
@@ -34,7 +35,6 @@
 
 add_definitions(
   -DORTHANC_PLUGIN_VERSION="${ORTHANC_PLUGIN_VERSION}"
-  -DHAS_ORTHANC_EXCEPTION=1
   )
 
 #set_target_properties(OrthancSQLiteStorage PROPERTIES 
--- a/SQLite/Plugins/SQLiteIndex.cpp	Thu Jan 17 11:42:24 2019 +0100
+++ b/SQLite/Plugins/SQLiteIndex.cpp	Thu Jan 17 11:42:42 2019 +0100
@@ -21,6 +21,7 @@
 
 #include "SQLiteIndex.h"
 
+#include "../../Framework/Common/Integer64Value.h"
 #include "../../Framework/Plugins/GlobalProperties.h"
 #include "../../Framework/SQLite/SQLiteDatabase.h"
 #include "../../Framework/SQLite/SQLiteTransaction.h"
@@ -173,4 +174,35 @@
 
     return dynamic_cast<SQLiteDatabase&>(statement.GetDatabase()).GetLastInsertRowId();
   }
+
+
+  int64_t SQLiteIndex::GetLastChangeIndex()
+  {
+    DatabaseManager::CachedStatement statement(
+      STATEMENT_FROM_HERE, GetManager(),
+      "SELECT seq FROM sqlite_sequence WHERE name='Changes'");
+
+    statement.SetReadOnly(true);
+    statement.Execute();
+    
+    if (statement.IsDone())
+    {
+      // No change has been recorded so far in the database
+      return 0;
+    }
+    else
+    {
+      const IValue& value = statement.GetResultField(0);
+      
+      switch (value.GetType())
+      {
+        case ValueType_Integer64:
+          return dynamic_cast<const Integer64Value&>(value).GetValue();
+          
+        default:
+          //LOG(ERROR) << value.Format();
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+    }
+  }
 }
--- a/SQLite/Plugins/SQLiteIndex.h	Thu Jan 17 11:42:24 2019 +0100
+++ b/SQLite/Plugins/SQLiteIndex.h	Thu Jan 17 11:42:42 2019 +0100
@@ -73,5 +73,8 @@
 
     virtual int64_t CreateResource(const char* publicId,
                                    OrthancPluginResourceType type);
+
+    // New primitive since Orthanc 1.5.2
+    virtual int64_t GetLastChangeIndex();
   };
 }