changeset 237:35598014f140

refactoring to remove GlobalProperties.cpp
author Sebastien Jodogne <s.jodogne@gmail.com>
date Thu, 08 Apr 2021 19:09:04 +0200
parents d1d2edbbe6fb
children f033cc039264
files Framework/Common/DatabaseManager.h Framework/Common/ITransaction.h Framework/MySQL/MySQLDatabase.cpp Framework/MySQL/MySQLDatabase.h Framework/MySQL/MySQLTransaction.cpp Framework/MySQL/MySQLTransaction.h Framework/Plugins/DatabaseBackendAdapterV3.cpp Framework/Plugins/GlobalProperties.h Framework/Plugins/IDatabaseBackend.h Framework/Plugins/IndexBackend.cpp Framework/Plugins/IndexBackend.h Framework/PostgreSQL/PostgreSQLDatabase.cpp Framework/PostgreSQL/PostgreSQLDatabase.h Framework/PostgreSQL/PostgreSQLStatement.cpp Framework/PostgreSQL/PostgreSQLTransaction.cpp Framework/PostgreSQL/PostgreSQLTransaction.h Framework/SQLite/SQLiteDatabase.cpp Framework/SQLite/SQLiteDatabase.h Framework/SQLite/SQLiteTransaction.cpp Framework/SQLite/SQLiteTransaction.h MySQL/Plugins/MySQLIndex.cpp MySQL/Plugins/MySQLIndex.h MySQL/Plugins/MySQLStorageArea.cpp PostgreSQL/Plugins/PostgreSQLIndex.cpp PostgreSQL/Plugins/PostgreSQLIndex.h PostgreSQL/Plugins/PostgreSQLStorageArea.cpp PostgreSQL/UnitTests/PostgreSQLTests.cpp Resources/CMake/DatabasesPluginConfiguration.cmake SQLite/Plugins/SQLiteIndex.cpp SQLite/Plugins/SQLiteIndex.h SQLite/UnitTests/UnitTestsMain.cpp
diffstat 31 files changed, 450 insertions(+), 210 deletions(-) [+]
line wrap: on
line diff
--- a/Framework/Common/DatabaseManager.h	Thu Apr 08 12:00:01 2021 +0200
+++ b/Framework/Common/DatabaseManager.h	Thu Apr 08 19:09:04 2021 +0200
@@ -36,6 +36,9 @@
    * WARNING: In PostgreSQL releases <= 3.3 and in MySQL releases <=
    * 3.0, this class was protected by a mutex. It is now assumed that
    * locking must be implemented at a higher level.
+   *
+   * This class maintains a list of precompiled statements. At any
+   * time, this class handles 0 or 1 active transaction.
    **/
   class DatabaseManager : public boost::noncopyable
   {
@@ -47,11 +50,6 @@
     CachedStatements               cachedStatements_;
     Dialect                        dialect_;
 
-    IDatabase& GetDatabase()
-    {
-      return *database_;
-    }
-
     void CloseIfUnavailable(Orthanc::ErrorCode e);
 
     IPrecompiledStatement* LookupCachedStatement(const StatementLocation& location) const;
@@ -71,6 +69,11 @@
       Close();
     }
 
+    IDatabase& GetDatabase()
+    {
+      return *database_;
+    }
+
     Dialect GetDialect() const
     {
       return dialect_;
@@ -110,6 +113,21 @@
       {
         return database_;
       }
+
+      bool DoesTableExist(const std::string& name)
+      {
+        return manager_.GetTransaction().DoesTableExist(name);
+      }
+
+      bool DoesTriggerExist(const std::string& name)
+      {
+        return manager_.GetTransaction().DoesTriggerExist(name);
+      }
+
+      void ExecuteMultiLines(const std::string& sql)
+      {
+        manager_.GetTransaction().ExecuteMultiLines(sql);
+      }
     };
 
 
--- a/Framework/Common/ITransaction.h	Thu Apr 08 12:00:01 2021 +0200
+++ b/Framework/Common/ITransaction.h	Thu Apr 08 19:09:04 2021 +0200
@@ -45,5 +45,11 @@
 
     virtual void ExecuteWithoutResult(IPrecompiledStatement& statement,
                                       const Dictionary& parameters) = 0;
+
+    virtual bool DoesTableExist(const std::string& name) = 0;
+
+    virtual bool DoesTriggerExist(const std::string& name) = 0;  // Only for MySQL
+
+    virtual void ExecuteMultiLines(const std::string& query) = 0;
   };
 }
--- a/Framework/MySQL/MySQLDatabase.cpp	Thu Apr 08 12:00:01 2021 +0200
+++ b/Framework/MySQL/MySQLDatabase.cpp	Thu Apr 08 19:09:04 2021 +0200
@@ -224,8 +224,8 @@
         throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);
       }
       
-      db.Execute("DROP DATABASE " + database, false);
-      db.Execute("CREATE DATABASE " + database, false);
+      db.ExecuteMultiLines("DROP DATABASE " + database, false);
+      db.ExecuteMultiLines("CREATE DATABASE " + database, false);
       t.Commit();
     }
   }
@@ -478,8 +478,8 @@
   }
 
 
-  void MySQLDatabase::Execute(const std::string& sql,
-                              bool arobaseSeparator)
+  void MySQLDatabase::ExecuteMultiLines(const std::string& sql,
+                                        bool arobaseSeparator)
   {
     if (mysql_ == NULL)
     {
@@ -527,6 +527,9 @@
   {
     class MySQLImplicitTransaction : public ImplicitTransaction
     {
+    private:
+      MySQLDatabase& db_;
+      
     protected:
       virtual IResult* ExecuteInternal(IPrecompiledStatement& statement,
                                        const Dictionary& parameters)
@@ -539,6 +542,27 @@
       {
         dynamic_cast<MySQLStatement&>(statement).ExecuteWithoutResult(*this, parameters);
       }
+
+    public:
+      MySQLImplicitTransaction(MySQLDatabase& db) :
+        db_(db)
+      {
+      }
+      
+      virtual bool DoesTableExist(const std::string& name) ORTHANC_OVERRIDE
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, "An explicit transaction is needed");
+      }
+
+      virtual bool DoesTriggerExist(const std::string& name) ORTHANC_OVERRIDE
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, "An explicit transaction is needed");
+      }
+
+      virtual void ExecuteMultiLines(const std::string& query) ORTHANC_OVERRIDE
+      {
+        db_.ExecuteMultiLines(query, false /* don't deal with arobases */);
+      }
     };
   }
   
@@ -553,7 +577,7 @@
     switch (type)
     {
       case TransactionType_Implicit:
-        return new MySQLImplicitTransaction;
+        return new MySQLImplicitTransaction(*this);
 
       case TransactionType_ReadOnly:
       case TransactionType_ReadWrite:
@@ -635,7 +659,7 @@
       {
         std::unique_ptr<MySQLDatabase> db(new MySQLDatabase(parameters_));
         db->Open();
-        db->Execute("SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE", false);
+        db->ExecuteMultiLines("SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE", false);
         return db.release();
       }
       
--- a/Framework/MySQL/MySQLDatabase.h	Thu Apr 08 12:00:01 2021 +0200
+++ b/Framework/MySQL/MySQLDatabase.h	Thu Apr 08 19:09:04 2021 +0200
@@ -79,8 +79,8 @@
 
     void AdvisoryLock(const std::string& lock);
 
-    void Execute(const std::string& sql,
-                 bool arobaseSeparator);
+    void ExecuteMultiLines(const std::string& sql,
+                           bool arobaseSeparator);
 
     bool DoesTableExist(MySQLTransaction& transaction,
                         const std::string& name);
--- a/Framework/MySQL/MySQLTransaction.cpp	Thu Apr 08 12:00:01 2021 +0200
+++ b/Framework/MySQL/MySQLTransaction.cpp	Thu Apr 08 19:09:04 2021 +0200
@@ -39,11 +39,11 @@
     switch (type)
     {
       case TransactionType_ReadWrite:
-        db_.Execute("START TRANSACTION READ WRITE", false);
+        db_.ExecuteMultiLines("START TRANSACTION READ WRITE", false);
         break;
 
       case TransactionType_ReadOnly:
-        db_.Execute("START TRANSACTION READ ONLY", false);
+        db_.ExecuteMultiLines("START TRANSACTION READ ONLY", false);
         break;
 
       default:
@@ -62,7 +62,7 @@
 
       try
       {
-        db_.Execute("ROLLBACK", false);
+        db_.ExecuteMultiLines("ROLLBACK", false);
       }
       catch (Orthanc::OrthancException&)
       {
@@ -76,7 +76,7 @@
   {
     if (active_)
     {
-      db_.Execute("ROLLBACK", false);
+      db_.ExecuteMultiLines("ROLLBACK", false);
       active_ = false;
     }
     else
@@ -91,7 +91,7 @@
   {
     if (active_)
     {
-      db_.Execute("COMMIT", false);
+      db_.ExecuteMultiLines("COMMIT", false);
       active_ = false;
     }
     else
--- a/Framework/MySQL/MySQLTransaction.h	Thu Apr 08 12:00:01 2021 +0200
+++ b/Framework/MySQL/MySQLTransaction.h	Thu Apr 08 19:09:04 2021 +0200
@@ -56,5 +56,20 @@
 
     virtual void ExecuteWithoutResult(IPrecompiledStatement& transaction,
                                       const Dictionary& parameters) ORTHANC_OVERRIDE;
+
+    virtual bool DoesTableExist(const std::string& name)
+    {
+      return db_.DoesTableExist(*this, name);
+    }
+
+    virtual bool DoesTriggerExist(const std::string& name)
+    {
+      return db_.DoesTriggerExist(*this, name);
+    }
+
+    virtual void ExecuteMultiLines(const std::string& query)
+    {
+      db_.ExecuteMultiLines(query, false /* don't deal with arobases */);
+    }
   };
 }
--- a/Framework/Plugins/DatabaseBackendAdapterV3.cpp	Thu Apr 08 12:00:01 2021 +0200
+++ b/Framework/Plugins/DatabaseBackendAdapterV3.cpp	Thu Apr 08 19:09:04 2021 +0200
@@ -151,11 +151,12 @@
       if (connections_.size() == 0)
       {
         assert(backend_.get() != NULL);
-        
-        std::unique_ptr<IDatabase> database(backend_->OpenDatabaseConnection());
-        backend_->ConfigureDatabase(*database);
 
-        connections_.push_back(new DatabaseManager(database.release()));
+        {
+          std::unique_ptr<DatabaseManager> manager(new DatabaseManager(backend_->OpenDatabaseConnection()));
+          backend_->ConfigureDatabase(*manager);
+          connections_.push_back(manager.release());
+        }
 
         for (size_t i = 1; i < countConnections_; i++)
         {
--- a/Framework/Plugins/GlobalProperties.h	Thu Apr 08 12:00:01 2021 +0200
+++ b/Framework/Plugins/GlobalProperties.h	Thu Apr 08 19:09:04 2021 +0200
@@ -21,8 +21,6 @@
 
 #pragma once
 
-#include "../Common/DatabaseManager.h"
-
 #define MISSING_SERVER_IDENTIFIER ""
 
 
@@ -58,41 +56,3 @@
     GlobalProperty_DatabaseInternal9 = 19
   };
 }
-
-
-namespace OrthancDatabases
-{
-  bool LookupGlobalProperty(std::string& target /* out */,
-                            IDatabase& db,
-                            ITransaction& transaction,
-                            const std::string& serverIdentifier,
-                            Orthanc::GlobalProperty property);
-
-  bool LookupGlobalProperty(std::string& target /* out */,
-                            DatabaseManager& manager,
-                            const std::string& serverIdentifier,
-                            Orthanc::GlobalProperty property);
-
-  void SetGlobalProperty(IDatabase& db,
-                         ITransaction& transaction,
-                         const std::string& serverIdentifier,
-                         Orthanc::GlobalProperty property,
-                         const std::string& utf8);
-
-  void SetGlobalProperty(DatabaseManager& manager,
-                         const std::string& serverIdentifier,
-                         Orthanc::GlobalProperty property,
-                         const std::string& utf8);
-
-  bool LookupGlobalIntegerProperty(int& target,
-                                   IDatabase& db,
-                                   ITransaction& transaction,
-                                   const std::string& serverIdentifier,
-                                   Orthanc::GlobalProperty property);
-
-  void SetGlobalIntegerProperty(IDatabase& db,
-                                ITransaction& transaction,
-                                const std::string& serverIdentifier,
-                                Orthanc::GlobalProperty property,
-                                int value);
-}
--- a/Framework/Plugins/IDatabaseBackend.h	Thu Apr 08 12:00:01 2021 +0200
+++ b/Framework/Plugins/IDatabaseBackend.h	Thu Apr 08 19:09:04 2021 +0200
@@ -42,7 +42,7 @@
     virtual IDatabase* OpenDatabaseConnection() = 0;
 
     // This function is invoked once, even if multiple connections are open
-    virtual void ConfigureDatabase(IDatabase& database) = 0;
+    virtual void ConfigureDatabase(DatabaseManager& database) = 0;
 
     virtual void SetOutputFactory(IDatabaseBackendOutput::IFactory* factory) = 0;
                         
@@ -213,7 +213,7 @@
     virtual void SetGlobalProperty(DatabaseManager& manager,
                                    const char* serverIdentifier,
                                    int32_t property,
-                                   const char* value) = 0;
+                                   const char* utf8) = 0;
 
     virtual void SetMainDicomTag(DatabaseManager& manager,
                                  int64_t id,
--- a/Framework/Plugins/IndexBackend.cpp	Thu Apr 08 12:00:01 2021 +0200
+++ b/Framework/Plugins/IndexBackend.cpp	Thu Apr 08 19:09:04 2021 +0200
@@ -1068,8 +1068,41 @@
                                           const char* serverIdentifier,
                                           int32_t property)
   {
-    return ::OrthancDatabases::LookupGlobalProperty(target, manager, serverIdentifier,
-                                                    static_cast<Orthanc::GlobalProperty>(property));
+    DatabaseManager::CachedStatement statement(
+      STATEMENT_FROM_HERE, manager,
+      "SELECT value FROM GlobalProperties WHERE property=${property}");
+
+    statement.SetReadOnly(true);
+    statement.SetParameterType("property", ValueType_Integer64);
+
+    Dictionary args;
+    args.SetIntegerValue("property", property);
+
+    statement.Execute(args);
+    statement.SetResultFieldType(0, ValueType_Utf8String);
+
+    if (statement.IsDone())
+    {
+      return false;
+    }
+    else
+    {
+      ValueType type = statement.GetResultField(0).GetType();
+
+      if (type == ValueType_Null)
+      {
+        return false;
+      }
+      else if (type != ValueType_Utf8String)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_Database);
+      }
+      else
+      {
+        target = dynamic_cast<const Utf8StringValue&>(statement.GetResultField(0)).GetContent();
+        return true;
+      }
+    }
   }
 
     
@@ -1332,10 +1365,53 @@
   void IndexBackend::SetGlobalProperty(DatabaseManager& manager,
                                        const char* serverIdentifier,
                                        int32_t property,
-                                       const char* value)
+                                       const char* utf8)
   {
-    return ::OrthancDatabases::SetGlobalProperty(
-      manager, serverIdentifier, static_cast<Orthanc::GlobalProperty>(property), value);
+    if (manager.GetDialect() == Dialect_SQLite)
+    {
+      DatabaseManager::CachedStatement statement(
+        STATEMENT_FROM_HERE, manager,
+        "INSERT OR REPLACE INTO GlobalProperties VALUES (${property}, ${value})");
+        
+      statement.SetParameterType("property", ValueType_Integer64);
+      statement.SetParameterType("value", ValueType_Utf8String);
+        
+      Dictionary args;
+      args.SetIntegerValue("property", static_cast<int>(property));
+      args.SetUtf8Value("value", utf8);
+        
+      statement.Execute(args);
+    }
+    else
+    {
+      {
+        DatabaseManager::CachedStatement statement(
+          STATEMENT_FROM_HERE, manager,
+          "DELETE FROM GlobalProperties WHERE property=${property}");
+        
+        statement.SetParameterType("property", ValueType_Integer64);
+        
+        Dictionary args;
+        args.SetIntegerValue("property", property);
+        
+        statement.Execute(args);
+      }
+
+      {
+        DatabaseManager::CachedStatement statement(
+          STATEMENT_FROM_HERE, manager,
+          "INSERT INTO GlobalProperties VALUES (${property}, ${value})");
+        
+        statement.SetParameterType("property", ValueType_Integer64);
+        statement.SetParameterType("value", ValueType_Utf8String);
+        
+        Dictionary args;
+        args.SetIntegerValue("property", static_cast<int>(property));
+        args.SetUtf8Value("value", utf8);
+        
+        statement.Execute(args);
+      }
+    }
   }
 
 
@@ -2304,6 +2380,43 @@
   }
 
 
+  bool IndexBackend::LookupGlobalIntegerProperty(int& target,
+                                                 DatabaseManager& manager,
+                                                 const char* serverIdentifier,
+                                                 int32_t property)
+  {
+    std::string value;
+
+    if (LookupGlobalProperty(value, manager, serverIdentifier, property))
+    {
+      try
+      {
+        target = boost::lexical_cast<int>(value);
+        return true;
+      }
+      catch (boost::bad_lexical_cast&)
+      {
+        LOG(ERROR) << "Corrupted PostgreSQL database";
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_Database);
+      }      
+    }
+    else
+    {
+      return false;
+    }    
+  }
+  
+
+  void IndexBackend::SetGlobalIntegerProperty(DatabaseManager& manager,
+                                              const char* serverIdentifier,
+                                              int32_t property,
+                                              int value)
+  {
+    std::string s = boost::lexical_cast<std::string>(value);
+    SetGlobalProperty(manager, serverIdentifier, property, s.c_str());
+  }
+  
+
   void IndexBackend::Finalize()
   {
     OrthancDatabases::DatabaseBackendAdapterV2::Finalize();
@@ -2318,8 +2431,8 @@
 
   DatabaseManager* IndexBackend::CreateSingleDatabaseManager(IDatabaseBackend& backend)
   {
-    std::unique_ptr<IDatabase> database(backend.OpenDatabaseConnection());
-    backend.ConfigureDatabase(*database);
-    return new DatabaseManager(database.release());
+    std::unique_ptr<DatabaseManager> manager(new DatabaseManager(backend.OpenDatabaseConnection()));
+    backend.ConfigureDatabase(*manager);
+    return manager.release();
   }
 }
--- a/Framework/Plugins/IndexBackend.h	Thu Apr 08 12:00:01 2021 +0200
+++ b/Framework/Plugins/IndexBackend.h	Thu Apr 08 19:09:04 2021 +0200
@@ -88,7 +88,7 @@
                                        uint32_t maxResults);
 
   public:
-    IndexBackend(OrthancPluginContext* context);
+    explicit IndexBackend(OrthancPluginContext* context);
 
     virtual OrthancPluginContext* GetContext() ORTHANC_OVERRIDE
     {
@@ -254,7 +254,7 @@
     virtual void SetGlobalProperty(DatabaseManager& manager,
                                    const char* serverIdentifier,
                                    int32_t property,
-                                   const char* value) ORTHANC_OVERRIDE;
+                                   const char* utf8) ORTHANC_OVERRIDE;
 
     virtual void SetMainDicomTag(DatabaseManager& manager,
                                  int64_t id,
@@ -381,6 +381,16 @@
                                const char* hashSeries,
                                const char* hashInstance);
 
+    bool LookupGlobalIntegerProperty(int& target /*out*/,
+                                     DatabaseManager& manager,
+                                     const char* serverIdentifier,
+                                     int32_t property);
+    
+    void SetGlobalIntegerProperty(DatabaseManager& manager,
+                                  const char* serverIdentifier,
+                                  int32_t property,
+                                  int value);
+
     /**
      * "maxDatabaseRetries" is to handle
      * "OrthancPluginErrorCode_DatabaseCannotSerialize" if there is a
--- a/Framework/PostgreSQL/PostgreSQLDatabase.cpp	Thu Apr 08 12:00:01 2021 +0200
+++ b/Framework/PostgreSQL/PostgreSQLDatabase.cpp	Thu Apr 08 19:09:04 2021 +0200
@@ -155,7 +155,7 @@
   }
 
 
-  void PostgreSQLDatabase::Execute(const std::string& sql)
+  void PostgreSQLDatabase::ExecuteMultiLines(const std::string& sql)
   {
     LOG(TRACE) << "PostgreSQL: " << sql;
     Open();
@@ -210,14 +210,14 @@
     PostgreSQLTransaction transaction(*this, TransactionType_ReadWrite);
     
     // Remove all the large objects
-    Execute("SELECT lo_unlink(loid) FROM (SELECT DISTINCT loid FROM pg_catalog.pg_largeobject) as loids;");
+    ExecuteMultiLines("SELECT lo_unlink(loid) FROM (SELECT DISTINCT loid FROM pg_catalog.pg_largeobject) as loids;");
 
     // http://stackoverflow.com/a/21247009/881731
-    Execute("DROP SCHEMA public CASCADE;");
-    Execute("CREATE SCHEMA public;");
-    Execute("GRANT ALL ON SCHEMA public TO postgres;");
-    Execute("GRANT ALL ON SCHEMA public TO public;");
-    Execute("COMMENT ON SCHEMA public IS 'standard public schema';");
+    ExecuteMultiLines("DROP SCHEMA public CASCADE;");
+    ExecuteMultiLines("CREATE SCHEMA public;");
+    ExecuteMultiLines("GRANT ALL ON SCHEMA public TO postgres;");
+    ExecuteMultiLines("GRANT ALL ON SCHEMA public TO public;");
+    ExecuteMultiLines("COMMENT ON SCHEMA public IS 'standard public schema';");
 
     transaction.Commit();
   }
@@ -233,6 +233,9 @@
   {
     class PostgreSQLImplicitTransaction : public ImplicitTransaction
     {
+    private:
+      PostgreSQLDatabase& db_;
+      
     protected:
       virtual IResult* ExecuteInternal(IPrecompiledStatement& statement,
                                        const Dictionary& parameters)
@@ -245,6 +248,27 @@
       {
         dynamic_cast<PostgreSQLStatement&>(statement).ExecuteWithoutResult(*this, parameters);
       }
+
+    public:
+      PostgreSQLImplicitTransaction(PostgreSQLDatabase& db) :
+        db_(db)
+      {
+      }
+
+      virtual bool DoesTableExist(const std::string& name) ORTHANC_OVERRIDE
+      {
+        return db_.DoesTableExist(name.c_str());
+      }
+
+      virtual bool DoesTriggerExist(const std::string& name) ORTHANC_OVERRIDE
+      {
+        return false;
+      }
+
+      virtual void ExecuteMultiLines(const std::string& query) ORTHANC_OVERRIDE
+      {
+        db_.ExecuteMultiLines(query);
+      }
     };
   }
   
@@ -254,7 +278,7 @@
     switch (type)
     {
       case TransactionType_Implicit:
-        return new PostgreSQLImplicitTransaction;
+        return new PostgreSQLImplicitTransaction(*this);
 
       case TransactionType_ReadWrite:
       case TransactionType_ReadOnly:
--- a/Framework/PostgreSQL/PostgreSQLDatabase.h	Thu Apr 08 12:00:01 2021 +0200
+++ b/Framework/PostgreSQL/PostgreSQLDatabase.h	Thu Apr 08 19:09:04 2021 +0200
@@ -62,7 +62,7 @@
 
     void AdvisoryLock(int32_t lock);
 
-    void Execute(const std::string& sql);
+    void ExecuteMultiLines(const std::string& sql);
 
     bool DoesTableExist(const char* name);
 
--- a/Framework/PostgreSQL/PostgreSQLStatement.cpp	Thu Apr 08 12:00:01 2021 +0200
+++ b/Framework/PostgreSQL/PostgreSQLStatement.cpp	Thu Apr 08 19:09:04 2021 +0200
@@ -201,7 +201,7 @@
       // "Although there is no libpq function for deleting a
       // prepared statement, the SQL DEALLOCATE statement can be
       // used for that purpose."
-      database_.Execute("DEALLOCATE \"" + id_ + "\"");
+      database_.ExecuteMultiLines("DEALLOCATE \"" + id_ + "\"");
     }
 
     id_.clear();
--- a/Framework/PostgreSQL/PostgreSQLTransaction.cpp	Thu Apr 08 12:00:01 2021 +0200
+++ b/Framework/PostgreSQL/PostgreSQLTransaction.cpp	Thu Apr 08 19:09:04 2021 +0200
@@ -46,7 +46,7 @@
 
       try
       {
-        database_.Execute("ABORT");
+        database_.ExecuteMultiLines("ABORT");
       }
       catch (Orthanc::OrthancException&)
       {
@@ -64,16 +64,16 @@
       throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
     }
 
-    database_.Execute("BEGIN");
+    database_.ExecuteMultiLines("BEGIN");
 
     switch (type)
     {
       case TransactionType_ReadWrite:
-        database_.Execute("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE READ WRITE");
+        database_.ExecuteMultiLines("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE READ WRITE");
         break;
 
       case TransactionType_ReadOnly:
-        database_.Execute("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE READ ONLY");
+        database_.ExecuteMultiLines("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE READ ONLY");
         break;
 
       default:
@@ -93,7 +93,7 @@
       throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
     }
 
-    database_.Execute("ABORT");
+    database_.ExecuteMultiLines("ABORT");
     isOpen_ = false;
   }
 
@@ -107,7 +107,7 @@
       throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
     }
 
-    database_.Execute("COMMIT");
+    database_.ExecuteMultiLines("COMMIT");
     isOpen_ = false;
   }
 
--- a/Framework/PostgreSQL/PostgreSQLTransaction.h	Thu Apr 08 12:00:01 2021 +0200
+++ b/Framework/PostgreSQL/PostgreSQLTransaction.h	Thu Apr 08 19:09:04 2021 +0200
@@ -59,5 +59,20 @@
 
     virtual void ExecuteWithoutResult(IPrecompiledStatement& statement,
                                       const Dictionary& parameters) ORTHANC_OVERRIDE;
+
+    virtual bool DoesTableExist(const std::string& name) ORTHANC_OVERRIDE
+    {
+      return database_.DoesTableExist(name.c_str());
+    }
+
+    virtual bool DoesTriggerExist(const std::string& name) ORTHANC_OVERRIDE
+    {
+      return false;
+    }
+
+    virtual void ExecuteMultiLines(const std::string& query) ORTHANC_OVERRIDE
+    {
+      database_.ExecuteMultiLines(query);
+    }
   };
 }
--- a/Framework/SQLite/SQLiteDatabase.cpp	Thu Apr 08 12:00:01 2021 +0200
+++ b/Framework/SQLite/SQLiteDatabase.cpp	Thu Apr 08 19:09:04 2021 +0200
@@ -48,6 +48,9 @@
   {
     class SQLiteImplicitTransaction : public ImplicitTransaction
     {
+    private:
+      SQLiteDatabase&  db_;
+      
     protected:
       virtual IResult* ExecuteInternal(IPrecompiledStatement& statement,
                                        const Dictionary& parameters) ORTHANC_OVERRIDE
@@ -60,6 +63,27 @@
       {
         dynamic_cast<SQLiteStatement&>(statement).ExecuteWithoutResult(*this, parameters);
       }
+
+    public:
+      SQLiteImplicitTransaction(SQLiteDatabase& db) :
+        db_(db)
+      {
+      }
+
+      virtual bool DoesTableExist(const std::string& name) ORTHANC_OVERRIDE
+      {
+        return db_.GetObject().DoesTableExist(name.c_str());
+      }
+
+      virtual bool DoesTriggerExist(const std::string& name) ORTHANC_OVERRIDE
+      {
+        return false;
+      }
+
+      virtual void ExecuteMultiLines(const std::string& query) ORTHANC_OVERRIDE
+      {
+        db_.GetObject().Execute(query);
+      }
     };
   }
   
@@ -68,7 +92,7 @@
     switch (type)
     {
       case TransactionType_Implicit:
-        return new SQLiteImplicitTransaction;
+        return new SQLiteImplicitTransaction(*this);
 
       case TransactionType_ReadOnly:
       case TransactionType_ReadWrite:
--- a/Framework/SQLite/SQLiteDatabase.h	Thu Apr 08 12:00:01 2021 +0200
+++ b/Framework/SQLite/SQLiteDatabase.h	Thu Apr 08 19:09:04 2021 +0200
@@ -53,11 +53,6 @@
     }
 
     void Execute(const std::string& sql);
-
-    bool DoesTableExist(const std::string& table)
-    {
-      return connection_.DoesTableExist(table.c_str());
-    }
     
     int64_t GetLastInsertRowId() const
     {
--- a/Framework/SQLite/SQLiteTransaction.cpp	Thu Apr 08 12:00:01 2021 +0200
+++ b/Framework/SQLite/SQLiteTransaction.cpp	Thu Apr 08 19:09:04 2021 +0200
@@ -30,6 +30,7 @@
 namespace OrthancDatabases
 {
   SQLiteTransaction::SQLiteTransaction(SQLiteDatabase& database) :
+    database_(database),
     transaction_(database.GetObject())
   {
     transaction_.Begin();
--- a/Framework/SQLite/SQLiteTransaction.h	Thu Apr 08 12:00:01 2021 +0200
+++ b/Framework/SQLite/SQLiteTransaction.h	Thu Apr 08 19:09:04 2021 +0200
@@ -35,6 +35,7 @@
   class SQLiteTransaction : public ITransaction
   {
   private:
+    SQLiteDatabase&               database_;
     Orthanc::SQLite::Transaction  transaction_;
     
   public:
@@ -60,5 +61,20 @@
 
     virtual void ExecuteWithoutResult(IPrecompiledStatement& statement,
                                       const Dictionary& parameters) ORTHANC_OVERRIDE;
+
+    virtual bool DoesTableExist(const std::string& name) ORTHANC_OVERRIDE
+    {
+      return database_.GetObject().DoesTableExist(name.c_str());
+    }
+
+    virtual bool DoesTriggerExist(const std::string& name) ORTHANC_OVERRIDE
+    {
+      return false;
+    }
+
+    virtual void ExecuteMultiLines(const std::string& query) ORTHANC_OVERRIDE
+    {
+      database_.GetObject().Execute(query);
+    }
   };
 }
--- a/MySQL/Plugins/MySQLIndex.cpp	Thu Apr 08 12:00:01 2021 +0200
+++ b/MySQL/Plugins/MySQLIndex.cpp	Thu Apr 08 19:09:04 2021 +0200
@@ -62,10 +62,8 @@
   }
   
 
-  void MySQLIndex::ConfigureDatabase(IDatabase& database)
+  void MySQLIndex::ConfigureDatabase(DatabaseManager& manager)
   {
-    MySQLDatabase& db = dynamic_cast<MySQLDatabase&>(database);
-    
     uint32_t expectedVersion = 6;
 
     if (GetContext())   // "GetContext()" can possibly be NULL in the unit tests
@@ -92,6 +90,8 @@
       MySQLDatabase::ClearDatabase(parameters_);
     }
 
+    MySQLDatabase& db = dynamic_cast<MySQLDatabase&>(manager.GetDatabase());
+    
     {
       MySQLDatabase::TransientAdvisoryLock lock(db, MYSQL_LOCK_DATABASE_SETUP);
 
@@ -110,19 +110,21 @@
        * https://groups.google.com/d/msg/orthanc-users/OCFFkm1qm0k/Mbroy8VWAQAJ
        **/      
       {
-        MySQLTransaction t(db, TransactionType_ReadWrite);
+        DatabaseManager::Transaction t(manager, TransactionType_ReadWrite);
         
-        db.Execute("ALTER DATABASE " + parameters_.GetDatabase() + 
-                    " CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci", false);
+        t.ExecuteMultiLines("ALTER DATABASE " + parameters_.GetDatabase() + 
+                            " CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
 
         // This is the first table to be created
-        if (!db.DoesTableExist(t, "GlobalProperties"))
+        if (!t.DoesTableExist("GlobalProperties"))
         {
           std::string query;
           
           Orthanc::EmbeddedResources::GetFileResource
             (query, Orthanc::EmbeddedResources::MYSQL_PREPARE_INDEX);
-          db.Execute(query, true);
+
+          // Need to escape arobases: Don't use "t.ExecuteMultiLines()" here
+          db.ExecuteMultiLines(query, true);
         }
 
         t.Commit();
@@ -139,25 +141,25 @@
       int version = 0;
 
       {
-        MySQLTransaction t(db, TransactionType_ReadWrite);
+        DatabaseManager::Transaction t(manager, TransactionType_ReadWrite);
 
         // This is the last table to be created
-        if (!db.DoesTableExist(t, "PatientRecyclingOrder"))
+        if (!t.DoesTableExist("PatientRecyclingOrder"))
         {
           LOG(ERROR) << "Corrupted MySQL database";
           throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);        
         }
 
         // This is the last item to be created
-        if (!db.DoesTriggerExist(t, "PatientAdded"))
+        if (!t.DoesTriggerExist("PatientAdded"))
         {
           ThrowCannotCreateTrigger();
         }
 
-        if (!LookupGlobalIntegerProperty(version, db, t, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabaseSchemaVersion))
+        if (!LookupGlobalIntegerProperty(version, manager, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabaseSchemaVersion))
         {
-          SetGlobalIntegerProperty(db, t, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabaseSchemaVersion, expectedVersion);
-          SetGlobalIntegerProperty(db, t, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabasePatchLevel, 1);
+          SetGlobalIntegerProperty(manager, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabaseSchemaVersion, expectedVersion);
+          SetGlobalIntegerProperty(manager, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabasePatchLevel, 1);
           version = expectedVersion;
         }
 
@@ -173,12 +175,12 @@
       int revision = 0;
 
       {
-        MySQLTransaction t(db, TransactionType_ReadWrite);
+        DatabaseManager::Transaction t(manager, TransactionType_ReadWrite);
 
-        if (!LookupGlobalIntegerProperty(revision, db, t, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabasePatchLevel))
+        if (!LookupGlobalIntegerProperty(revision, manager, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabasePatchLevel))
         {
           revision = 1;
-          SetGlobalIntegerProperty(db, t, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabasePatchLevel, revision);
+          SetGlobalIntegerProperty(manager, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabasePatchLevel, revision);
         }
 
         t.Commit();
@@ -186,72 +188,76 @@
 
       if (revision == 1)
       {
-        MySQLTransaction t(db, TransactionType_ReadWrite);
+        DatabaseManager::Transaction t(manager, TransactionType_ReadWrite);
         
         // The serialization of jobs as a global property can lead to
         // very long values => switch to the LONGTEXT type that can
         // store up to 4GB:
         // https://stackoverflow.com/a/13932834/881731
-        db.Execute("ALTER TABLE GlobalProperties MODIFY value LONGTEXT", false);
+        t.ExecuteMultiLines("ALTER TABLE GlobalProperties MODIFY value LONGTEXT");
         
         revision = 2;
-        SetGlobalIntegerProperty(db, t, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabasePatchLevel, revision);
+        SetGlobalIntegerProperty(manager, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabasePatchLevel, revision);
 
         t.Commit();
       }
 
       if (revision == 2)
       {        
-        MySQLTransaction t(db, TransactionType_ReadWrite);
+        DatabaseManager::Transaction t(manager, TransactionType_ReadWrite);
 
         // Install the "GetLastChangeIndex" extension
         std::string query;
 
         Orthanc::EmbeddedResources::GetFileResource
           (query, Orthanc::EmbeddedResources::MYSQL_GET_LAST_CHANGE_INDEX);
-        db.Execute(query, true);
 
-        if (!db.DoesTriggerExist(t, "ChangeAdded"))
+        // Need to escape arobases: Don't use "t.ExecuteMultiLines()" here
+        db.ExecuteMultiLines(query, true);
+
+        if (!t.DoesTriggerExist("ChangeAdded"))
         {
           ThrowCannotCreateTrigger();
         }
         
         revision = 3;
-        SetGlobalIntegerProperty(db, t, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabasePatchLevel, revision);
+        SetGlobalIntegerProperty(manager, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabasePatchLevel, revision);
 
         t.Commit();
       }
       
       if (revision == 3)
       {
-        MySQLTransaction t(db, TransactionType_ReadWrite);
+        DatabaseManager::Transaction t(manager, TransactionType_ReadWrite);
 
         // Reconfiguration of "Metadata" from TEXT type (up to 64KB)
         // to the LONGTEXT type (up to 4GB). This might be important
         // for applications such as the Osimis Web viewer that stores
         // large amount of metadata.
         // http://book.orthanc-server.com/faq/features.html#central-registry-of-metadata-and-attachments
-        db.Execute("ALTER TABLE Metadata MODIFY value LONGTEXT", false);
+        t.ExecuteMultiLines("ALTER TABLE Metadata MODIFY value LONGTEXT");
         
         revision = 4;
-        SetGlobalIntegerProperty(db, t, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabasePatchLevel, revision);
+        SetGlobalIntegerProperty(manager, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabasePatchLevel, revision);
 
         t.Commit();
       }
       
       if (revision == 4)
       {
-        MySQLTransaction t(db, TransactionType_ReadWrite);
+        DatabaseManager::Transaction t(manager, TransactionType_ReadWrite);
         
         // Install the "CreateInstance" extension
         std::string query;
         
         Orthanc::EmbeddedResources::GetFileResource
           (query, Orthanc::EmbeddedResources::MYSQL_CREATE_INSTANCE);
-        db.Execute(query, true);
+
+        // Need to escape arobases: Don't use "t.ExecuteMultiLines()" here
+        db.ExecuteMultiLines(query, true);
         
         revision = 5;
-        SetGlobalIntegerProperty(db, t, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabasePatchLevel, revision);
+        SetGlobalIntegerProperty(manager, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabasePatchLevel, revision);
 
         t.Commit();
       }
--- a/MySQL/Plugins/MySQLIndex.h	Thu Apr 08 12:00:01 2021 +0200
+++ b/MySQL/Plugins/MySQLIndex.h	Thu Apr 08 19:09:04 2021 +0200
@@ -43,7 +43,7 @@
 
     virtual IDatabase* OpenDatabaseConnection() ORTHANC_OVERRIDE;
 
-    virtual void ConfigureDatabase(IDatabase& database) ORTHANC_OVERRIDE;
+    virtual void ConfigureDatabase(DatabaseManager& database) ORTHANC_OVERRIDE;
  
     virtual int64_t CreateResource(DatabaseManager& manager,
                                    const char* publicId,
--- a/MySQL/Plugins/MySQLStorageArea.cpp	Thu Apr 08 12:00:01 2021 +0200
+++ b/MySQL/Plugins/MySQLStorageArea.cpp	Thu Apr 08 19:09:04 2021 +0200
@@ -59,13 +59,13 @@
                
       if (clearAll)
       {
-        db.Execute("DROP TABLE IF EXISTS StorageArea", false);
+        db.ExecuteMultiLines("DROP TABLE IF EXISTS StorageArea", false);
       }
 
-      db.Execute("CREATE TABLE IF NOT EXISTS StorageArea("
-                 "uuid VARCHAR(64) NOT NULL PRIMARY KEY,"
-                 "content LONGBLOB NOT NULL,"
-                 "type INTEGER NOT NULL)", false);
+      db.ExecuteMultiLines("CREATE TABLE IF NOT EXISTS StorageArea("
+                           "uuid VARCHAR(64) NOT NULL PRIMARY KEY,"
+                           "content LONGBLOB NOT NULL,"
+                           "type INTEGER NOT NULL)", false);
 
       t.Commit();
     }
--- a/PostgreSQL/Plugins/PostgreSQLIndex.cpp	Thu Apr 08 12:00:01 2021 +0200
+++ b/PostgreSQL/Plugins/PostgreSQLIndex.cpp	Thu Apr 08 19:09:04 2021 +0200
@@ -60,7 +60,7 @@
   }
 
   
-  void PostgreSQLIndex::ConfigureDatabase(IDatabase& database)
+  void PostgreSQLIndex::ConfigureDatabase(DatabaseManager& manager)
   {
     uint32_t expectedVersion = 6;
 
@@ -78,7 +78,7 @@
       throw Orthanc::OrthancException(Orthanc::ErrorCode_Plugin);
     }
 
-    PostgreSQLDatabase& db = dynamic_cast<PostgreSQLDatabase&>(database);
+    PostgreSQLDatabase& db = dynamic_cast<PostgreSQLDatabase&>(manager.GetDatabase());
 
     if (parameters_.HasLock())
     {
@@ -94,29 +94,29 @@
       }
 
       {
-        PostgreSQLTransaction t(db, TransactionType_ReadWrite);
+        DatabaseManager::Transaction t(manager, TransactionType_ReadWrite);
 
-        if (!db.DoesTableExist("Resources"))
+        if (!t.DoesTableExist("Resources"))
         {
           std::string query;
 
           Orthanc::EmbeddedResources::GetFileResource
             (query, Orthanc::EmbeddedResources::POSTGRESQL_PREPARE_INDEX);
-          db.Execute(query);
+          t.ExecuteMultiLines(query);
 
-          SetGlobalIntegerProperty(db, t, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabaseSchemaVersion, expectedVersion);
-          SetGlobalIntegerProperty(db, t, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabasePatchLevel, 1);
-          SetGlobalIntegerProperty(db, t, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_HasTrigramIndex, 0);
+          SetGlobalIntegerProperty(manager, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabaseSchemaVersion, expectedVersion);
+          SetGlobalIntegerProperty(manager, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabasePatchLevel, 1);
+          SetGlobalIntegerProperty(manager, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_HasTrigramIndex, 0);
         }
           
-        if (!db.DoesTableExist("Resources"))
+        if (!t.DoesTableExist("Resources"))
         {
           LOG(ERROR) << "Corrupted PostgreSQL database";
           throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);        
         }
 
         int version = 0;
-        if (!LookupGlobalIntegerProperty(version, db, t, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabaseSchemaVersion) ||
+        if (!LookupGlobalIntegerProperty(version, manager, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabaseSchemaVersion) ||
             version != 6)
         {
           LOG(ERROR) << "PostgreSQL plugin is incompatible with database schema version: " << version;
@@ -124,10 +124,10 @@
         }
 
         int revision;
-        if (!LookupGlobalIntegerProperty(revision, db, t, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabasePatchLevel))
+        if (!LookupGlobalIntegerProperty(revision, manager, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabasePatchLevel))
         {
           revision = 1;
-          SetGlobalIntegerProperty(db, t, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabasePatchLevel, revision);
+          SetGlobalIntegerProperty(manager, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabasePatchLevel, revision);
         }
 
         if (revision != 1)
@@ -140,10 +140,10 @@
       }
 
       {
-        PostgreSQLTransaction t(db, TransactionType_ReadWrite);
+        DatabaseManager::Transaction t(manager, TransactionType_ReadWrite);
 
         int hasTrigram = 0;
-        if (!LookupGlobalIntegerProperty(hasTrigram, db, t, MISSING_SERVER_IDENTIFIER,
+        if (!LookupGlobalIntegerProperty(hasTrigram, manager, MISSING_SERVER_IDENTIFIER,
                                          Orthanc::GlobalProperty_HasTrigramIndex) ||
             hasTrigram != 1)
         {
@@ -163,11 +163,11 @@
             LOG(WARNING) << "Trying to enable trigram matching on the PostgreSQL database "
                          << "to speed up wildcard searches. This may take several minutes";
 
-            db.Execute(
+            t.ExecuteMultiLines(
               "CREATE EXTENSION IF NOT EXISTS pg_trgm; "
               "CREATE INDEX DicomIdentifiersIndexValues2 ON DicomIdentifiers USING gin(value gin_trgm_ops);");
 
-            SetGlobalIntegerProperty(db, t, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_HasTrigramIndex, 1);
+            SetGlobalIntegerProperty(manager, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_HasTrigramIndex, 1);
             LOG(WARNING) << "Trigram index has been created";
 
             t.Commit();
@@ -187,10 +187,10 @@
       }
 
       {
-        PostgreSQLTransaction t(db, TransactionType_ReadWrite);
+        DatabaseManager::Transaction t(manager, TransactionType_ReadWrite);
 
         int property = 0;
-        if (!LookupGlobalIntegerProperty(property, db, t, MISSING_SERVER_IDENTIFIER,
+        if (!LookupGlobalIntegerProperty(property, manager, MISSING_SERVER_IDENTIFIER,
                                          Orthanc::GlobalProperty_HasCreateInstance) ||
             property != 2)
         {
@@ -199,20 +199,20 @@
           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)");
+            t.ExecuteMultiLines("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);
+          t.ExecuteMultiLines(query);
 
-          SetGlobalIntegerProperty(db, t, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_HasCreateInstance, 2);
+          SetGlobalIntegerProperty(manager, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_HasCreateInstance, 2);
         }
 
       
-        if (!LookupGlobalIntegerProperty(property, db, t, MISSING_SERVER_IDENTIFIER,
+        if (!LookupGlobalIntegerProperty(property, manager, MISSING_SERVER_IDENTIFIER,
                                          Orthanc::GlobalProperty_GetTotalSizeIsFast) ||
             property != 1)
         {
@@ -221,16 +221,16 @@
           std::string query;
           Orthanc::EmbeddedResources::GetFileResource
             (query, Orthanc::EmbeddedResources::POSTGRESQL_FAST_TOTAL_SIZE);
-          db.Execute(query);
+          t.ExecuteMultiLines(query);
 
-          SetGlobalIntegerProperty(db, t, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_GetTotalSizeIsFast, 1);
+          SetGlobalIntegerProperty(manager, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_GetTotalSizeIsFast, 1);
         }
 
 
         // Installing this extension requires the "GlobalIntegers" table
         // created by the "FastTotalSize" extension
         property = 0;
-        if (!LookupGlobalIntegerProperty(property, db, t, MISSING_SERVER_IDENTIFIER,
+        if (!LookupGlobalIntegerProperty(property, manager, MISSING_SERVER_IDENTIFIER,
                                          Orthanc::GlobalProperty_HasFastCountResources) ||
             property != 1)
         {
@@ -239,16 +239,16 @@
           std::string query;
           Orthanc::EmbeddedResources::GetFileResource
             (query, Orthanc::EmbeddedResources::POSTGRESQL_FAST_COUNT_RESOURCES);
-          db.Execute(query);
+          t.ExecuteMultiLines(query);
 
-          SetGlobalIntegerProperty(db, t, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_HasFastCountResources, 1);
+          SetGlobalIntegerProperty(manager, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_HasFastCountResources, 1);
         }
 
 
         // Installing this extension requires the "GlobalIntegers" table
         // created by the "GetLastChangeIndex" extension
         property = 0;
-        if (!LookupGlobalIntegerProperty(property, db, t, MISSING_SERVER_IDENTIFIER,
+        if (!LookupGlobalIntegerProperty(property, manager, MISSING_SERVER_IDENTIFIER,
                                          Orthanc::GlobalProperty_GetLastChangeIndex) ||
             property != 1)
         {
@@ -257,9 +257,23 @@
           std::string query;
           Orthanc::EmbeddedResources::GetFileResource
             (query, Orthanc::EmbeddedResources::POSTGRESQL_GET_LAST_CHANGE_INDEX);
-          db.Execute(query);
+          t.ExecuteMultiLines(query);
+
+          SetGlobalIntegerProperty(manager, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_GetLastChangeIndex, 1);
+        }
+
+        t.Commit();
+      }
+ 
 
-          SetGlobalIntegerProperty(db, t, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_GetLastChangeIndex, 1);
+      {
+        // New in release 4.0 to deal with multiple writers
+        DatabaseManager::Transaction t(manager, TransactionType_ReadWrite);
+
+        if (!t.DoesTableExist("ServerProperties"))
+        {
+          t.ExecuteMultiLines("CREATE TABLE ServerProperties(server VARCHAR(64) NOT NULL, "
+                              "property INTEGER, value TEXT, PRIMARY KEY(server, property))");
         }
 
         t.Commit();
--- a/PostgreSQL/Plugins/PostgreSQLIndex.h	Thu Apr 08 12:00:01 2021 +0200
+++ b/PostgreSQL/Plugins/PostgreSQLIndex.h	Thu Apr 08 19:09:04 2021 +0200
@@ -43,7 +43,7 @@
 
     virtual IDatabase* OpenDatabaseConnection() ORTHANC_OVERRIDE;
 
-    virtual void ConfigureDatabase(IDatabase& database) ORTHANC_OVERRIDE;
+    virtual void ConfigureDatabase(DatabaseManager& manager) ORTHANC_OVERRIDE;
 
     virtual int64_t CreateResource(DatabaseManager& manager,
                                    const char* publicId,
--- a/PostgreSQL/Plugins/PostgreSQLStorageArea.cpp	Thu Apr 08 12:00:01 2021 +0200
+++ b/PostgreSQL/Plugins/PostgreSQLStorageArea.cpp	Thu Apr 08 19:09:04 2021 +0200
@@ -53,14 +53,14 @@
 
         if (!db.DoesTableExist("StorageArea"))
         {
-          db.Execute("CREATE TABLE IF NOT EXISTS StorageArea("
-                     "uuid VARCHAR NOT NULL PRIMARY KEY,"
-                     "content OID NOT NULL,"
-                     "type INTEGER NOT NULL)");
+          db.ExecuteMultiLines("CREATE TABLE IF NOT EXISTS StorageArea("
+                               "uuid VARCHAR NOT NULL PRIMARY KEY,"
+                               "content OID NOT NULL,"
+                               "type INTEGER NOT NULL)");
           
           // Automatically remove the large objects associated with the table
-          db.Execute("CREATE OR REPLACE RULE StorageAreaDelete AS ON DELETE "
-                     "TO StorageArea DO SELECT lo_unlink(old.content);");
+          db.ExecuteMultiLines("CREATE OR REPLACE RULE StorageAreaDelete AS ON DELETE "
+                               "TO StorageArea DO SELECT lo_unlink(old.content);");
         }
         
         t.Commit();
--- a/PostgreSQL/UnitTests/PostgreSQLTests.cpp	Thu Apr 08 12:00:01 2021 +0200
+++ b/PostgreSQL/UnitTests/PostgreSQLTests.cpp	Thu Apr 08 19:09:04 2021 +0200
@@ -86,7 +86,7 @@
   std::unique_ptr<PostgreSQLDatabase> pg(CreateTestDatabase());
 
   ASSERT_FALSE(pg->DoesTableExist("Test"));
-  pg->Execute("CREATE TABLE Test(name INTEGER, value BIGINT)");
+  pg->ExecuteMultiLines("CREATE TABLE Test(name INTEGER, value BIGINT)");
   ASSERT_TRUE(pg->DoesTableExist("Test"));
 
   PostgreSQLStatement s(*pg, "INSERT INTO Test VALUES ($1,$2)");
@@ -158,7 +158,7 @@
 {
   std::unique_ptr<PostgreSQLDatabase> pg(CreateTestDatabase());
 
-  pg->Execute("CREATE TABLE Test(name INTEGER, value VARCHAR(40))");
+  pg->ExecuteMultiLines("CREATE TABLE Test(name INTEGER, value VARCHAR(40))");
 
   PostgreSQLStatement s(*pg, "INSERT INTO Test VALUES ($1,$2)");
   s.DeclareInputInteger(0);
@@ -204,7 +204,7 @@
 {
   std::unique_ptr<PostgreSQLDatabase> pg(CreateTestDatabase());
 
-  pg->Execute("CREATE TABLE Test(name INTEGER, value INTEGER)");
+  pg->ExecuteMultiLines("CREATE TABLE Test(name INTEGER, value INTEGER)");
 
   {
     PostgreSQLStatement s(*pg, "INSERT INTO Test VALUES ($1,$2)");
@@ -283,10 +283,10 @@
   std::unique_ptr<PostgreSQLDatabase> pg(CreateTestDatabase());
   ASSERT_EQ(0, CountLargeObjects(*pg));
 
-  pg->Execute("CREATE TABLE Test(name VARCHAR, value OID)");
+  pg->ExecuteMultiLines("CREATE TABLE Test(name VARCHAR, value OID)");
 
   // Automatically remove the large objects associated with the table
-  pg->Execute("CREATE RULE TestDelete AS ON DELETE TO Test DO SELECT lo_unlink(old.value);");
+  pg->ExecuteMultiLines("CREATE RULE TestDelete AS ON DELETE TO Test DO SELECT lo_unlink(old.value);");
 
   {
     PostgreSQLStatement s(*pg, "INSERT INTO Test VALUES ($1,$2)");
--- a/Resources/CMake/DatabasesPluginConfiguration.cmake	Thu Apr 08 12:00:01 2021 +0200
+++ b/Resources/CMake/DatabasesPluginConfiguration.cmake	Thu Apr 08 19:09:04 2021 +0200
@@ -77,7 +77,6 @@
   ${ORTHANC_CORE_SOURCES}
   ${ORTHANC_DATABASES_ROOT}/Framework/Plugins/DatabaseBackendAdapterV2.cpp
   ${ORTHANC_DATABASES_ROOT}/Framework/Plugins/DatabaseBackendAdapterV3.cpp
-  ${ORTHANC_DATABASES_ROOT}/Framework/Plugins/GlobalProperties.cpp
   ${ORTHANC_DATABASES_ROOT}/Framework/Plugins/IndexBackend.cpp
   ${ORTHANC_DATABASES_ROOT}/Framework/Plugins/StorageBackend.cpp
   ${ORTHANC_DATABASES_ROOT}/Resources/Orthanc/Databases/DatabaseConstraint.cpp
--- a/SQLite/Plugins/SQLiteIndex.cpp	Thu Apr 08 12:00:01 2021 +0200
+++ b/SQLite/Plugins/SQLiteIndex.cpp	Thu Apr 08 19:09:04 2021 +0200
@@ -47,14 +47,25 @@
       db->Open(path_);
     }
 
+    db->Execute("PRAGMA ENCODING=\"UTF-8\";");
+
+    if (fast_)
+    {
+      // 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");
+    }
+
     return db.release();
   }
 
 
-  void SQLiteIndex::ConfigureDatabase(IDatabase& database)
+  void SQLiteIndex::ConfigureDatabase(DatabaseManager& manager)
   {
-    SQLiteDatabase& db = dynamic_cast<SQLiteDatabase&>(database);
-    
     uint32_t expectedVersion = 6;
 
     if (GetContext())   // "GetContext()" can possibly be NULL in the unit tests
@@ -72,47 +83,35 @@
     }
 
     {
-      SQLiteTransaction t(db);
+      DatabaseManager::Transaction t(manager, TransactionType_ReadWrite);
 
-      if (!db.DoesTableExist("Resources"))
+      if (!t.DoesTableExist("Resources"))
       {
         std::string query;
 
         Orthanc::EmbeddedResources::GetFileResource
           (query, Orthanc::EmbeddedResources::SQLITE_PREPARE_INDEX);
-        db.Execute(query);
- 
-        SetGlobalIntegerProperty(db, t, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabaseSchemaVersion, expectedVersion);
-        SetGlobalIntegerProperty(db, t, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabasePatchLevel, 1);
-     }
+
+        t.ExecuteMultiLines(query);
+
+        SetGlobalIntegerProperty(manager, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabaseSchemaVersion, expectedVersion);
+        SetGlobalIntegerProperty(manager, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabasePatchLevel, 1);
+      }
           
       t.Commit();
     }
 
-    db.Execute("PRAGMA ENCODING=\"UTF-8\";");
+    {
+      DatabaseManager::Transaction t(manager, TransactionType_ReadWrite);
 
-    if (fast_)
-    {
-      // 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");
-    }
-
-    {
-      SQLiteTransaction t(db);
-
-      if (!db.DoesTableExist("Resources"))
+      if (!t.DoesTableExist("Resources"))
       {
         LOG(ERROR) << "Corrupted SQLite database";
         throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);        
       }
 
       int version = 0;
-      if (!LookupGlobalIntegerProperty(version, db, t, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabaseSchemaVersion) ||
+      if (!LookupGlobalIntegerProperty(version, manager, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabaseSchemaVersion) ||
           version != 6)
       {
         LOG(ERROR) << "SQLite plugin is incompatible with database schema version: " << version;
@@ -120,10 +119,10 @@
       }
 
       int revision;
-      if (!LookupGlobalIntegerProperty(revision, db, t, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabasePatchLevel))
+      if (!LookupGlobalIntegerProperty(revision, manager, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabasePatchLevel))
       {
         revision = 1;
-        SetGlobalIntegerProperty(db, t, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabasePatchLevel, revision);
+        SetGlobalIntegerProperty(manager, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabasePatchLevel, revision);
       }
 
       if (revision != 1)
--- a/SQLite/Plugins/SQLiteIndex.h	Thu Apr 08 12:00:01 2021 +0200
+++ b/SQLite/Plugins/SQLiteIndex.h	Thu Apr 08 19:09:04 2021 +0200
@@ -44,7 +44,7 @@
 
     virtual IDatabase* OpenDatabaseConnection() ORTHANC_OVERRIDE;
 
-    virtual void ConfigureDatabase(IDatabase& database) ORTHANC_OVERRIDE;
+    virtual void ConfigureDatabase(DatabaseManager& manager) ORTHANC_OVERRIDE;
     
     virtual int64_t CreateResource(DatabaseManager& manager,
                                    const char* publicId,
--- a/SQLite/UnitTests/UnitTestsMain.cpp	Thu Apr 08 12:00:01 2021 +0200
+++ b/SQLite/UnitTests/UnitTestsMain.cpp	Thu Apr 08 19:09:04 2021 +0200
@@ -65,8 +65,8 @@
   OrthancDatabases::SQLiteDatabase db;
   db.OpenInMemory();
 
-  ASSERT_FALSE(db.DoesTableExist("test"));
-  ASSERT_FALSE(db.DoesTableExist("test2"));
+  ASSERT_FALSE(db.GetObject().DoesTableExist("test"));
+  ASSERT_FALSE(db.GetObject().DoesTableExist("test2"));
 
   {
     std::unique_ptr<OrthancDatabases::ITransaction> t(db.CreateTransaction(OrthancDatabases::TransactionType_ReadWrite));
@@ -101,8 +101,8 @@
     t->ExecuteWithoutResult(*s, args);
   }
 
-  ASSERT_TRUE(db.DoesTableExist("test"));
-  ASSERT_TRUE(db.DoesTableExist("test2"));
+  ASSERT_TRUE(db.GetObject().DoesTableExist("test"));
+  ASSERT_TRUE(db.GetObject().DoesTableExist("test2"));
 }