changeset 329:b5fb8b77ce4d

initial commit of ODBC framework
author Sebastien Jodogne <s.jodogne@gmail.com>
date Tue, 10 Aug 2021 20:08:53 +0200
parents 6a49c495c940
children 8f17f23c9af7
files Framework/Odbc/OdbcDatabase.cpp Framework/Odbc/OdbcDatabase.h Framework/Odbc/OdbcEnvironment.cpp Framework/Odbc/OdbcEnvironment.h Framework/Odbc/OdbcPreparedStatement.cpp Framework/Odbc/OdbcPreparedStatement.h Framework/Odbc/OdbcResult.cpp Framework/Odbc/OdbcResult.h Framework/Odbc/OdbcStatement.cpp Framework/Odbc/OdbcStatement.h MySQL/CMakeLists.txt Odbc/CMakeLists.txt Odbc/Plugins/IndexPlugin.cpp Odbc/Plugins/OdbcIndex.cpp Odbc/Plugins/OdbcIndex.h Odbc/Plugins/PrepareIndex.sql Odbc/Plugins/PrepareStorage.sql Odbc/Plugins/StoragePlugin.cpp Odbc/UnitTests/UnitTestsMain.cpp PostgreSQL/CMakeLists.txt Resources/CMake/DatabasesFrameworkConfiguration.cmake Resources/CMake/DatabasesFrameworkParameters.cmake Resources/CMake/UnixOdbcConfiguration.cmake Resources/Odbc/config.h.in SQLite/CMakeLists.txt
diffstat 25 files changed, 4129 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Odbc/OdbcDatabase.cpp	Tue Aug 10 20:08:53 2021 +0200
@@ -0,0 +1,635 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2021 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "OdbcDatabase.h"
+
+#include "../Common/ImplicitTransaction.h"
+#include "../Common/RetryDatabaseFactory.h"
+#include "../Common/Utf8StringValue.h"
+#include "OdbcPreparedStatement.h"
+#include "OdbcResult.h"
+
+#include <Logging.h>
+#include <OrthancException.h>
+#include <Toolbox.h>
+
+#include <boost/algorithm/string/predicate.hpp>
+#include <sqlext.h>
+
+
+namespace OrthancDatabases
+{
+  static void SetAutoCommitTransaction(SQLHDBC handle,
+                                       bool autocommit)
+  {
+    // Go to autocommit mode
+    SQLPOINTER value = (SQLPOINTER) (autocommit ? SQL_AUTOCOMMIT_ON : SQL_AUTOCOMMIT_OFF);
+      
+    if (!SQL_SUCCEEDED(SQLSetConnectAttr(handle, SQL_ATTR_AUTOCOMMIT, value, SQL_IS_UINTEGER)))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_Database,
+                                      "Cannot switch the autocommit mode");
+    }
+  }
+    
+
+  class OdbcDatabase::OdbcImplicitTransaction : public ImplicitTransaction
+  {
+  private:
+    OdbcDatabase&  db_;
+
+  protected:
+    virtual IResult* ExecuteInternal(IPrecompiledStatement& statement,
+                                     const Dictionary& parameters) ORTHANC_OVERRIDE
+    {
+      return dynamic_cast<OdbcPreparedStatement&>(statement).Execute(parameters);
+    }
+
+    virtual void ExecuteWithoutResultInternal(IPrecompiledStatement& statement,
+                                              const Dictionary& parameters) ORTHANC_OVERRIDE
+    {
+      std::unique_ptr<IResult> result(Execute(statement, parameters));
+    }
+      
+  public:
+    OdbcImplicitTransaction(OdbcDatabase& db) :
+      db_(db)
+    {
+      SetAutoCommitTransaction(db_.GetHandle(), true);
+    }
+
+    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);
+    }
+  };
+    
+    
+  class OdbcDatabase::OdbcExplicitTransaction : public ITransaction
+  {
+  private:
+    OdbcDatabase&  db_;
+    bool           isOpen_;
+
+    void EndTransaction(SQLSMALLINT completionType)
+    {
+      if (!isOpen_)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls, "Transaction is already finalized");
+      }
+      else if (SQL_SUCCEEDED(SQLEndTran(SQL_HANDLE_DBC, db_.GetHandle(), completionType)))
+      {
+        isOpen_ = false;
+      }
+      else
+      {
+        SQLCHAR stateBuf[SQL_SQLSTATE_SIZE + 1];
+        SQLSMALLINT stateLength = 0;
+      
+        const SQLSMALLINT recNum = 1;
+        
+        if (SQL_SUCCEEDED(SQLGetDiagField(SQL_HANDLE_DBC, db_.GetHandle(),
+                                          recNum, SQL_DIAG_SQLSTATE, &stateBuf, sizeof(stateBuf), &stateLength)))
+        {
+          const std::string state(reinterpret_cast<const char*>(stateBuf));
+
+          if (state == "40001")
+          {
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_DatabaseCannotSerialize);
+          }
+        }
+        
+        switch (completionType)
+        {
+          case SQL_COMMIT:
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_Database, "Cannot commit transaction");
+
+          case SQL_ROLLBACK:
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_Database, "Cannot rollback transaction");
+
+          default:
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+        }
+      }
+    }
+      
+  public:
+    OdbcExplicitTransaction(OdbcDatabase& db) :
+      db_(db),
+      isOpen_(true)
+    {
+      SetAutoCommitTransaction(db_.GetHandle(), false);
+    }
+
+    virtual ~OdbcExplicitTransaction()
+    {
+      if (isOpen_)
+      {
+        LOG(INFO) << "An active ODBC transaction was dismissed";
+        if (!SQL_SUCCEEDED(SQLEndTran(SQL_HANDLE_DBC, db_.GetHandle(), SQL_ROLLBACK)))
+        {
+          LOG(ERROR) << "Cannot rollback transaction";
+        }
+      }
+    }
+
+    virtual bool IsImplicit() const ORTHANC_OVERRIDE
+    {
+      return false;
+    }
+
+    virtual void Commit() ORTHANC_OVERRIDE
+    {
+      EndTransaction(SQL_COMMIT);
+    }
+
+    virtual void Rollback() ORTHANC_OVERRIDE
+    {
+      EndTransaction(SQL_ROLLBACK);
+    }
+
+    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);
+    }
+
+    virtual IResult* Execute(IPrecompiledStatement& statement,
+                             const Dictionary& parameters) ORTHANC_OVERRIDE
+    {
+      return dynamic_cast<OdbcPreparedStatement&>(statement).Execute(parameters);
+    }
+
+    virtual void ExecuteWithoutResult(IPrecompiledStatement& statement,
+                                      const Dictionary& parameters) ORTHANC_OVERRIDE
+    {
+      std::unique_ptr<IResult> result(Execute(statement, parameters));
+    }
+  };
+
+
+  static bool ParseThreePartsVersion(unsigned int& majorVersion,
+                                     const std::string& version)
+  {
+    std::vector<std::string> tokens;
+    Orthanc::Toolbox::TokenizeString(tokens, version, '.');
+
+    try
+    {
+      if (tokens.size() == 3u)
+      {
+        int tmp = boost::lexical_cast<int>(tokens[0]);
+        if (tmp >= 0)
+        {
+          majorVersion = static_cast<unsigned int>(tmp);
+          return true;
+        }
+      }
+    }
+    catch (boost::bad_lexical_cast&)
+    {
+    }
+
+    return false;
+  }
+  
+    
+  OdbcDatabase::OdbcDatabase(OdbcEnvironment& environment,
+                             const std::string& connectionString) :
+    dbmsMajorVersion_(0)
+  {
+    LOG(INFO) << "Creating an ODBC connection: " << connectionString;
+      
+    /* Allocate a connection handle */
+    if (!SQL_SUCCEEDED(SQLAllocHandle(SQL_HANDLE_DBC, environment.GetHandle(), &handle_)))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_DatabaseUnavailable,
+                                      "Cannot create ODBC connection");
+    }
+
+    /* Connect to the DSN mydsn */
+    SQLCHAR* tmp = const_cast<SQLCHAR*>(reinterpret_cast<const SQLCHAR*>(connectionString.c_str()));
+    SQLCHAR outBuffer[2048];
+    SQLSMALLINT outSize = 0;
+
+    bool success = true;
+      
+    if (SQL_SUCCEEDED(SQLDriverConnect(handle_, NULL, tmp, SQL_NTS /* null-terminated string */,
+                                       outBuffer, sizeof(outBuffer), &outSize, SQL_DRIVER_COMPLETE)))
+    {
+      LOG(INFO) << "Returned connection string: " << outBuffer;        
+    }
+    else
+    {
+      success = false;
+    }
+
+    if (!SQL_SUCCEEDED(SQLSetConnectAttr(handle_, SQL_ATTR_TXN_ISOLATION, (SQLPOINTER) SQL_TXN_SERIALIZABLE, SQL_NTS)))
+    {
+      /**
+       * Switch to the "serializable" isolation level that is expected
+       * by Orthanc. This is already the default for MySQL and MSSQL,
+       * but is needed for PostgreSQL.
+       * https://docs.microsoft.com/en-us/sql/odbc/reference/develop-app/transaction-isolation-levels
+       **/
+      success = false;
+    }
+
+    SQLCHAR versionBuffer[2048];
+    SQLSMALLINT versionSize;
+
+    if (success &&
+        SQL_SUCCEEDED(SQLGetInfo(handle_, SQL_DBMS_NAME, outBuffer, sizeof(outBuffer) - 1, &outSize)) &&
+        SQL_SUCCEEDED(SQLGetInfo(handle_, SQL_DBMS_VER, versionBuffer, sizeof(versionBuffer) - 1, &versionSize)))
+    {
+      std::string dbms(reinterpret_cast<const char*>(outBuffer), outSize);
+      std::string version(reinterpret_cast<const char*>(versionBuffer), versionSize);
+
+      LOG(WARNING) << "DBMS Name: " << dbms;
+      LOG(WARNING) << "DBMS Version: " << version;
+        
+      if (dbms == "PostgreSQL")
+      {
+        dialect_ = Dialect_PostgreSQL;
+      }
+      else if (dbms == "SQLite")
+      {
+        dialect_ = Dialect_SQLite;
+        ExecuteMultiLines("PRAGMA FOREIGN_KEYS=ON");  // Necessary for cascaded delete to work
+        ExecuteMultiLines("PRAGMA ENCODING=\"UTF-8\"");
+
+        // The following lines speed up SQLite
+        
+        /*ExecuteMultiLines("PRAGMA SYNCHRONOUS=NORMAL;");
+          ExecuteMultiLines("PRAGMA JOURNAL_MODE=WAL;");
+          ExecuteMultiLines("PRAGMA LOCKING_MODE=EXCLUSIVE;");
+          ExecuteMultiLines("PRAGMA WAL_AUTOCHECKPOINT=1000;");*/
+      }
+      else if (dbms == "MySQL")
+      {
+        dialect_ = Dialect_MySQL;
+
+        if (!ParseThreePartsVersion(dbmsMajorVersion_, version))
+        {
+          SQLFreeHandle(SQL_HANDLE_DBC, handle_);
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_Database, "Cannot parse the version of MySQL: " + version);
+        }
+      }
+      else if (dbms == "Microsoft SQL Server")
+      {
+        dialect_ = Dialect_MSSQL;
+
+        if (!ParseThreePartsVersion(dbmsMajorVersion_, version))
+        {
+          SQLFreeHandle(SQL_HANDLE_DBC, handle_);
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_Database, "Cannot parse the version of SQL Server: " + version);
+        }
+      }
+      else
+      {
+        SQLFreeHandle(SQL_HANDLE_DBC, handle_);
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_Database, "Unknown SQL dialect for DBMS: " + dbms);
+      }
+    }
+    else
+    {
+      success = false;
+    }
+
+    if (!success)
+    {
+      std::string error = FormatError();
+      SQLFreeHandle(SQL_HANDLE_DBC, handle_); // Cannot call FormatError() below this point
+        
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_DatabaseUnavailable, "Error in SQLDriverConnect():\n" + error);
+    }
+  }
+
+    
+  OdbcDatabase::~OdbcDatabase()
+  {
+    LOG(INFO) << "Destructing an ODBC connection";
+      
+    if (!SQL_SUCCEEDED(SQLDisconnect(handle_)))
+    {
+      LOG(ERROR) << "Cannot disconnect from driver";
+    }
+      
+    if (!SQL_SUCCEEDED(SQLFreeHandle(SQL_HANDLE_DBC, handle_)))
+    {
+      LOG(ERROR) << "Cannot destruct the ODBC connection";
+    }
+  }
+
+
+  std::string OdbcDatabase::FormatError()
+  {
+    return OdbcEnvironment::FormatError(handle_, SQL_HANDLE_DBC);
+  }
+
+
+  void OdbcDatabase::ListTables(std::set<std::string>& target)
+  {
+    target.clear();
+
+    OdbcStatement statement(GetHandle());
+
+    if (SQL_SUCCEEDED(SQLTables(statement.GetHandle(), NULL, 0, NULL, 0, NULL, 0,
+                                const_cast<SQLCHAR*>(reinterpret_cast<const SQLCHAR*>("'TABLE'")), SQL_NTS)))
+    {
+      OdbcResult result(statement, dialect_);
+
+      while (!result.IsDone())
+      {
+        if (result.GetFieldsCount() < 5)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_Database, "Invalid result for SQLTables()");
+        }
+        else
+        {
+          if (result.GetField(2).GetType() == ValueType_Utf8String &&
+              result.GetField(3).GetType() == ValueType_Utf8String &&
+              dynamic_cast<const Utf8StringValue&>(result.GetField(3)).GetContent() == "TABLE")
+          {
+            std::string name = dynamic_cast<const Utf8StringValue&>(result.GetField(2)).GetContent();
+            Orthanc::Toolbox::ToLowerCase(name);
+            target.insert(name);
+          }
+        }
+
+        result.Next();
+      }
+    }
+  }
+
+
+  bool OdbcDatabase::DoesTableExist(const std::string& name)
+  {
+    std::set<std::string> tables;
+    ListTables(tables);
+    return (tables.find(name) != tables.end());
+  }
+
+  
+  void OdbcDatabase::ExecuteMultiLines(const std::string& query)
+  {
+    OdbcStatement statement(GetHandle());
+
+    std::vector<std::string> lines;
+    Orthanc::Toolbox::TokenizeString(lines, query, ';');
+      
+    for (size_t i = 0; i < lines.size(); i++)
+    {
+      std::string line = Orthanc::Toolbox::StripSpaces(lines[i]);
+      if (!line.empty())
+      {
+        LOG(INFO) << "Running ODBC SQL: " << line;
+        SQLCHAR* tmp = const_cast<SQLCHAR*>(reinterpret_cast<const SQLCHAR*>(line.c_str()));
+
+        SQLRETURN code = SQLExecDirect(statement.GetHandle(), tmp, SQL_NTS);
+
+        if (code != SQL_NO_DATA &&
+            code != SQL_SUCCESS &&
+            code != SQL_SUCCESS_WITH_INFO)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_Database,
+                                          "Cannot execute multi-line SQL:\n" + statement.FormatError());
+        }
+      }
+    }
+  }
+
+
+  IPrecompiledStatement* OdbcDatabase::Compile(const Query& query)
+  {
+    return new OdbcPreparedStatement(GetHandle(), GetDialect(), query);
+  }
+
+    
+  ITransaction* OdbcDatabase::CreateTransaction(TransactionType type)
+  {
+    /**
+     * In ODBC, there is no "START TRANSACTION". A transaction is
+     * automatically created with each connection, and the "READ
+     * ONLY" status can only be set at the statement level
+     * (cf. SQL_CONCUR_READ_ONLY). One can only control the
+     * autocommit: https://stackoverflow.com/a/35351267/881731
+     **/
+    switch (type)
+    {
+      case TransactionType_Implicit:
+        return new OdbcImplicitTransaction(*this);
+          
+      case TransactionType_ReadWrite:
+      case TransactionType_ReadOnly:
+        return new OdbcExplicitTransaction(*this);
+          
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  unsigned int OdbcDatabase::GetDbmsMajorVersion() const
+  {
+    return dbmsMajorVersion_;
+  }    
+
+
+  IDatabaseFactory* OdbcDatabase::CreateDatabaseFactory(unsigned int maxConnectionRetries,
+                                                        unsigned int connectionRetryInterval,
+                                                        const std::string& connectionString,
+                                                        bool checkEncodings)
+  {
+    class Factory : public RetryDatabaseFactory
+    {
+    private:
+      OdbcEnvironment environment_;
+      std::string     connectionString_;
+      bool            checkEncodings_;
+
+      bool LookupConnectionOption(std::string& value,
+                                  const std::string& option) const
+      {
+        std::vector<std::string> tokens;
+        Orthanc::Toolbox::TokenizeString(tokens, connectionString_, ';');
+
+        for (size_t i = 0; i < tokens.size(); i++)
+        {
+          if (boost::starts_with(tokens[i], option + "="))
+          {
+            value = tokens[i];
+            return true;
+          }
+        }
+
+        return false;
+      }
+
+      
+      void CheckMSSQLEncodings(OdbcDatabase& db)
+      {
+        // https://en.wikipedia.org/wiki/History_of_Microsoft_SQL_Server
+        if (db.GetDbmsMajorVersion() <= 14)
+        {
+          // Microsoft SQL Server up to 2017
+
+          std::string value;
+          if (LookupConnectionOption(value, "AutoTranslate"))
+          {
+            if (value != "AutoTranslate=no")
+            {
+              LOG(WARNING) << "For UTF-8 to work properly, it is strongly advised to set \"AutoTranslate=no\" in the "
+                           << "ODBC connection string when connecting to Microsoft SQL Server with version <= 2017";
+            }
+          }
+          else
+          {
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_Database,
+                                            "Your Microsoft SQL Server has version <= 2017, and thus doesn't support UTF-8; "
+                                            "Please upgrade or add \"AutoTranslate=no\" to your ODBC connection string");
+          }
+        }
+        else
+        {
+          std::string value;
+          if (LookupConnectionOption(value, "AutoTranslate") &&
+              value != "AutoTranslate=yes")
+          {
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_Database,
+                                            "Your Microsoft SQL Server has version >= 2019, and thus fully supports UTF-8; "
+                                            "Please set \"AutoTranslate=yes\" in your ODBC connection string");
+          }
+        }
+      }
+
+
+      void CheckMySQLEncodings(OdbcDatabase& db)
+      {
+        // https://dev.mysql.com/doc/connector-odbc/en/connector-odbc-configuration-connection-parameters.html
+
+        std::string value;
+        if (LookupConnectionOption(value, "charset"))
+        {
+          if (value != "charset=utf8")
+          {
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_Database,
+                                            "For compatibility with UTF-8 in Orthanc, your connection string to MySQL "
+                                            "must *not* set the \"charset\" option to another value than \"utf8\"");
+          }
+        }
+        else if (db.GetDbmsMajorVersion() < 8)
+        {
+          // MySQL up to 5.7
+          LOG(WARNING) << "It is advised to set the \"charset=utf8\" option in your connection string if using MySQL <= 5.7";
+        }
+        else
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_Database,
+                                          "For compatibility with UTF-8 in Orthanc, your connection string to MySQL >= 8.0 "
+                                          "*must* set the \"charset=utf8\" in your connection string");
+        }
+      }
+      
+
+    protected:
+      IDatabase* TryOpen()
+      {
+        std::unique_ptr<OdbcDatabase> db(new OdbcDatabase(environment_, connectionString_));
+
+        if (checkEncodings_)
+        {
+          switch (db->GetDialect())
+          {
+            case Dialect_MSSQL:
+              CheckMSSQLEncodings(*db);
+              break;
+
+            case Dialect_MySQL:
+              CheckMySQLEncodings(*db);
+              break;
+
+            case Dialect_SQLite:
+            case Dialect_PostgreSQL:
+              // Nothing specific to be checked wrt. encodings
+              break;
+
+            default:
+              throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+          }
+        }
+
+        if (db->GetDbmsMajorVersion() >= 15)
+        {
+          /**
+           * SQL Server 2019 introduces support for UTF-8. Note that
+           * "ALTER" cannot be run inside a transaction, and must be
+           * done *before* the creation of the tables.
+           * https://docs.microsoft.com/en-US/sql/relational-databases/collations/collation-and-unicode-support#utf8
+           *
+           * Furthermore, this call must be done by both
+           * "odbc-index" and "odbc-storage" plugins, because
+           * altering collation is an operation that requires
+           * exclusive lock: If "odbc-storage" is the first plugin
+           * to be loaded and doesn't set the UTF-8 collation,
+           * "odbc-index" cannot start because it doesn't have
+           * exclusive access.
+           **/
+          db->ExecuteMultiLines("ALTER DATABASE CURRENT COLLATE LATIN1_GENERAL_100_CI_AS_SC_UTF8");
+        }
+        
+        return db.release();
+      }
+      
+    public:
+      Factory(unsigned int maxConnectionRetries,
+              unsigned int connectionRetryInterval,
+              const std::string& connectionString,
+              bool checkEncodings) :
+        RetryDatabaseFactory(maxConnectionRetries, connectionRetryInterval),
+        connectionString_(connectionString),
+        checkEncodings_(checkEncodings)
+      {
+      }
+    };
+
+    return new Factory(maxConnectionRetries, connectionRetryInterval, connectionString, checkEncodings);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Odbc/OdbcDatabase.h	Tue Aug 10 20:08:53 2021 +0200
@@ -0,0 +1,82 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2021 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "OdbcEnvironment.h"
+
+#include "../Common/IDatabaseFactory.h"
+
+#include <Compatibility.h>
+
+#include <set>
+
+
+namespace OrthancDatabases
+{
+  class OdbcDatabase : public IDatabase
+  {
+  private:
+    class OdbcImplicitTransaction;
+    class OdbcExplicitTransaction;
+    
+    SQLHDBC       handle_;
+    Dialect       dialect_;
+    unsigned int  dbmsMajorVersion_;
+
+  public:
+    OdbcDatabase(OdbcEnvironment& environment,
+                 const std::string& connectionString);
+
+    virtual ~OdbcDatabase();
+
+    SQLHDBC GetHandle()
+    {
+      return handle_;
+    }
+
+    std::string FormatError();
+
+    void ListTables(std::set<std::string>& target);
+
+    // "name" must be in lower-case
+    bool DoesTableExist(const std::string& name);
+
+    void ExecuteMultiLines(const std::string& query);
+
+    virtual Dialect GetDialect() const ORTHANC_OVERRIDE
+    {
+      return dialect_;
+    }
+
+    virtual IPrecompiledStatement* Compile(const Query& query) ORTHANC_OVERRIDE;
+
+    virtual ITransaction* CreateTransaction(TransactionType type) ORTHANC_OVERRIDE;
+
+    // https://en.wikipedia.org/wiki/History_of_Microsoft_SQL_Server
+    unsigned int GetDbmsMajorVersion() const;
+
+    static IDatabaseFactory* CreateDatabaseFactory(unsigned int maxConnectionRetries,
+                                                   unsigned int connectionRetryInterval,
+                                                   const std::string& connectionString,
+                                                   bool checkEncodings);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Odbc/OdbcEnvironment.cpp	Tue Aug 10 20:08:53 2021 +0200
@@ -0,0 +1,97 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2021 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "OdbcEnvironment.h"
+
+#include <Logging.h>
+#include <OrthancException.h>
+
+#include <boost/lexical_cast.hpp>
+#include <sqlext.h>
+
+
+namespace OrthancDatabases
+{
+  OdbcEnvironment::OdbcEnvironment()
+  {
+    LOG(INFO) << "Creating the ODBC environment";
+      
+    /* Allocate an environment handle */
+    if (!SQL_SUCCEEDED(SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &handle_)))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_Database,
+                                      "Cannot create ODBC environment");
+    }
+      
+    /* We want ODBC 3 support */
+    if (!SQL_SUCCEEDED(SQLSetEnvAttr(handle_, SQL_ATTR_ODBC_VERSION, (void *) SQL_OV_ODBC3, 0)))
+    {
+      SQLFreeHandle(SQL_HANDLE_ENV, handle_);
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_Database,
+                                      "Your environment doesn't support ODBC 3.x");
+    }
+  }
+
+
+  OdbcEnvironment::~OdbcEnvironment()
+  {
+    LOG(INFO) << "Destructing the ODBC environment";
+      
+    if (!SQL_SUCCEEDED(SQLFreeHandle(SQL_HANDLE_ENV, handle_)))
+    {
+      LOG(ERROR) << "Cannot tear down ODBC environment";
+    }
+  }
+
+
+  std::string OdbcEnvironment::FormatError(SQLHANDLE handle,
+                                           SQLSMALLINT type)
+  {
+    SQLINTEGER   i = 0;
+    SQLINTEGER   native;
+    SQLCHAR      state[SQL_SQLSTATE_SIZE + 1];
+    SQLCHAR      text[256];
+    SQLSMALLINT  len;
+
+    std::string s;
+      
+    for (;;)
+    {
+      SQLRETURN ret = SQLGetDiagRec(type, handle, ++i, state, &native, text, sizeof(text), &len);
+      if (SQL_SUCCEEDED(ret))
+      {
+        if (i >= 2)
+        {
+          s += "\n";
+        }
+          
+        s += (std::string(reinterpret_cast<const char*>(state)) + " : " +
+              boost::lexical_cast<std::string>(i) + "/" +
+              boost::lexical_cast<std::string>(native) + " " +
+              std::string(reinterpret_cast<const char*>(text)));
+      }
+      else
+      {
+        return s;
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Odbc/OdbcEnvironment.h	Tue Aug 10 20:08:53 2021 +0200
@@ -0,0 +1,53 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2021 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#if defined(_WIN32)
+#  include <windows.h>  // Used in "sql.h"
+#endif
+
+#include <boost/noncopyable.hpp>
+#include <sql.h>
+#include <string>
+
+
+namespace OrthancDatabases
+{
+  class OdbcEnvironment : public boost::noncopyable
+  {
+  private:
+    SQLHENV  handle_;
+
+  public:
+    OdbcEnvironment();
+
+    virtual ~OdbcEnvironment();
+
+    SQLHENV GetHandle()
+    {
+      return handle_;
+    }
+
+    static std::string FormatError(SQLHANDLE handle,
+                                   SQLSMALLINT type);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Odbc/OdbcPreparedStatement.cpp	Tue Aug 10 20:08:53 2021 +0200
@@ -0,0 +1,252 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2021 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "OdbcPreparedStatement.h"
+
+#include "../Common/InputFileValue.h"
+#include "../Common/Integer64Value.h"
+#include "../Common/Utf8StringValue.h"
+#include "OdbcResult.h"
+
+#include <Logging.h>
+#include <OrthancException.h>
+
+#include <sqlext.h>
+
+
+namespace OrthancDatabases
+{
+  void OdbcPreparedStatement::Setup(const Query& query)
+  {
+    formatter_.SetNamedDialect(Dialect_MSSQL);  /* ODBC uses "?" to name its parameters */
+    
+    std::string sql;
+    query.Format(sql, formatter_);
+      
+    LOG(INFO) << "Preparing ODBC statement: " << sql;
+    SQLCHAR* s = const_cast<SQLCHAR*>(reinterpret_cast<const SQLCHAR*>(sql.c_str()));
+        
+    if (!SQL_SUCCEEDED(SQLPrepare(statement_.GetHandle(), s, SQL_NTS)))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_Database,
+                                      "Cannot prepare ODBC statement: " + sql);
+    }
+
+    paramsIndex_.resize(formatter_.GetParametersCount());
+
+    size_t countInt64 = 0;
+    size_t countStrings = 0;
+
+    for (size_t i = 0; i < paramsIndex_.size(); i++)
+    {
+      switch (formatter_.GetParameterType(i))
+      {
+        case ValueType_Integer64:
+          paramsIndex_[i] = countInt64;
+          countInt64++;
+          break;
+            
+        case ValueType_InputFile:
+        case ValueType_Utf8String:
+          paramsIndex_[i] = countStrings;
+          countStrings++;
+          break;
+            
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+      }
+    }
+
+    paramsInt64_.resize(countInt64);
+    paramsString_.resize(countStrings);
+  }
+
+
+  OdbcPreparedStatement::OdbcPreparedStatement(SQLHSTMT databaseHandle,
+                                               Dialect dialect,
+                                               const Query& query) :
+    statement_(databaseHandle),
+    formatter_(dialect)
+  {
+    Setup(query);
+  }
+    
+
+  OdbcPreparedStatement::OdbcPreparedStatement(SQLHSTMT databaseHandle,
+                                               Dialect dialect,
+                                               const std::string& sql) :
+    statement_(databaseHandle),
+    formatter_(dialect)
+  {
+    Query query(sql);
+    Setup(query);
+  }
+
+    
+  IResult* OdbcPreparedStatement::Execute()
+  {
+    Dictionary parameters;
+    return Execute(parameters);
+  }
+
+    
+  IResult* OdbcPreparedStatement::Execute(const Dictionary& parameters)
+  {
+    /**
+     * NB: This class creates a copy of all the string parameters from
+     * "parameters", because "SQLBindParameter()" assumes that the
+     * lifetime of the bound values must be larger than the lifetime
+     * of the cursor. This is no problem for the index plugin, but
+     * might be problematic if storing large files in the storage area
+     * (as this doubles RAM requirements).
+     **/
+    
+    for (size_t i = 0; i < formatter_.GetParametersCount(); i++)
+    {
+      const std::string& name = formatter_.GetParameterName(i);
+          
+      if (!parameters.HasKey(name))
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentItem,
+                                        "Missing parameter to SQL prepared statement: " + name);
+      }
+      else 
+      {
+        const IValue& value = parameters.GetValue(name);
+        if (value.GetType() == ValueType_Null)
+        {
+          SQLSMALLINT cType, sqlType;
+              
+          switch (formatter_.GetParameterType(i))
+          {
+            case ValueType_Integer64:
+              cType = SQL_C_SBIGINT;
+              sqlType = SQL_BIGINT;
+              break;
+
+            case ValueType_Utf8String:
+              cType = SQL_C_CHAR;
+              sqlType = SQL_VARCHAR;
+              break;
+
+            default:
+              throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+          }
+
+          SQLLEN null = SQL_NULL_DATA;
+          if (!SQL_SUCCEEDED(SQLBindParameter(statement_.GetHandle(), i + 1, SQL_PARAM_INPUT, cType, sqlType,
+                                              0 /* ignored */, 0 /* ignored */, NULL, 0, &null)))
+          {
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_Database,
+                                            "Cannot bind NULL parameter: " + statement_.FormatError());
+          }
+        }
+        else if (value.GetType() != formatter_.GetParameterType(i))
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadParameterType,
+                                          "Parameter \"" + name + "\" should be of type \"" +
+                                          EnumerationToString(formatter_.GetParameterType(i)) +
+                                          "\", found \"" + EnumerationToString(value.GetType()) + "\"");
+        }
+        else
+        {
+          assert(i < paramsIndex_.size());
+          size_t index = paramsIndex_[i];
+              
+          switch (value.GetType())
+          {
+            case ValueType_Integer64:
+              assert(index < paramsInt64_.size());
+              paramsInt64_[index] = dynamic_cast<const Integer64Value&>(value).GetValue();
+
+              if (!SQL_SUCCEEDED(SQLBindParameter(statement_.GetHandle(), i + 1, SQL_PARAM_INPUT, SQL_C_SBIGINT, SQL_BIGINT,
+                                                  0 /* ignored */, 0 /* ignored */, &paramsInt64_[index],
+                                                  sizeof(int64_t), NULL)))
+              {
+                throw Orthanc::OrthancException(Orthanc::ErrorCode_Database,
+                                                "Cannot bind integer parameter: " + statement_.FormatError());
+              }
+                                                  
+              break;
+
+            case ValueType_Utf8String:
+            {
+              assert(index < paramsString_.size());
+              paramsString_[index] = dynamic_cast<const Utf8StringValue&>(value).GetContent();
+
+              const char* content = (paramsString_[index].empty() ? "" : paramsString_[index].c_str());
+
+              if (!SQL_SUCCEEDED(SQLBindParameter(
+                                   statement_.GetHandle(), i + 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR,
+                                   0 /* ignored */, 0 /* ignored */, const_cast<char*>(content), paramsString_[index].size(), NULL)))
+              {
+                throw Orthanc::OrthancException(Orthanc::ErrorCode_Database,
+                                                "Cannot bind UTF-8 parameter: " + statement_.FormatError());
+              }
+                                                  
+              break;
+            }
+
+            case ValueType_InputFile:
+            {
+              assert(index < paramsString_.size());
+              paramsString_[index] = dynamic_cast<const InputFileValue&>(value).GetContent();
+
+              const char* content = (paramsString_[index].empty() ? NULL : paramsString_[index].c_str());
+
+              SQLLEN a = paramsString_[index].size();
+              if (!SQL_SUCCEEDED(SQLBindParameter(
+                                   statement_.GetHandle(), i + 1, SQL_PARAM_INPUT, SQL_C_BINARY, SQL_LONGVARBINARY,
+                                   paramsString_[index].size() /* only used by MSSQL */,
+                                   0 /* ignored */, const_cast<char*>(content), 0, &a)))
+              {
+                throw Orthanc::OrthancException(Orthanc::ErrorCode_Database,
+                                                "Cannot bind binary parameter: " + statement_.FormatError());
+              }
+
+              break;
+            }
+
+            default:
+              throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+          }
+        }
+      }
+    }
+
+    const Dialect dialect = formatter_.GetAutoincrementDialect();
+    
+    SQLRETURN code = SQLExecute(statement_.GetHandle());
+    
+    if (code == SQL_SUCCESS ||
+        code == SQL_NO_DATA /* this is the case of DELETE in PostgreSQL and MSSQL */)
+    {          
+      return new OdbcResult(statement_, dialect);
+    }
+    else
+    {
+      statement_.CheckCollision(dialect);
+
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_Database,
+                                      "Cannot execute ODBC statement:\n" + statement_.FormatError());
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Odbc/OdbcPreparedStatement.h	Tue Aug 10 20:08:53 2021 +0200
@@ -0,0 +1,58 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2021 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "OdbcStatement.h"
+
+#include "../Common/Dictionary.h"
+#include "../Common/GenericFormatter.h"
+#include "../Common/IPrecompiledStatement.h"
+#include "../Common/IResult.h"
+
+
+namespace OrthancDatabases
+{
+  class OdbcPreparedStatement : public IPrecompiledStatement
+  {
+  private:
+    OdbcStatement             statement_;
+    GenericFormatter          formatter_;
+    std::vector<int64_t>      paramsInt64_;
+    std::vector<std::string>  paramsString_;
+    std::vector<size_t>       paramsIndex_;
+
+    void Setup(const Query& query);
+    
+  public:      
+    OdbcPreparedStatement(SQLHSTMT databaseHandle,
+                          Dialect dialect,
+                          const Query& query);
+
+    OdbcPreparedStatement(SQLHSTMT databaseHandle,
+                          Dialect dialect,
+                          const std::string& sql);
+
+    IResult* Execute();
+
+    IResult* Execute(const Dictionary& parameters);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Odbc/OdbcResult.cpp	Tue Aug 10 20:08:53 2021 +0200
@@ -0,0 +1,393 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2021 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "OdbcResult.h"
+
+#include "../Common/BinaryStringValue.h"
+#include "../Common/Integer64Value.h"
+#include "../Common/NullValue.h"
+#include "../Common/Utf8StringValue.h"
+
+#include <ChunkedBuffer.h>
+#include <Logging.h>
+#include <OrthancException.h>
+#include <Toolbox.h>
+
+#include <boost/lexical_cast.hpp>
+#include <sqlext.h>
+
+
+namespace OrthancDatabases
+{
+  static void ThrowCannotReadString()
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_Database, "Cannot read text field");
+  }
+
+
+  void OdbcResult::LoadFirst()
+  {
+    if (first_)
+    {
+      Next();
+      first_ = false;
+    }
+  }
+
+    
+  void OdbcResult::SetValue(size_t index,
+                            IValue* value)
+  {
+    std::unique_ptr<IValue> raii(value);
+        
+    if (index >= values_.size())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+    else if (value == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+    else
+    {
+      if (values_[index] != NULL)
+      {
+        delete values_[index];
+      }
+          
+      values_[index] = raii.release();
+    }
+  }
+      
+
+  void OdbcResult::ReadString(std::string& target,
+                              size_t column,
+                              bool isBinary)
+  {
+    // https://docs.microsoft.com/en-us/sql/odbc/reference/develop-app/getting-long-data
+
+    std::string buffer;
+    buffer.resize(1024 * 1024);
+
+    const SQLSMALLINT targetType = (isBinary ? SQL_BINARY : SQL_C_CHAR);
+
+    SQLLEN length;
+    SQLRETURN code = SQLGetData(statement_.GetHandle(), column + 1, targetType, &buffer[0], buffer.size(), &length);
+    if (code == SQL_NO_DATA)
+    {
+      target.clear();
+    }
+    else if (code == SQL_SUCCESS)
+    {
+      if (length == -1)
+      {
+        target.clear();  // No data available
+      }
+      else
+      {
+        // The "buffer" was large enough to store the text value, plus the null termination
+        target.assign(buffer.c_str(), length);
+      }
+    }
+    else if (code == SQL_SUCCESS_WITH_INFO)
+    {
+      Orthanc::ChunkedBuffer chunks;
+      
+      if (isBinary)
+      {
+        chunks.AddChunk(buffer.c_str(), buffer.size());
+      }
+      else
+      {
+        /**
+         * WARNING: At this point, in the MSSQL driver, "length"
+         * contains the number of Unicode characters! This is
+         * different from the actual number of **bytes** that are
+         * required to store the UTF-8 string. As a consequence, the
+         * "length" cannot be used to determine the final size of
+         * the "target" string.
+         **/
+        chunks.AddChunk(buffer.c_str(), buffer.size() - 1);
+      }
+
+      for (;;)
+      {
+        code = SQLGetData(statement_.GetHandle(), column + 1, targetType, &buffer[0], buffer.size(), &length);
+            
+        if (code == SQL_SUCCESS)
+        {
+          // This is the last chunk
+          if (length == 0 ||
+              length > static_cast<SQLLEN>(buffer.size()))
+          {
+            ThrowCannotReadString();
+          }
+                  
+          chunks.AddChunk(buffer.c_str(), length);
+          break;
+        }
+        else if (code == SQL_SUCCESS_WITH_INFO)
+        {
+          // This is an intermediate chunk
+          if (isBinary)
+          {
+            chunks.AddChunk(buffer.c_str(), buffer.size());
+          }
+          else
+          {
+            chunks.AddChunk(buffer.c_str(), buffer.size() - 1);
+          }
+        }
+        else
+        {
+          ThrowCannotReadString();
+        }
+      }
+          
+      chunks.Flatten(target);
+    }
+    else
+    {
+      statement_.CheckCollision(dialect_);
+      ThrowCannotReadString();
+    }
+  }
+      
+
+  OdbcResult::OdbcResult(OdbcStatement& statement,
+                         Dialect dialect) :
+    statement_(statement),
+    dialect_(dialect),
+    first_(true),
+    done_(false)
+  {
+    SQLSMALLINT count;
+    if (!SQL_SUCCEEDED(SQLNumResultCols(statement_.GetHandle(), &count)))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_Database);
+    }
+
+    types_.resize(count);
+    typeNames_.resize(count);
+    values_.resize(count);
+        
+    for (size_t i = 0; i < values_.size(); i++)
+    {
+      /**
+       * NB: Don't use "SQLDescribeCol()", as it is less flexible
+       * (cf. OMSSQL-7: "SQLDescribeParam()" doesn't work with
+       * encrypted columns)
+       **/
+          
+      if (!SQL_SUCCEEDED(SQLColAttribute(statement_.GetHandle(), i + 1, SQL_DESC_TYPE, NULL, -1, NULL, &types_[i])))
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_Database);
+      }
+
+      SQLCHAR buffer[1024];
+      SQLSMALLINT length;
+
+      if (!SQL_SUCCEEDED(SQLColAttribute(statement_.GetHandle(), i + 1, SQL_DESC_TYPE_NAME,
+                                         buffer, sizeof(buffer) - 1, &length, NULL)))
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_Database);
+      }
+
+      std::string name(reinterpret_cast<const char*>(buffer), length);
+      Orthanc::Toolbox::ToLowerCase(typeNames_[i], name);
+    }
+  }
+
+    
+  OdbcResult::~OdbcResult()
+  {
+    for (size_t i = 0; i < values_.size(); i++)
+    {
+      if (values_[i] != NULL)
+      {
+        delete values_[i];
+      }
+    }
+
+    if (!first_ &&
+        !SQL_SUCCEEDED(SQLCloseCursor(statement_.GetHandle())))
+    {
+      LOG(WARNING) << "Cannot close the ODBC cursor: " << std::endl << statement_.FormatError();
+    }
+  }
+    
+      
+  void OdbcResult::SetExpectedType(size_t field,
+                                   ValueType type) 
+  {
+    if (field >= types_.size())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      // Ignore this information
+    }
+  }
+
+
+  bool OdbcResult::IsDone() const 
+  {
+    const_cast<OdbcResult&>(*this).LoadFirst();
+    return done_;
+  }
+    
+
+  void OdbcResult::Next() 
+  {
+    SQLRETURN code = SQLFetch(statement_.GetHandle());
+
+    if (code == SQL_NO_DATA)
+    {
+      done_ = true;
+    }
+    else if (code == SQL_SUCCESS)
+    {
+      done_ = false;
+    }
+    else
+    {
+      statement_.CheckCollision(dialect_);
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_Database, "Cannot fetch new row");
+    }
+
+    assert(values_.size() == types_.size() &&
+           values_.size() == typeNames_.size());
+
+    for (size_t i = 0; i < values_.size(); i++)
+    {
+      SQLLEN type = types_[i];
+      const std::string& name = typeNames_[i];
+          
+      if (done_)
+      {
+        SetValue(i, new NullValue);
+      }
+      else if (type == SQL_INTEGER)
+      {
+        int32_t value;
+        SQLLEN length;
+        if (SQL_SUCCEEDED(SQLGetData(statement_.GetHandle(), i + 1, SQL_INTEGER, &value, sizeof(value), &length)))
+        {
+          if (length == SQL_NULL_DATA)
+          {
+            SetValue(i, new NullValue);
+          }
+          else
+          {
+            SetValue(i, new Integer64Value(value));
+          }
+        }
+        else
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_Database, "Cannot get int32_t field");
+        }
+      }
+      else if (type == SQL_BIGINT ||
+               (dialect_ == Dialect_PostgreSQL && name == "bigserial"))
+      {
+        int64_t value;
+        SQLLEN length;
+        if (SQL_SUCCEEDED(SQLGetData(statement_.GetHandle(), i + 1, SQL_C_SBIGINT, &value, sizeof(value), &length)))
+        {
+          if (length == SQL_NULL_DATA)
+          {
+            SetValue(i, new NullValue);
+          }
+          else
+          {
+            SetValue(i, new Integer64Value(value));
+          }
+        }
+        else
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_Database, "Cannot get int64_t field");
+        }
+      }
+      else if (type == SQL_VARCHAR ||
+               name == "varchar" ||
+               (dialect_ == Dialect_MSSQL && name == "nvarchar") ||  // This means UTF-16
+               (dialect_ == Dialect_MySQL && name == "longtext") ||
+               (dialect_ == Dialect_MySQL && name.empty() && type == -9) ||  // Seen in "SQLTables()"
+               (dialect_ == Dialect_PostgreSQL && name == "text") ||
+               (dialect_ == Dialect_SQLite && name == "text") ||
+               (dialect_ == Dialect_SQLite && name == "wvarchar"))  // Seen on Windows with sqliteodbc-0.9998-win32.exe
+      {
+        std::string value;
+        ReadString(value, i, false /* not binary */);
+        SetValue(i, new Utf8StringValue(value));
+      }
+      else if (type == SQL_NUMERIC)
+      {
+        /**
+         * SQL_NUMERIC_STRUCT could be used here, but is much more
+         * complex to deal with:
+         * https://stackoverflow.com/a/9188737/881731
+         **/
+        
+        std::string value;
+        ReadString(value, i, false /* not binary */);
+        SetValue(i, new Integer64Value(boost::lexical_cast<int64_t>(value)));
+      }
+      else if (type == SQL_BINARY ||
+               (dialect_ == Dialect_PostgreSQL && name == "bytea") ||
+               (dialect_ == Dialect_MySQL && name == "longblob") ||
+               (dialect_ == Dialect_MSSQL && name == "varbinary"))
+      {
+        std::string value;
+        ReadString(value, i, true /* binary */);
+        SetValue(i, new BinaryStringValue(value));
+      }
+      else
+      {
+        throw Orthanc::OrthancException(
+          Orthanc::ErrorCode_NotImplemented,
+          "Unknown type in result: " + name + " (" + boost::lexical_cast<std::string>(type) + ")");
+      }
+    }
+  }
+
+
+  const IValue& OdbcResult::GetField(size_t field) const 
+  {
+    if (field >= values_.size())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+    else if (IsDone())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+    else if (values_[field] == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+    else
+    {
+      return *values_[field];
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Odbc/OdbcResult.h	Tue Aug 10 20:08:53 2021 +0200
@@ -0,0 +1,76 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2021 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "OdbcStatement.h"
+
+#include "../Common/IResult.h"
+
+#include <Compatibility.h>
+
+#include <sqltypes.h>
+#include <vector>
+
+
+namespace OrthancDatabases
+{
+  class OdbcResult : public IResult
+  {
+  private:
+    OdbcStatement&            statement_;
+    Dialect                   dialect_;
+    bool                      first_;
+    bool                      done_;
+    std::vector<SQLLEN>       types_;
+    std::vector<std::string>  typeNames_;
+    std::vector<IValue*>      values_;
+
+    void LoadFirst();
+
+    void SetValue(size_t index,
+                  IValue* value);
+
+    void ReadString(std::string& target,
+                    size_t column,
+                    bool isBinary);
+    
+  public:
+    OdbcResult(OdbcStatement& statement,
+               Dialect dialect);
+    
+    virtual ~OdbcResult();
+      
+    virtual void SetExpectedType(size_t field,
+                                 ValueType type) ORTHANC_OVERRIDE;
+
+    virtual bool IsDone() const ORTHANC_OVERRIDE;
+
+    virtual void Next() ORTHANC_OVERRIDE;
+    
+    virtual size_t GetFieldsCount() const ORTHANC_OVERRIDE
+    {
+      return values_.size();
+    }        
+
+    virtual const IValue& GetField(size_t field) const ORTHANC_OVERRIDE;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Odbc/OdbcStatement.cpp	Tue Aug 10 20:08:53 2021 +0200
@@ -0,0 +1,86 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2021 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "OdbcStatement.h"
+
+#include "OdbcEnvironment.h"
+
+#include <Logging.h>
+#include <OrthancException.h>
+
+#include <sqlext.h>
+
+
+namespace OrthancDatabases
+{
+  OdbcStatement::OdbcStatement(SQLHSTMT databaseHandle)
+  {
+    if (!SQL_SUCCEEDED(SQLAllocHandle(SQL_HANDLE_STMT, databaseHandle, &handle_)))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_Database, "Cannot allocate statement");          
+    }
+  }
+
+  
+  OdbcStatement::~OdbcStatement()
+  {
+    if (!SQL_SUCCEEDED(SQLFreeHandle(SQL_HANDLE_STMT, handle_)))
+    {
+      LOG(ERROR) << "Cannot destruct statement";
+    }
+  }
+
+  
+  std::string OdbcStatement::FormatError()
+  {
+    return OdbcEnvironment::FormatError(handle_, SQL_HANDLE_STMT);
+  }
+
+
+  void OdbcStatement::CheckCollision(Dialect dialect)
+  {
+    SQLINTEGER native = -1;
+    SQLCHAR stateBuf[SQL_SQLSTATE_SIZE + 1];
+    SQLSMALLINT stateLength = 0;
+
+    for (SQLSMALLINT recNum = 1; ; recNum++)
+    {
+      if (SQL_SUCCEEDED(SQLGetDiagField(SQL_HANDLE_STMT, handle_,
+                                        recNum, SQL_DIAG_NATIVE, &native, SQL_IS_INTEGER, 0)) &&
+          SQL_SUCCEEDED(SQLGetDiagField(SQL_HANDLE_STMT, handle_,
+                                        recNum, SQL_DIAG_SQLSTATE, &stateBuf, sizeof(stateBuf), &stateLength)))
+      {
+        const std::string state(reinterpret_cast<const char*>(stateBuf));
+          
+        if (state == "40001" ||
+            (dialect == Dialect_MySQL && native == 1213) ||
+            (dialect == Dialect_MSSQL && native == 1205))
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_DatabaseCannotSerialize);
+        }
+      }
+      else
+      {
+        return;
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Odbc/OdbcStatement.h	Tue Aug 10 20:08:53 2021 +0200
@@ -0,0 +1,56 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2021 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#if defined(_WIN32)
+#  include <windows.h>  // Used in "sql.h"
+#endif
+
+#include "../Common/DatabasesEnumerations.h"
+
+#include <boost/noncopyable.hpp>
+#include <sql.h>
+#include <string>
+
+
+namespace OrthancDatabases
+{
+  class OdbcStatement : public boost::noncopyable
+  {
+  private:
+    SQLHSTMT handle_;
+      
+  public:
+    OdbcStatement(SQLHSTMT databaseHandle);
+
+    ~OdbcStatement();
+
+    SQLHSTMT GetHandle()
+    {
+      return handle_;
+    }
+      
+    std::string FormatError();
+
+    void CheckCollision(Dialect dialect);
+  };
+}
--- a/MySQL/CMakeLists.txt	Thu Jul 22 20:20:26 2021 +0200
+++ b/MySQL/CMakeLists.txt	Tue Aug 10 20:08:53 2021 +0200
@@ -1,3 +1,22 @@
+# Orthanc - A Lightweight, RESTful DICOM Store
+# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+# Department, University Hospital of Liege, Belgium
+# Copyright (C) 2017-2021 Osimis S.A., Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the GNU Affero General Public License
+# as published by the Free Software Foundation, either version 3 of
+# the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
 cmake_minimum_required(VERSION 2.8)
 project(OrthancMySQL)
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Odbc/CMakeLists.txt	Tue Aug 10 20:08:53 2021 +0200
@@ -0,0 +1,158 @@
+# Orthanc - A Lightweight, RESTful DICOM Store
+# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+# Department, University Hospital of Liege, Belgium
+# Copyright (C) 2017-2021 Osimis S.A., Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the GNU Affero General Public License
+# as published by the Free Software Foundation, either version 3 of
+# the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+cmake_minimum_required(VERSION 2.8)
+project(OrthancODBC)
+
+set(ORTHANC_PLUGIN_VERSION "mainline")
+
+set(ORTHANC_OPTIMAL_VERSION_MAJOR    1)
+set(ORTHANC_OPTIMAL_VERSION_MINOR    9)
+set(ORTHANC_OPTIMAL_VERSION_REVISION 2)
+
+if (ORTHANC_PLUGIN_VERSION STREQUAL "mainline")
+  set(ORTHANC_FRAMEWORK_VERSION "mainline")
+  set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "hg")
+else()
+  set(ORTHANC_FRAMEWORK_VERSION "1.9.6")
+  set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "web")
+endif()
+
+include(${CMAKE_SOURCE_DIR}/../Resources/CMake/DatabasesPluginParameters.cmake)
+
+set(ENABLE_ODBC_BACKEND ON)
+
+include(${CMAKE_SOURCE_DIR}/../Resources/CMake/DatabasesPluginConfiguration.cmake)
+
+
+if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
+  execute_process(
+    COMMAND 
+    ${PYTHON_EXECUTABLE} ${ORTHANC_FRAMEWORK_ROOT}/../Resources/WindowsResources.py
+    ${ORTHANC_PLUGIN_VERSION} "ODBC storage area plugin" OrthancODBCStorage.dll
+    "ODBC as a database back-end to Orthanc (storage area)"
+    ERROR_VARIABLE Failure
+    OUTPUT_FILE ${AUTOGENERATED_DIR}/StorageVersion.rc
+    )
+
+  if (Failure)
+    message(FATAL_ERROR "Error while computing the version information: ${Failure}")
+  endif()
+
+  execute_process(
+    COMMAND 
+    ${PYTHON_EXECUTABLE} ${ORTHANC_FRAMEWORK_ROOT}/../Resources/WindowsResources.py
+    ${ORTHANC_PLUGIN_VERSION} "ODBC index plugin" OrthancODBCIndex.dll
+    "ODBC as a database back-end to Orthanc (index)"
+    ERROR_VARIABLE Failure
+    OUTPUT_FILE ${AUTOGENERATED_DIR}/IndexVersion.rc
+    )
+
+  if (Failure)
+    message(FATAL_ERROR "Error while computing the version information: ${Failure}")
+  endif()
+
+  set(INDEX_RESOURCES ${AUTOGENERATED_DIR}/IndexVersion.rc)
+  set(STORAGE_RESOURCES ${AUTOGENERATED_DIR}/StorageVersion.rc)
+endif()
+
+
+EmbedResources(
+  ODBC_PREPARE_INDEX    ${CMAKE_SOURCE_DIR}/Plugins/PrepareIndex.sql
+  ODBC_PREPARE_STORAGE  ${CMAKE_SOURCE_DIR}/Plugins/PrepareStorage.sql
+  )
+
+add_custom_target(
+  AutogeneratedTarget
+  DEPENDS 
+  ${AUTOGENERATED_SOURCES}
+  )
+
+add_library(FrameworkForPlugins STATIC
+  ${AUTOGENERATED_SOURCES}
+  ${DATABASES_SOURCES}
+  ${LTDL_SOURCES}
+  ${UNIX_ODBC_SOURCES}
+
+  ${ORTHANC_DATABASES_ROOT}/Framework/Plugins/PluginInitialization.cpp
+  Plugins/OdbcIndex.cpp
+  )
+
+set_target_properties(FrameworkForPlugins PROPERTIES
+  POSITION_INDEPENDENT_CODE ON
+  COMPILE_FLAGS -DORTHANC_ENABLE_LOGGING_PLUGIN=1
+  )
+
+add_library(OrthancODBCIndex SHARED
+  ${INDEX_RESOURCES}
+  Plugins/IndexPlugin.cpp
+  )
+
+add_library(OrthancODBCStorage SHARED
+  ${STORAGE_RESOURCES}
+  Plugins/StoragePlugin.cpp
+  )
+
+add_dependencies(FrameworkForPlugins AutogeneratedTarget)
+
+target_link_libraries(OrthancODBCIndex FrameworkForPlugins)
+target_link_libraries(OrthancODBCStorage FrameworkForPlugins)
+
+message("Setting the version of the libraries to ${ORTHANC_PLUGIN_VERSION}")
+
+add_definitions(
+  -DORTHANC_PLUGIN_VERSION="${ORTHANC_PLUGIN_VERSION}"
+  )
+
+set_target_properties(OrthancODBCStorage PROPERTIES 
+  VERSION ${ORTHANC_PLUGIN_VERSION} 
+  SOVERSION ${ORTHANC_PLUGIN_VERSION}
+  COMPILE_FLAGS -DORTHANC_ENABLE_LOGGING_PLUGIN=1
+  )
+
+set_target_properties(OrthancODBCIndex PROPERTIES 
+  VERSION ${ORTHANC_PLUGIN_VERSION} 
+  SOVERSION ${ORTHANC_PLUGIN_VERSION}
+  COMPILE_FLAGS -DORTHANC_ENABLE_LOGGING_PLUGIN=1
+  )
+
+install(
+  TARGETS OrthancODBCIndex OrthancODBCStorage
+  RUNTIME DESTINATION lib    # Destination for Windows
+  LIBRARY DESTINATION share/orthanc/plugins    # Destination for Linux
+  )
+
+
+add_executable(UnitTests
+  ${AUTOGENERATED_SOURCES}
+  ${DATABASES_SOURCES}
+  ${GOOGLE_TEST_SOURCES}
+  ${LTDL_SOURCES}
+  ${UNIX_ODBC_SOURCES}
+
+  Plugins/OdbcIndex.cpp
+  UnitTests/UnitTestsMain.cpp
+  )
+
+add_dependencies(UnitTests AutogeneratedTarget)
+
+target_link_libraries(UnitTests ${GOOGLE_TEST_LIBRARIES})
+set_target_properties(UnitTests PROPERTIES
+  COMPILE_FLAGS -DORTHANC_ENABLE_LOGGING_PLUGIN=0
+  )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Odbc/Plugins/IndexPlugin.cpp	Tue Aug 10 20:08:53 2021 +0200
@@ -0,0 +1,138 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2021 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "OdbcIndex.h"
+
+#include "../../Framework/Plugins/PluginInitialization.h"
+#include "../../Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h"
+
+#include <Logging.h>
+
+
+#if defined(_WIN32)
+#  warning Strings have not been tested on Windows (UTF-16 issues ahead)!
+#  include <windows.h>
+#else
+#  include <ltdl.h>
+#  include <libltdl/lt_dlloader.h>
+#endif
+
+
+static const char* const KEY_ODBC = "Odbc";
+
+
+extern "C"
+{
+#if !defined(_WIN32)
+  extern lt_dlvtable *dlopen_LTX_get_vtable(lt_user_data loader_data);
+#endif
+
+  
+  ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context)
+  {
+    if (!OrthancDatabases::InitializePlugin(context, "ODBC", true))
+    {
+      return -1;
+    }
+
+#if !defined(_WIN32)
+    lt_dlinit();
+    
+    /**
+     * The following call is necessary for "libltdl" to access the
+     * "dlopen()" primitives if statically linking. Otherwise, only the
+     * "preopen" primitives are available.
+     **/
+    lt_dlloader_add(dlopen_LTX_get_vtable(NULL));
+#endif
+    
+    OrthancPlugins::OrthancConfiguration configuration;
+
+    if (!configuration.IsSection(KEY_ODBC))
+    {
+      LOG(WARNING) << "No available configuration for the ODBC index plugin";
+      return 0;
+    }
+
+    OrthancPlugins::OrthancConfiguration odbc;
+    configuration.GetSection(odbc, KEY_ODBC);
+
+    bool enable;
+    if (!odbc.LookupBooleanValue(enable, "EnableIndex") ||
+        !enable)
+    {
+      LOG(WARNING) << "The ODBC index is currently disabled, set \"EnableIndex\" "
+                   << "to \"true\" in the \"" << KEY_ODBC << "\" section of the configuration file of Orthanc";
+      return 0;
+    }
+
+    try
+    {
+      const std::string connectionString = odbc.GetStringValue("IndexConnectionString", "");
+      const unsigned int countConnections = odbc.GetUnsignedIntegerValue("IndexConnectionsCount", 1);
+      const unsigned int maxConnectionRetries = odbc.GetUnsignedIntegerValue("MaxConnectionRetries", 10);
+      const unsigned int connectionRetryInterval = odbc.GetUnsignedIntegerValue("ConnectionRetryInterval", 5);
+
+      if (connectionString.empty())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange,
+                                        "No connection string provided for the ODBC index");
+      }
+
+      std::unique_ptr<OrthancDatabases::OdbcIndex> index(new OrthancDatabases::OdbcIndex(context, connectionString));
+      index->SetMaxConnectionRetries(maxConnectionRetries);
+      index->SetConnectionRetryInterval(connectionRetryInterval);
+
+      OrthancDatabases::IndexBackend::Register(index.release(), countConnections, maxConnectionRetries);
+    }
+    catch (Orthanc::OrthancException& e)
+    {
+      LOG(ERROR) << e.What();
+      return -1;
+    }
+    catch (...)
+    {
+      LOG(ERROR) << "Native exception while initializing the plugin";
+      return -1;
+    }
+
+    return 0;
+  }
+
+
+  ORTHANC_PLUGINS_API void OrthancPluginFinalize()
+  {
+    LOG(WARNING) << "ODBC index is finalizing";
+    OrthancDatabases::IndexBackend::Finalize();
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
+  {
+    return "odbc-index";
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
+  {
+    return ORTHANC_PLUGIN_VERSION;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Odbc/Plugins/OdbcIndex.cpp	Tue Aug 10 20:08:53 2021 +0200
@@ -0,0 +1,694 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2021 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "OdbcIndex.h"
+
+#include "../../Framework/Common/Integer64Value.h"
+#include "../../Framework/Odbc/OdbcDatabase.h"
+#include "../../Framework/Plugins/GlobalProperties.h"
+
+#include <EmbeddedResources.h>  // Autogenerated file
+
+#include <Logging.h>
+#include <OrthancException.h>
+#include <Toolbox.h>
+
+#include <boost/algorithm/string/replace.hpp>
+
+
+// Some aliases for internal properties
+static const Orthanc::GlobalProperty GlobalProperty_LastChange = Orthanc::GlobalProperty_DatabaseInternal0;
+
+
+namespace OrthancDatabases
+{
+  static int64_t GetSQLiteLastInsert(DatabaseManager& manager)
+  {
+    DatabaseManager::CachedStatement statement(
+      STATEMENT_FROM_HERE, manager, "SELECT LAST_INSERT_ROWID()");
+    
+    statement.Execute();
+    
+    return statement.ReadInteger64(0);
+  }
+  
+  
+  static int64_t GetMySQLLastInsert(DatabaseManager& manager)
+  {
+    DatabaseManager::CachedStatement statement(
+      STATEMENT_FROM_HERE, manager, "SELECT LAST_INSERT_ID()");
+    
+    statement.Execute();
+    
+    return statement.ReadInteger64(0);
+  }
+  
+  
+  static int64_t GetMSSQLLastInsert(DatabaseManager& manager)
+  {
+    DatabaseManager::CachedStatement statement(
+      STATEMENT_FROM_HERE, manager, "SELECT @@IDENTITY");
+    
+    statement.Execute();
+    
+    return statement.ReadInteger64(0);
+  }
+  
+  
+  static void AddPatientToRecyclingOrder(DatabaseManager& manager,
+                                         int64_t patient)
+  {
+    // In the other database plugins, this is done with a trigger
+
+    std::unique_ptr<DatabaseManager::CachedStatement> statement;
+
+    switch (manager.GetDialect())
+    {
+      case Dialect_SQLite:
+      case Dialect_MySQL:
+        statement.reset(
+          new DatabaseManager::CachedStatement(
+            STATEMENT_FROM_HERE, manager, "INSERT INTO PatientRecyclingOrder VALUES(NULL, ${patient})"));
+        break;
+        
+      case Dialect_PostgreSQL:
+        statement.reset(
+          new DatabaseManager::CachedStatement(
+            STATEMENT_FROM_HERE, manager, "INSERT INTO PatientRecyclingOrder VALUES(DEFAULT, ${patient})"));
+        break;
+        
+      case Dialect_MSSQL:
+        statement.reset(
+          new DatabaseManager::CachedStatement(
+            STATEMENT_FROM_HERE, manager, "INSERT INTO PatientRecyclingOrder VALUES(${patient})"));
+        break;
+        
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    statement->SetParameterType("patient", ValueType_Integer64);
+
+    Dictionary args;
+    args.SetIntegerValue("patient", patient);
+    statement->Execute(args);
+  }
+
+
+  static OrthancPluginResourceType GetParentType(OrthancPluginResourceType level)
+  {
+    switch (level)
+    {
+      case OrthancPluginResourceType_Study:
+        return OrthancPluginResourceType_Patient;
+
+      case OrthancPluginResourceType_Series:
+        return OrthancPluginResourceType_Study;
+
+      case OrthancPluginResourceType_Instance:
+        return OrthancPluginResourceType_Series;
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  OdbcIndex::OdbcIndex(OrthancPluginContext* context,
+                       const std::string& connectionString) :
+    IndexBackend(context),
+    maxConnectionRetries_(10),
+    connectionRetryInterval_(5),
+    connectionString_(connectionString)
+  {
+  }
+
+  
+  void OdbcIndex::SetConnectionRetryInterval(unsigned int seconds)
+  {
+    if (seconds == 0)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      connectionRetryInterval_ = seconds;
+    }
+  }
+
+
+  IDatabaseFactory* OdbcIndex::CreateDatabaseFactory()
+  {
+    return OdbcDatabase::CreateDatabaseFactory(maxConnectionRetries_, connectionRetryInterval_, connectionString_, true);
+  }
+  
+  
+  void OdbcIndex::ConfigureDatabase(DatabaseManager& manager)
+  {
+    uint32_t expectedVersion = 6;
+    
+    if (GetContext())   // "GetContext()" can possibly be NULL in the unit tests
+    {
+      expectedVersion = OrthancPluginGetExpectedDatabaseVersion(GetContext());
+    }
+
+    // Check the expected version of the database
+    if (expectedVersion != 6)
+    {
+      LOG(ERROR) << "This database plugin is incompatible with your version of Orthanc "
+                 << "expecting the DB schema version " << expectedVersion
+                 << ", but this plugin is only compatible with version 6";
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_Plugin);
+    }
+
+    OdbcDatabase& db = dynamic_cast<OdbcDatabase&>(manager.GetDatabase());
+
+    if (!db.DoesTableExist("resources"))
+    {
+      std::string sql;
+      Orthanc::EmbeddedResources::GetFileResource(sql, Orthanc::EmbeddedResources::ODBC_PREPARE_INDEX);
+
+      switch (db.GetDialect())
+      {
+        case Dialect_SQLite:
+          boost::replace_all(sql, "${LONGTEXT}", "TEXT");
+          boost::replace_all(sql, "${AUTOINCREMENT_TYPE}", "INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT");
+          boost::replace_all(sql, "${AUTOINCREMENT_INSERT}", "NULL, ");
+          break;
+        
+        case Dialect_PostgreSQL:
+          boost::replace_all(sql, "${LONGTEXT}", "TEXT");
+          boost::replace_all(sql, "${AUTOINCREMENT_TYPE}", "BIGSERIAL NOT NULL PRIMARY KEY");
+          boost::replace_all(sql, "${AUTOINCREMENT_INSERT}", "DEFAULT, ");
+          break;
+        
+        case Dialect_MySQL:
+          boost::replace_all(sql, "${LONGTEXT}", "LONGTEXT");
+          boost::replace_all(sql, "${AUTOINCREMENT_TYPE}", "BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY");
+          boost::replace_all(sql, "${AUTOINCREMENT_INSERT}", "NULL, ");
+          break;
+
+        case Dialect_MSSQL:
+          /**
+           * cf. OMSSQL-5: Use VARCHAR(MAX) instead of TEXT: (1)
+           * Microsoft issued a warning stating that "ntext, text, and
+           * image data types will be removed in a future version of
+           * SQL Server"
+           * (https://msdn.microsoft.com/en-us/library/ms187993.aspx),
+           * and (2) SQL Server does not support comparison of TEXT
+           * with '=' operator (e.g. in WHERE statements such as
+           * IndexBackend::LookupIdentifier())."
+           **/
+          boost::replace_all(sql, "${LONGTEXT}", "VARCHAR(MAX)");
+          boost::replace_all(sql, "${AUTOINCREMENT_TYPE}", "BIGINT IDENTITY NOT NULL PRIMARY KEY");
+          boost::replace_all(sql, "${AUTOINCREMENT_INSERT}", "");
+          break;
+
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+      }
+
+      {
+        DatabaseManager::Transaction t(manager, TransactionType_ReadWrite);
+
+        db.ExecuteMultiLines(sql);
+
+        if (db.GetDialect() == Dialect_MySQL)
+        {
+          // Switch to the collation that is the default since MySQL
+          // 8.0.1. This must be *after* the creation of the tables.
+          db.ExecuteMultiLines("ALTER DATABASE CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
+        }
+
+        t.Commit();
+      }
+    }
+  }
+
+  
+  int64_t OdbcIndex::CreateResource(DatabaseManager& manager,
+                                    const char* publicId,
+                                    OrthancPluginResourceType type)
+  {
+    Dictionary args;
+    args.SetUtf8Value("id", publicId);
+    args.SetIntegerValue("type", static_cast<int>(type));
+    
+    switch (manager.GetDatabase().GetDialect())
+    {
+      case Dialect_SQLite:
+      {
+        {
+          DatabaseManager::CachedStatement statement(
+            STATEMENT_FROM_HERE, manager, "INSERT INTO Resources VALUES(NULL, ${type}, ${id}, NULL)");
+          
+          statement.SetParameterType("id", ValueType_Utf8String);
+          statement.SetParameterType("type", ValueType_Integer64);
+          statement.Execute(args);
+        }
+
+        // Must be out of the scope of "DatabaseManager::CachedStatement"
+        const int64_t id = GetSQLiteLastInsert(manager);
+        
+        if (type == OrthancPluginResourceType_Patient)
+        {
+          AddPatientToRecyclingOrder(manager, id);
+        }
+        
+        return id;
+      }
+      
+      case Dialect_PostgreSQL:
+      {
+        int64_t id;
+        
+        {
+          DatabaseManager::CachedStatement statement(
+            STATEMENT_FROM_HERE, manager,
+            "INSERT INTO Resources VALUES(DEFAULT, ${type}, ${id}, NULL) RETURNING internalId");
+          
+          statement.SetParameterType("id", ValueType_Utf8String);
+          statement.SetParameterType("type", ValueType_Integer64);
+          statement.Execute(args);
+          id = statement.ReadInteger64(0);
+        }
+        
+        if (type == OrthancPluginResourceType_Patient)
+        {
+          AddPatientToRecyclingOrder(manager, id);
+        }
+        
+        return id;
+      }
+        
+      case Dialect_MySQL:
+      {
+        {
+          DatabaseManager::CachedStatement statement(
+            STATEMENT_FROM_HERE, manager, "INSERT INTO Resources VALUES(NULL, ${type}, ${id}, NULL)");
+          
+          statement.SetParameterType("id", ValueType_Utf8String);
+          statement.SetParameterType("type", ValueType_Integer64);
+          statement.Execute(args);
+        }
+        
+        // Must be out of the scope of "DatabaseManager::CachedStatement"
+        const int64_t id = GetMySQLLastInsert(manager);
+        
+        if (type == OrthancPluginResourceType_Patient)
+        {
+          AddPatientToRecyclingOrder(manager, id);
+        }
+
+        return id;
+      }
+        
+      case Dialect_MSSQL:
+      {
+        {
+          DatabaseManager::CachedStatement statement(
+            STATEMENT_FROM_HERE, manager, "INSERT INTO Resources VALUES(${type}, ${id}, NULL)");
+          
+          statement.SetParameterType("id", ValueType_Utf8String);
+          statement.SetParameterType("type", ValueType_Integer64);
+          statement.Execute(args);
+        }
+        
+        // Must be out of the scope of "DatabaseManager::CachedStatement"
+        const int64_t id = GetMSSQLLastInsert(manager);
+
+        if (type == OrthancPluginResourceType_Patient)
+        {
+          AddPatientToRecyclingOrder(manager, id);
+        }
+
+        return id;
+      }
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+    }
+  }
+
+
+  void OdbcIndex::DeleteResource(IDatabaseBackendOutput& output,
+                                 DatabaseManager& manager,
+                                 int64_t id)
+  {
+    /**
+     * Contrarily to PostgreSQL and SQLite, the MySQL dialect
+     * doesn't support cascaded delete inside the same
+     * table. Furthermore, for maximum portability, we don't use
+     * triggers in the ODBC plugins. We therefore implement a custom
+     * version of this deletion.
+     **/
+
+    ClearDeletedFiles(manager);
+    ClearDeletedResources(manager);
+
+    OrthancPluginResourceType type;
+    bool hasParent;
+    int64_t parentId;
+
+    {
+      DatabaseManager::CachedStatement lookupResource(
+        STATEMENT_FROM_HERE, manager,
+        "SELECT resourceType, parentId FROM Resources WHERE internalId=${id}");
+      lookupResource.SetParameterType("id", ValueType_Integer64);
+      
+      Dictionary args;
+      args.SetIntegerValue("id", id);
+      lookupResource.Execute(args);
+      
+      if (lookupResource.IsDone())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);
+      }
+      
+      type = static_cast<OrthancPluginResourceType>(lookupResource.ReadInteger32(0));
+      
+      if (lookupResource.GetResultField(1).GetType() == ValueType_Null)
+      {
+        hasParent = false;
+      }
+      else
+      {
+        hasParent = true;
+        parentId = lookupResource.ReadInteger64(1);
+      }
+    }
+
+    {
+      DatabaseManager::CachedStatement scheduleRootDeletion(
+        STATEMENT_FROM_HERE, manager,
+        "INSERT INTO DeletedResources SELECT internalId, resourceType, publicId "
+        "FROM Resources WHERE Resources.internalId = ${id}");
+      scheduleRootDeletion.SetParameterType("id", ValueType_Integer64);
+      
+      Dictionary args;
+      args.SetIntegerValue("id", id);
+      scheduleRootDeletion.Execute(args);
+    }
+
+    {
+      const std::string scheduleChildrenDeletion =
+        "INSERT INTO DeletedResources SELECT Resources.internalId, Resources.resourceType, Resources.publicId "
+        "FROM Resources INNER JOIN DeletedResources ON Resources.parentId = DeletedResources.internalId "
+        "WHERE Resources.resourceType = ${level}";
+      
+      switch (type)
+      {
+        /**
+         * WARNING: Don't add "break" or reorder cases below.
+         **/
+        
+        case OrthancPluginResourceType_Patient:
+        {
+          DatabaseManager::CachedStatement statement(STATEMENT_FROM_HERE, manager, scheduleChildrenDeletion);
+          statement.SetParameterType("level", ValueType_Integer64);
+          
+          Dictionary args;
+          args.SetIntegerValue("level", OrthancPluginResourceType_Study);
+          statement.Execute(args);
+        }
+        
+        case OrthancPluginResourceType_Study:
+        {
+          DatabaseManager::CachedStatement statement(STATEMENT_FROM_HERE, manager, scheduleChildrenDeletion);
+          statement.SetParameterType("level", ValueType_Integer64);
+          
+          Dictionary args;
+          args.SetIntegerValue("level", OrthancPluginResourceType_Series);
+          statement.Execute(args);
+        }
+        
+        case OrthancPluginResourceType_Series:
+        {
+          DatabaseManager::CachedStatement statement(STATEMENT_FROM_HERE, manager, scheduleChildrenDeletion);
+          statement.SetParameterType("level", ValueType_Integer64);
+
+          Dictionary args;
+          args.SetIntegerValue("level", OrthancPluginResourceType_Instance);
+          statement.Execute(args);
+        }
+        
+        case OrthancPluginResourceType_Instance:
+          // No child
+          break;
+
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+    }
+
+    bool hasRemainingAncestor = false;
+    std::string remainingAncestor;
+    OrthancPluginResourceType ancestorType;
+    
+    if (hasParent)
+    {
+      int64_t currentAncestor = parentId;
+      int64_t currentResource = id;
+      OrthancPluginResourceType currentType = type;
+
+      for (;;)
+      {
+        bool hasSiblings;
+
+        {
+          std::string suffix;
+          if (manager.GetDialect() == Dialect_MSSQL)
+          {
+            suffix = "ORDER BY internalId OFFSET 0 ROWS FETCH FIRST 1 ROWS ONLY";
+          }
+          else
+          {
+            suffix = "LIMIT 1";
+          }
+
+          DatabaseManager::CachedStatement lookupSiblings(
+            STATEMENT_FROM_HERE, manager,
+            "SELECT internalId FROM Resources WHERE parentId = ${parent} AND internalId <> ${id} " + suffix);
+
+          lookupSiblings.SetParameterType("parent", ValueType_Integer64);
+          lookupSiblings.SetParameterType("id", ValueType_Integer64);
+
+          Dictionary args;
+          args.SetIntegerValue("parent", currentAncestor);
+          args.SetIntegerValue("id", currentResource);
+          lookupSiblings.Execute(args);
+
+          hasSiblings = !lookupSiblings.IsDone();
+        }
+
+        if (hasSiblings)
+        {
+          // There remains some sibling: Signal this remaining ancestor
+          hasRemainingAncestor = true;
+          remainingAncestor = GetPublicId(manager, currentAncestor);
+          ancestorType = GetParentType(currentType);
+          break;
+        }
+        else
+        {
+          // No sibling remaining: This parent resource must be deleted
+          {
+            DatabaseManager::CachedStatement addDeletedResource(
+              STATEMENT_FROM_HERE, manager,
+              "INSERT INTO DeletedResources SELECT internalId, resourceType, publicId "
+              "FROM Resources WHERE internalId=${id}");
+            addDeletedResource.SetParameterType("id", ValueType_Integer64);
+
+            Dictionary args;
+            args.SetIntegerValue("id", currentAncestor);
+            addDeletedResource.Execute(args);
+          }
+
+          int64_t tmp;
+          if (LookupParent(tmp, manager, currentAncestor))
+          {
+            currentResource = currentAncestor;
+            currentAncestor = tmp;
+            currentType = GetParentType(currentType);
+          }
+          else
+          {
+            assert(currentType == OrthancPluginResourceType_Study);
+            break;
+          }
+        }
+      }
+    }
+
+    {
+      // This is implemented by triggers in the PostgreSQL and MySQL plugins
+      DatabaseManager::CachedStatement lookupDeletedAttachments(
+        STATEMENT_FROM_HERE, manager,
+        "INSERT INTO DeletedFiles SELECT AttachedFiles.* FROM AttachedFiles "
+        "INNER JOIN DeletedResources ON AttachedFiles.id = DeletedResources.internalId");
+      lookupDeletedAttachments.Execute();
+    }
+
+    {
+      // Note that the attachments are automatically deleted by DELETE CASCADE
+      DatabaseManager::CachedStatement applyResourcesDeletion(
+        STATEMENT_FROM_HERE, manager,
+        "DELETE FROM Resources WHERE internalId IN (SELECT internalId FROM DeletedResources)");
+      applyResourcesDeletion.Execute();
+    }
+
+    SignalDeletedResources(output, manager);
+    SignalDeletedFiles(output, manager);
+    
+    if (hasRemainingAncestor)
+    {
+      assert(!remainingAncestor.empty());
+      output.SignalRemainingAncestor(remainingAncestor, ancestorType);
+    }
+  }
+
+
+  static void ExecuteLogChange(DatabaseManager::CachedStatement& statement,
+                               const Dictionary& args)
+  {
+    statement.SetParameterType("changeType", ValueType_Integer64);
+    statement.SetParameterType("id", ValueType_Integer64);
+    statement.SetParameterType("resourceType", ValueType_Integer64);
+    statement.SetParameterType("date", ValueType_Utf8String);
+    statement.Execute(args);
+  }
+  
+  
+  void OdbcIndex::LogChange(DatabaseManager& manager,
+                            int32_t changeType,
+                            int64_t resourceId,
+                            OrthancPluginResourceType resourceType,
+                            const char* date)
+  {
+    Dictionary args;
+    args.SetIntegerValue("changeType", changeType);
+    args.SetIntegerValue("id", resourceId);
+    args.SetIntegerValue("resourceType", resourceType);
+    args.SetUtf8Value("date", date);
+
+    int64_t seq;
+
+    switch (manager.GetDatabase().GetDialect())
+    {
+      case Dialect_SQLite:
+      {
+        DatabaseManager::CachedStatement statement(
+          STATEMENT_FROM_HERE, manager,
+          "INSERT INTO Changes VALUES(NULL, ${changeType}, ${id}, ${resourceType}, ${date})");
+        ExecuteLogChange(statement, args);
+        seq = GetSQLiteLastInsert(manager);
+        break;
+      }
+      
+      case Dialect_PostgreSQL:
+      {
+        DatabaseManager::CachedStatement statement(
+          STATEMENT_FROM_HERE, manager,
+          "INSERT INTO Changes VALUES(DEFAULT, ${changeType}, ${id}, ${resourceType}, ${date}) RETURNING seq");
+        ExecuteLogChange(statement, args);
+        seq = statement.ReadInteger64(0);
+        break;
+      }
+        
+      case Dialect_MySQL:
+      {
+        DatabaseManager::CachedStatement statement(
+          STATEMENT_FROM_HERE, manager,
+          "INSERT INTO Changes VALUES(NULL, ${changeType}, ${id}, ${resourceType}, ${date})");
+        ExecuteLogChange(statement, args);
+        seq = GetMySQLLastInsert(manager);
+        break;
+      }
+        
+      case Dialect_MSSQL:
+      {
+        DatabaseManager::CachedStatement statement(
+          STATEMENT_FROM_HERE, manager,
+          "INSERT INTO Changes VALUES(${changeType}, ${id}, ${resourceType}, ${date})");
+        ExecuteLogChange(statement, args);
+        seq = GetMSSQLLastInsert(manager);
+        break;
+      }
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+    }
+    
+    std::string value = boost::lexical_cast<std::string>(seq);
+    SetGlobalProperty(manager, MISSING_SERVER_IDENTIFIER, GlobalProperty_LastChange, value.c_str());
+  }
+
+
+  int64_t OdbcIndex::GetLastChangeIndex(DatabaseManager& manager)
+  {
+    std::string value;
+    
+    if (LookupGlobalProperty(value, manager, MISSING_SERVER_IDENTIFIER, GlobalProperty_LastChange))
+    {
+      return boost::lexical_cast<int64_t>(value);
+    }
+    else
+    {
+      return 0;
+    }
+  }
+
+
+  void OdbcIndex::DeleteAttachment(IDatabaseBackendOutput& output,
+                                   DatabaseManager& manager,
+                                   int64_t id,
+                                   int32_t attachment)
+  {
+    ClearDeletedFiles(manager);
+
+    Dictionary args;
+    args.SetIntegerValue("id", id);
+    args.SetIntegerValue("type", static_cast<int>(attachment));
+    
+    {
+      // This is implemented by triggers in the PostgreSQL and MySQL plugins
+      DatabaseManager::CachedStatement statement(
+        STATEMENT_FROM_HERE, manager,
+        "INSERT INTO DeletedFiles SELECT * FROM AttachedFiles WHERE id=${id} AND fileType=${type}");
+      
+      statement.SetParameterType("id", ValueType_Integer64);
+      statement.SetParameterType("type", ValueType_Integer64);
+      statement.Execute(args);
+    }
+
+    {
+      DatabaseManager::CachedStatement statement(
+        STATEMENT_FROM_HERE, manager,
+        "DELETE FROM AttachedFiles WHERE id=${id} AND fileType=${type}");
+
+      statement.SetParameterType("id", ValueType_Integer64);
+      statement.SetParameterType("type", ValueType_Integer64);
+      statement.Execute(args);
+    }
+
+    SignalDeletedFiles(output, manager);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Odbc/Plugins/OdbcIndex.h	Tue Aug 10 20:08:53 2021 +0200
@@ -0,0 +1,86 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2021 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../Framework/Plugins/IndexBackend.h"
+
+namespace OrthancDatabases
+{
+  class OdbcIndex : public IndexBackend
+  {
+  private:
+    unsigned int maxConnectionRetries_;
+    unsigned int connectionRetryInterval_;
+    std::string  connectionString_;
+    
+  public:
+    OdbcIndex(OrthancPluginContext* context,
+              const std::string& connectionString);
+
+    unsigned int GetMaxConnectionRetries() const
+    {
+      return maxConnectionRetries_;
+    }
+
+    void SetMaxConnectionRetries(unsigned int retries)
+    {
+      maxConnectionRetries_ = retries;
+    }
+
+    unsigned int GetConnectionRetryInterval() const
+    {
+      return connectionRetryInterval_;
+    }
+
+    void SetConnectionRetryInterval(unsigned int seconds);
+
+    virtual IDatabaseFactory* CreateDatabaseFactory() ORTHANC_OVERRIDE;    
+    
+    virtual void ConfigureDatabase(DatabaseManager& manager) ORTHANC_OVERRIDE;
+    
+    virtual bool HasRevisionsSupport() const ORTHANC_OVERRIDE
+    {
+      return true;
+    }
+
+    virtual int64_t CreateResource(DatabaseManager& manager,
+                                   const char* publicId,
+                                   OrthancPluginResourceType type) ORTHANC_OVERRIDE;
+    
+    virtual void DeleteResource(IDatabaseBackendOutput& output,
+                                DatabaseManager& manager,
+                                int64_t id) ORTHANC_OVERRIDE;
+    
+    virtual void LogChange(DatabaseManager& manager,
+                           int32_t changeType,
+                           int64_t resourceId,
+                           OrthancPluginResourceType resourceType,
+                           const char* date) ORTHANC_OVERRIDE;
+
+    virtual int64_t GetLastChangeIndex(DatabaseManager& manager) ORTHANC_OVERRIDE;
+
+    virtual void DeleteAttachment(IDatabaseBackendOutput& output,
+                                  DatabaseManager& manager,
+                                  int64_t id,
+                                  int32_t attachment) ORTHANC_OVERRIDE;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Odbc/Plugins/PrepareIndex.sql	Tue Aug 10 20:08:53 2021 +0200
@@ -0,0 +1,124 @@
+CREATE TABLE GlobalProperties(
+       property INTEGER PRIMARY KEY,
+       value ${LONGTEXT}
+       );
+
+CREATE TABLE ServerProperties(
+       server VARCHAR(64) NOT NULL,
+       property INTEGER NOT NULL,
+       value ${LONGTEXT},
+       PRIMARY KEY(server, property)
+       );
+
+CREATE TABLE Resources(
+       internalId ${AUTOINCREMENT_TYPE},
+       resourceType INTEGER NOT NULL,
+       publicId VARCHAR(64) NOT NULL,
+       parentId BIGINT
+       );
+
+CREATE TABLE MainDicomTags(
+       id BIGINT NOT NULL,
+       tagGroup INTEGER NOT NULL,
+       tagElement INTEGER NOT NULL,
+       value VARCHAR(255),
+       PRIMARY KEY(id, tagGroup, tagElement),
+       CONSTRAINT MainDicomTags1 FOREIGN KEY (id) REFERENCES Resources(internalId) ON DELETE CASCADE
+       );
+
+CREATE TABLE DicomIdentifiers(
+       id BIGINT NOT NULL,
+       tagGroup INTEGER NOT NULL,
+       tagElement INTEGER NOT NULL,
+       value VARCHAR(255),
+       PRIMARY KEY(id, tagGroup, tagElement),
+       CONSTRAINT DicomIdentifiers1 FOREIGN KEY (id) REFERENCES Resources(internalId) ON DELETE CASCADE
+       );
+
+CREATE TABLE Metadata(
+       id BIGINT NOT NULL,
+       type INTEGER NOT NULL,
+       value ${LONGTEXT},
+       revision INTEGER,
+       PRIMARY KEY(id, type),
+       CONSTRAINT Metadata1 FOREIGN KEY (id) REFERENCES Resources(internalId) ON DELETE CASCADE
+       );
+       
+CREATE TABLE AttachedFiles(
+       id BIGINT NOT NULL,
+       fileType INTEGER,
+       uuid VARCHAR(64) NOT NULL,
+       compressedSize BIGINT,
+       uncompressedSize BIGINT,
+       compressionType INTEGER,
+       uncompressedHash VARCHAR(40),
+       compressedHash VARCHAR(40),
+       revision INTEGER,
+       PRIMARY KEY(id, fileType),
+       CONSTRAINT AttachedFiles1 FOREIGN KEY (id) REFERENCES Resources(internalId) ON DELETE CASCADE
+       );              
+
+CREATE TABLE Changes(
+       seq ${AUTOINCREMENT_TYPE},
+       changeType INTEGER,
+       internalId BIGINT NOT NULL,
+       resourceType INTEGER,
+       date VARCHAR(64),
+       CONSTRAINT Changes1 FOREIGN KEY (internalId) REFERENCES Resources(internalId) ON DELETE CASCADE
+       );
+       
+CREATE TABLE ExportedResources(
+       seq ${AUTOINCREMENT_TYPE},
+       resourceType INTEGER,
+       publicId VARCHAR(64),
+       remoteModality VARCHAR(64),
+       patientId VARCHAR(64),
+       studyInstanceUid VARCHAR(128),
+       seriesInstanceUid VARCHAR(128),
+       sopInstanceUid VARCHAR(128),
+       date VARCHAR(64)
+       ); 
+
+CREATE TABLE PatientRecyclingOrder(
+       seq ${AUTOINCREMENT_TYPE},
+       patientId BIGINT NOT NULL,
+       CONSTRAINT PatientRecyclingOrder1 FOREIGN KEY (patientId) REFERENCES Resources(internalId) ON DELETE CASCADE
+       );
+
+CREATE INDEX ChildrenIndex ON Resources(parentId);
+CREATE INDEX PublicIndex ON Resources(publicId);
+CREATE INDEX ResourceTypeIndex ON Resources(resourceType);
+CREATE INDEX PatientRecyclingIndex ON PatientRecyclingOrder(patientId);
+
+CREATE INDEX MainDicomTagsIndex ON MainDicomTags(id);
+CREATE INDEX DicomIdentifiersIndex1 ON DicomIdentifiers(id);
+CREATE INDEX DicomIdentifiersIndex2 ON DicomIdentifiers(tagGroup, tagElement);
+CREATE INDEX DicomIdentifiersIndexValues ON DicomIdentifiers(value);
+
+CREATE INDEX ChangesIndex ON Changes(internalId);
+
+
+
+-- New tables wrt. Orthanc core
+CREATE TABLE DeletedFiles(   -- Same structure as AttachedFiles
+       id BIGINT NOT NULL,
+       fileType INTEGER,
+       uuid VARCHAR(64) NOT NULL,
+       compressedSize BIGINT,
+       uncompressedSize BIGINT,
+       compressionType INTEGER,
+       uncompressedHash VARCHAR(40),
+       compressedHash VARCHAR(40),
+       revision INTEGER
+       );
+
+CREATE TABLE DeletedResources(
+       internalId BIGINT NOT NULL PRIMARY KEY,
+       resourceType INTEGER NOT NULL,
+       publicId VARCHAR(64) NOT NULL
+       );
+
+
+
+-- Set version of database to 6
+INSERT INTO GlobalProperties VALUES(1, '6');
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Odbc/Plugins/PrepareStorage.sql	Tue Aug 10 20:08:53 2021 +0200
@@ -0,0 +1,4 @@
+CREATE TABLE StorageArea(
+       uuid VARCHAR(64) NOT NULL PRIMARY KEY,
+       content ${BINARY} NOT NULL,
+       type INTEGER NOT NULL);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Odbc/Plugins/StoragePlugin.cpp	Tue Aug 10 20:08:53 2021 +0200
@@ -0,0 +1,199 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2021 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../../Framework/Odbc/OdbcDatabase.h"
+#include "../../Framework/Plugins/PluginInitialization.h"
+#include "../../Framework/Plugins/StorageBackend.h"
+#include "../../Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h"
+
+#include <EmbeddedResources.h>  // Autogenerated file
+
+#include <Logging.h>
+
+
+namespace OrthancDatabases
+{
+  class OdbcStorageArea : public StorageBackend
+  {
+  protected:
+    virtual bool HasReadRange() const ORTHANC_OVERRIDE
+    {
+      // Read range is only available in native PostgreSQL/MySQL plugins
+      return false;
+    }
+
+  public:
+    OdbcStorageArea(unsigned int maxConnectionRetries,
+                    unsigned int connectionRetryInterval,
+                    const std::string& connectionString) :
+      StorageBackend(OdbcDatabase::CreateDatabaseFactory(
+                       maxConnectionRetries, connectionRetryInterval, connectionString, false),
+                     maxConnectionRetries)
+    {
+      {
+        AccessorBase accessor(*this);
+        OdbcDatabase& db = dynamic_cast<OdbcDatabase&>(accessor.GetManager().GetDatabase());
+        
+        if (!db.DoesTableExist("storagearea"))
+        {
+          std::string sql;
+          Orthanc::EmbeddedResources::GetFileResource(sql, Orthanc::EmbeddedResources::ODBC_PREPARE_STORAGE);
+          
+          switch (db.GetDialect())
+          {
+            case Dialect_SQLite:
+              boost::replace_all(sql, "${BINARY}", "BLOB");
+              break;
+              
+            case Dialect_PostgreSQL:
+              boost::replace_all(sql, "${BINARY}", "BYTEA");
+              break;
+              
+            case Dialect_MySQL:
+              boost::replace_all(sql, "${BINARY}", "LONGBLOB");
+              break;
+              
+            case Dialect_MSSQL:
+              boost::replace_all(sql, "${BINARY}", "VARBINARY(MAX)");
+              break;
+              
+            default:
+              throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+          }
+
+          {
+            DatabaseManager::Transaction t(accessor.GetManager(), TransactionType_ReadWrite);
+            db.ExecuteMultiLines(sql);
+            t.Commit();
+          }
+        }
+      }
+    }
+  };
+}
+
+
+
+#if defined(_WIN32)
+#  include <windows.h>
+#else
+#  include <ltdl.h>
+#  include <libltdl/lt_dlloader.h>
+#endif
+
+
+static const char* const KEY_ODBC = "Odbc";
+
+
+extern "C"
+{
+#if !defined(_WIN32)
+  extern lt_dlvtable *dlopen_LTX_get_vtable(lt_user_data loader_data);
+#endif
+
+  
+  ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context)
+  {
+    if (!OrthancDatabases::InitializePlugin(context, "ODBC", false))
+    {
+      return -1;
+    }
+
+#if !defined(_WIN32)
+    lt_dlinit();
+    
+    /**
+     * The following call is necessary for "libltdl" to access the
+     * "dlopen()" primitives if statically linking. Otherwise, only the
+     * "preopen" primitives are available.
+     **/
+    lt_dlloader_add(dlopen_LTX_get_vtable(NULL));
+#endif
+
+    OrthancPlugins::OrthancConfiguration configuration;
+
+    if (!configuration.IsSection(KEY_ODBC))
+    {
+      LOG(WARNING) << "No available configuration for the ODBC storage area plugin";
+      return 0;
+    }
+
+    OrthancPlugins::OrthancConfiguration odbc;
+    configuration.GetSection(odbc, KEY_ODBC);
+
+    bool enable;
+    if (!odbc.LookupBooleanValue(enable, "EnableStorage") ||
+        !enable)
+    {
+      LOG(WARNING) << "The ODBC storage area is currently disabled, set \"EnableStorage\" "
+                   << "to \"true\" in the \"" << KEY_ODBC << "\" section of the configuration file of Orthanc";
+      return 0;
+    }
+
+    try
+    {
+      const std::string connectionString = odbc.GetStringValue("StorageConnectionString", "");
+      const unsigned int maxConnectionRetries = odbc.GetUnsignedIntegerValue("MaxConnectionRetries", 10);
+      const unsigned int connectionRetryInterval = odbc.GetUnsignedIntegerValue("ConnectionRetryInterval", 5);
+
+      if (connectionString.empty())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange,
+                                        "No connection string provided for the ODBC storage area");
+      }
+
+      OrthancDatabases::StorageBackend::Register(
+        context, new OrthancDatabases::OdbcStorageArea(
+          maxConnectionRetries, connectionRetryInterval, connectionString));
+    }
+    catch (Orthanc::OrthancException& e)
+    {
+      LOG(ERROR) << e.What();
+      return -1;
+    }
+    catch (...)
+    {
+      LOG(ERROR) << "Native exception while initializing the plugin";
+      return -1;
+    }
+
+    return 0;
+  }
+
+
+  ORTHANC_PLUGINS_API void OrthancPluginFinalize()
+  {
+    LOG(WARNING) << "ODBC storage area is finalizing";
+    OrthancDatabases::StorageBackend::Finalize();
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
+  {
+    return "odbc-storage";
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
+  {
+    return ORTHANC_PLUGIN_VERSION;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Odbc/UnitTests/UnitTestsMain.cpp	Tue Aug 10 20:08:53 2021 +0200
@@ -0,0 +1,93 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2021 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../Plugins/OdbcIndex.h"
+
+static std::string connectionString_;
+
+#include "../../Framework/Plugins/IndexUnitTests.h"
+
+#include <Logging.h>
+
+
+#if defined(_WIN32)
+#  warning Strings have not been tested on Windows (UTF16 issues ahead)!
+#  include <windows.h>
+#else
+#  include <ltdl.h>
+#  include <libltdl/lt_dlloader.h>
+#endif
+
+
+#if !defined(_WIN32)
+extern "C"
+{
+  extern lt_dlvtable *dlopen_LTX_get_vtable(lt_user_data loader_data);
+}
+#endif
+
+
+int main(int argc, char **argv)
+{
+  if (argc < 2)
+  {
+    std::cerr
+      << std::endl
+      << "Usage:    " << argv[0] << " <connection string>"
+      << std::endl << std::endl
+      << "Example:  " << argv[0] << " \"DSN=test\""
+      << std::endl << std::endl;
+    return -1;
+  }
+
+  for (int i = 1; i < argc; i++)
+  {
+    // Ignore arguments beginning with "-" to allow passing arguments
+    // to Google Test such as "--gtest_filter="
+    if (argv[i] != NULL &&
+        argv[i][0] != '-')
+    {
+      connectionString_ = argv[i];
+    }
+  }
+
+#if !defined(_WIN32)
+  lt_dlinit();
+
+  /**
+   * The following call is necessary for "libltdl" to access the
+   * "dlopen()" primitives if statically linking. Otherwise, only the
+   * "preopen" primitives are available.
+   **/
+  lt_dlloader_add(dlopen_LTX_get_vtable(NULL));
+#endif
+
+  ::testing::InitGoogleTest(&argc, argv);
+  Orthanc::Logging::Initialize();
+  Orthanc::Logging::EnableInfoLevel(true);
+  //Orthanc::Logging::EnableTraceLevel(true);
+
+  int result = RUN_ALL_TESTS();
+  
+  Orthanc::Logging::Finalize();
+
+  return result;
+}
--- a/PostgreSQL/CMakeLists.txt	Thu Jul 22 20:20:26 2021 +0200
+++ b/PostgreSQL/CMakeLists.txt	Tue Aug 10 20:08:53 2021 +0200
@@ -1,3 +1,22 @@
+# Orthanc - A Lightweight, RESTful DICOM Store
+# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+# Department, University Hospital of Liege, Belgium
+# Copyright (C) 2017-2021 Osimis S.A., Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the GNU Affero General Public License
+# as published by the Free Software Foundation, either version 3 of
+# the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
 cmake_minimum_required(VERSION 2.8)
 project(OrthancPostgreSQL)
 
--- a/Resources/CMake/DatabasesFrameworkConfiguration.cmake	Thu Jul 22 20:20:26 2021 +0200
+++ b/Resources/CMake/DatabasesFrameworkConfiguration.cmake	Tue Aug 10 20:08:53 2021 +0200
@@ -47,6 +47,10 @@
   endif()
 endif()
 
+if (ENABLE_ODBC_BACKEND)
+endif()
+  
+
 
 #####################################################################
 ## Configure the Orthanc Framework
@@ -171,3 +175,26 @@
   unset(USE_SYSTEM_LIBPQ CACHE)
   add_definitions(-DORTHANC_ENABLE_POSTGRESQL=0)
 endif()
+
+
+
+#####################################################################
+## Configure ODBC if need be
+#####################################################################
+
+if (ENABLE_ODBC_BACKEND)
+  include(${CMAKE_CURRENT_LIST_DIR}/UnixOdbcConfiguration.cmake)
+  add_definitions(-DORTHANC_ENABLE_ODBC=1)
+  list(APPEND DATABASES_SOURCES
+    ${ORTHANC_DATABASES_ROOT}/Framework/Odbc/OdbcDatabase.cpp
+    ${ORTHANC_DATABASES_ROOT}/Framework/Odbc/OdbcEnvironment.cpp
+    ${ORTHANC_DATABASES_ROOT}/Framework/Odbc/OdbcPreparedStatement.cpp
+    ${ORTHANC_DATABASES_ROOT}/Framework/Odbc/OdbcResult.cpp
+    ${ORTHANC_DATABASES_ROOT}/Framework/Odbc/OdbcStatement.cpp
+    ${LIBPQ_SOURCES}
+    )
+else()
+  unset(USE_SYSTEM_UNIX_ODBC)
+  unset(USE_SYSTEM_LTDL)
+  add_definitions(-DORTHANC_ENABLE_ODBC=0)
+endif()
--- a/Resources/CMake/DatabasesFrameworkParameters.cmake	Thu Jul 22 20:20:26 2021 +0200
+++ b/Resources/CMake/DatabasesFrameworkParameters.cmake	Tue Aug 10 20:08:53 2021 +0200
@@ -36,6 +36,11 @@
 set(USE_SYSTEM_LIBPQ ON CACHE BOOL "Use the system version of the PostgreSQL client library")
 set(USE_SYSTEM_MYSQL_CLIENT ON CACHE BOOL "Use the system version of the MySQL client library")
 
+if (NOT CMAKE_SYSTEM_NAME STREQUAL "Windows")
+  set(USE_SYSTEM_UNIX_ODBC ON CACHE BOOL "Use the system version of unixODBC")
+  set(USE_SYSTEM_LTDL ON CACHE BOOL "Use the system version of libltdl")
+endif()
+
 
 #####################################################################
 ## Internal CMake parameters to enable the optional subcomponents of
@@ -43,5 +48,6 @@
 #####################################################################
 
 set(ENABLE_MYSQL_BACKEND OFF)
+set(ENABLE_ODBC_BACKEND OFF)
 set(ENABLE_POSTGRESQL_BACKEND OFF)
 set(ENABLE_SQLITE_BACKEND OFF)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/CMake/UnixOdbcConfiguration.cmake	Tue Aug 10 20:08:53 2021 +0200
@@ -0,0 +1,248 @@
+# Orthanc - A Lightweight, RESTful DICOM Store
+# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+# Department, University Hospital of Liege, Belgium
+# Copyright (C) 2017-2021 Osimis S.A., Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the GNU Affero General Public License
+# as published by the Free Software Foundation, either version 3 of
+# the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+if (STATIC_BUILD OR NOT USE_SYSTEM_UNIX_ODBC)
+  include(CheckFunctionExists)
+  include(CheckTypeSize)
+  
+  set(VERSION "2.3.9")  # Used in "config.h.in"
+  set(UNIX_ODBC_SOURCES_DIR ${CMAKE_BINARY_DIR}/unixODBC-${VERSION})
+  set(UNIX_ODBC_MD5 "06f76e034bb41df5233554abe961a16f")
+  set(UNIX_ODBC_URL "http://orthanc.osimis.io/ThirdPartyDownloads/unixODBC-${VERSION}.tar.gz")
+
+  DownloadPackage(${UNIX_ODBC_MD5} ${UNIX_ODBC_URL} "${UNIX_ODBC_SOURCES_DIR}")
+
+  if (STATIC_BUILD OR NOT USE_SYSTEM_LTDL)
+    add_definitions(
+      -DLT_OBJDIR=".libs/"
+      -DLTDL  # Necessary for LT_SCOPE to be properly defined
+      #-DLT_DEBUG_LOADERS  # Get debug messages
+      )
+
+    include_directories(
+      ${UNIX_ODBC_SOURCES_DIR}/libltdl
+      ${UNIX_ODBC_SOURCES_DIR}/libltdl/libltdl/
+      )
+
+    list(APPEND LTDL_SOURCES
+      ${UNIX_ODBC_SOURCES_DIR}/libltdl/loaders/dlopen.c
+      ${UNIX_ODBC_SOURCES_DIR}/libltdl/loaders/preopen.c
+      ${UNIX_ODBC_SOURCES_DIR}/libltdl/lt__alloc.c
+      ${UNIX_ODBC_SOURCES_DIR}/libltdl/lt__strl.c
+      ${UNIX_ODBC_SOURCES_DIR}/libltdl/lt_dlloader.c
+      ${UNIX_ODBC_SOURCES_DIR}/libltdl/lt_error.c
+      ${UNIX_ODBC_SOURCES_DIR}/libltdl/ltdl.c
+      ${UNIX_ODBC_SOURCES_DIR}/libltdl/slist.c
+      )
+  else()
+    check_include_file("libltdl/lt_dlloader.h"  HAVE_LT_DLLOADER_H)
+    if (NOT HAVE_LT_DLLOADER_H)
+      message(FATAL_ERROR "Please install the libltdl-dev package")
+    endif()
+
+    link_libraries(ltdl)    
+  endif()
+
+
+  include_directories(
+    ${CMAKE_CURRENT_BINARY_DIR}/AUTOGENERATED
+    ${UNIX_ODBC_SOURCES_DIR}/include
+    ${UNIX_ODBC_SOURCES_DIR}/DriverManager
+    )
+
+  file(GLOB UNIX_ODBC_SOURCES
+    ${UNIX_ODBC_SOURCES_DIR}/cur/*.c
+    ${UNIX_ODBC_SOURCES_DIR}/DriverManager/*.c
+    ${UNIX_ODBC_SOURCES_DIR}/odbcinst/*.c
+    ${UNIX_ODBC_SOURCES_DIR}/ini/*.c
+    ${UNIX_ODBC_SOURCES_DIR}/log/*.c
+    ${UNIX_ODBC_SOURCES_DIR}/lst/*.c
+    )
+
+  list(REMOVE_ITEM UNIX_ODBC_SOURCES
+    ${UNIX_ODBC_SOURCES_DIR}/cur/SQLConnect.c
+    ${UNIX_ODBC_SOURCES_DIR}/cur/SQLGetDiagRec.c
+    )
+
+
+  set(ASCII_ENCODING "auto-search")
+  set(SYSTEM_FILE_PATH "/etc")
+  set(DEFLIB_PATH "/usr/lib")
+  set(ENABLE_DRIVER_ICONV ON)  # Enables support for encodings
+
+  set(STDC_HEADERS 1)
+  set(UNIXODBC ON)
+  set(UNIXODBC_SOURCE ON)   # This makes "intptr_t" to be defined
+  set(ICONV_CONST ON)
+  set(STRICT_ODBC_ERROR ON)
+
+  if (CMAKE_SIZEOF_VOID_P EQUAL 8)
+    set(PLATFORM64 1)
+  endif()
+
+  list(GET CMAKE_FIND_LIBRARY_SUFFIXES 0 SHLIBEXT)
+
+  check_include_file("alloca.h"               HAVE_ALLOCA_H)
+  check_include_file("argz.h"                 HAVE_ARGZ_H)
+  check_include_file("crypt.h"                HAVE_CRYPT_H)
+  check_include_file("dirent.h"               HAVE_DIRENT_H)
+  check_include_file("dlfcn.h"                HAVE_DLFCN_H)
+  check_include_file("inttypes.h"             HAVE_INTTYPES_H)
+  check_include_file("langinfo.h"             HAVE_LANGINFO_H)
+  check_include_file("crypt.h"                HAVE_CRYPT_H)
+  check_include_file("limits.h"               HAVE_LIMITS_H)
+  check_include_file("locale.h"               HAVE_LOCALE_H)
+  check_include_file("malloc.h"               HAVE_MALLOC_H)
+  check_include_file("memory.h"               HAVE_MEMORY_H)
+  check_include_file("pwd.h"                  HAVE_PWD_H)
+  check_include_file("stdarg.h"               HAVE_STDARG_H)
+  check_include_file("stdlib.h"               HAVE_STDLIB_H)
+  check_include_file("string.h"               HAVE_STRING_H)
+  check_include_file("strings.h"              HAVE_STRINGS_H)
+  check_include_file("time.h"                 HAVE_TIME_H)
+  check_include_file("sys/sem.h"              HAVE_SYS_SEM_H)
+  check_include_file("sys/stat.h"             HAVE_SYS_STAT_H)
+  check_include_file("sys/time.h"             HAVE_SYS_TIME_H)
+  check_include_file("sys/timeb.h"            HAVE_SYS_TIMEB_H)
+  check_include_file("unistd.h"               HAVE_UNISTD_H)
+  check_include_file("readline/readline.h"    HAVE_READLINE_H)
+  check_include_file("readline/history.h"     HAVE_READLINE_HISTORY_H)
+
+  check_symbol_exists(alloca "alloca.h"         HAVE_ALLOCA)
+  check_symbol_exists(argz_add "argz.h"         HAVE_ARGZ_ADD)
+  check_symbol_exists(argz_append "argz.h"      HAVE_ARGZ_APPEND)
+  check_symbol_exists(argz_count "argz.h"       HAVE_ARGZ_COUNT)
+  check_symbol_exists(argz_create_sep "argz.h"  HAVE_ARGZ_CREATE_SEP)
+  check_symbol_exists(argz_insert "argz.h"      HAVE_ARGZ_INSERT)
+  check_symbol_exists(argz_next "argz.h"        HAVE_ARGZ_NEXT)
+  check_symbol_exists(argz_stringify "argz.h"   HAVE_ARGZ_STRINGIFY)
+
+  check_function_exists(atoll HAVE_ATOLL)
+  check_function_exists(closedir HAVE_CLOSEDIR)
+  check_function_exists(endpwent HAVE_ENDPWENT)
+
+  if (HAVE_ARGZ_H)
+    set(HAVE_WORKING_ARGZ 1)
+  endif()
+
+  find_package(Threads)
+  if (Threads_FOUND)
+    set(HAVE_LIBPTHREAD 1)
+  endif ()
+
+  set(CMAKE_REQUIRED_LIBRARIES)
+  if (HAVE_DLFCN_H)
+    list(APPEND CMAKE_REQUIRED_LIBRARIES "dl")
+  endif()
+  if (HAVE_CRYPT_H)
+    list(APPEND CMAKE_REQUIRED_LIBRARIES "crypt")
+  endif()
+  if (HAVE_READLINE_H)
+    list(APPEND CMAKE_REQUIRED_LIBRARIES "readline")
+  endif()
+  if (HAVE_LT_DLLOADER_H)
+    set(HAVE_LIBDLLOADER 0)  # to improve
+    set(HAVE_LTDL 1)  # to improve
+  endif()
+
+  check_function_exists(dlerror        HAVE_DLERROR)
+  check_function_exists(dlloader_init  HAVE_LIBDLLOADER)
+  check_function_exists(dlopen         HAVE_LIBDL)
+  check_function_exists(encrypt        HAVE_LIBCRYPT)
+  check_function_exists(ftime          HAVE_FTIME)
+  check_function_exists(getpwuid       HAVE_GETPWUID)
+  check_function_exists(gettimeofday   HAVE_GETTIMEOFDAY)
+  check_function_exists(gettimeofday   HAVE_GETTIMEOFDAY)
+  check_function_exists(getuid         HAVE_GETUID)
+  check_function_exists(iconv          HAVE_ICONV)
+  check_function_exists(localtime_r    HAVE_LOCALTIME_R)
+  check_function_exists(opendir        HAVE_OPENDIR)
+  check_function_exists(putenv         HAVE_PUTENV)
+  check_function_exists(readdir        HAVE_READDIR)
+  check_function_exists(readline       HAVE_READLINE)
+  check_function_exists(setenv         HAVE_SETENV)
+  check_function_exists(setlocale      HAVE_SETLOCALE)
+  check_function_exists(socket         HAVE_SOCKET)
+  check_function_exists(strcasecmp     HAVE_STRCASECMP)
+  check_function_exists(strchr         HAVE_STRCHR)
+  check_function_exists(strdup         HAVE_STRDUP)
+  check_function_exists(strncasecmp    HAVE_STRNCASECMP)
+  check_function_exists(strstr         HAVE_STRSTR)
+  check_function_exists(strtol         HAVE_STRTOL)
+  check_function_exists(strtoll        HAVE_STRTOLL)
+  check_function_exists(time           HAVE_TIME)
+  check_function_exists(vprintf        HAVE_VPRINTF)
+  check_function_exists(vsnprintf      HAVE_VSNPRINTF)
+
+  set(CMAKE_EXTRA_INCLUDE_FILES)
+  if (HAVE_ARGZ_H)
+    list(APPEND CMAKE_EXTRA_INCLUDE_FILES "argz.h")
+  endif()
+
+  check_type_size("long" SIZEOF_LONG)
+  check_type_size("long int" SIZEOF_LONG_INT)
+
+  check_type_size("error_t" HAVE_ERROR_T)
+  if (DEFINED HAVE_ERROR_T)
+    set(HAVE_ERROR_T 1)
+  endif()
+
+  check_type_size("long long" HAVE_LONG_LONG)
+  if (DEFINED HAVE_LONG_LONG)
+    set(HAVE_LONG_LONG 1)
+  endif()
+
+  check_type_size("nl_langinfo" HAVE_LANGINFO_CODESET)
+  if (DEFINED HAVE_LANGINFO_CODESET)
+    set(HAVE_LANGINFO_CODESET 1)  # to improve
+    set(HAVE_NL_LANGINFO 1)
+  endif()
+
+  check_type_size("ptrdiff_t" HAVE_PTRDIFF_T)
+  if (DEFINED HAVE_PTRDIFF_T)
+    set(HAVE_PTRDIFF_T 1)
+  endif()
+
+  configure_file(
+    ${CMAKE_CURRENT_LIST_DIR}/../Odbc/config.h.in
+    ${CMAKE_CURRENT_BINARY_DIR}/AUTOGENERATED/config.h
+    )
+
+  configure_file(
+    ${CMAKE_CURRENT_LIST_DIR}/../Odbc/config.h.in
+    ${CMAKE_CURRENT_BINARY_DIR}/AUTOGENERATED/unixodbc_conf.h
+    )
+
+  add_definitions(
+    -DHAVE_CONFIG_H=1
+    )
+
+else()
+  check_include_file("sqlext.h" HAVE_UNIX_ODBC_H)
+  if (NOT HAVE_UNIX_ODBC_H)
+    message(FATAL_ERROR "Please install the unixodbc-dev package")
+  endif()
+
+  check_include_file("libltdl/lt_dlloader.h"  HAVE_LT_DLLOADER_H)
+  if (NOT HAVE_LT_DLLOADER_H)
+    message(FATAL_ERROR "Please install the libltdl-dev package")
+  endif()
+
+  link_libraries(odbc ltdl)
+endif()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Odbc/config.h.in	Tue Aug 10 20:08:53 2021 +0200
@@ -0,0 +1,507 @@
+/* config.h.in.  Generated from configure.ac by autoheader.  */
+
+/* Encoding to use for CHAR */
+#cmakedefine ASCII_ENCODING "@ASCII_ENCODING@"
+
+/* Install bindir */
+#cmakedefine BIN_PREFIX
+
+/* Use a semaphore to allow ODBCConfig to display running counts */
+#cmakedefine COLLECT_STATS
+
+/* Define to one of `_getb67', `GETB67', `getb67' for Cray-2 and Cray-YMP
+   systems. This function is required for `alloca.c' support on those systems.
+   */
+#cmakedefine CRAY_STACKSEG_END
+
+/* Define to 1 if using `alloca.c'. */
+#cmakedefine C_ALLOCA
+
+/* Lib directory */
+#define DEFLIB_PATH "@DEFLIB_PATH@"
+
+/* Using perdriver iconv */
+#cmakedefine ENABLE_DRIVER_ICONV
+
+/* Using ini cacheing */
+#cmakedefine ENABLE_INI_CACHING
+
+/* Install exec_prefix */
+#cmakedefine EXEC_PREFIX
+
+/* Disable the precise but slow checking of the validity of handles */
+#cmakedefine FAST_HANDLE_VALIDATE
+
+/* Define to 1 if you have `alloca', as a function or macro. */
+#cmakedefine HAVE_ALLOCA @HAVE_ALLOCA@
+
+/* Define to 1 if you have <alloca.h> and it should be used (not on Ultrix).
+   */
+#cmakedefine HAVE_ALLOCA_H @HAVE_ALLOCA_H@
+
+/* Define to 1 if you have the `argz_add' function. */
+#cmakedefine HAVE_ARGZ_ADD @HAVE_ARGZ_ADD@
+
+/* Define to 1 if you have the `argz_append' function. */
+#cmakedefine HAVE_ARGZ_APPEND @HAVE_ARGZ_APPEND@
+
+/* Define to 1 if you have the `argz_count' function. */
+#cmakedefine HAVE_ARGZ_COUNT @HAVE_ARGZ_COUNT@
+
+/* Define to 1 if you have the `argz_create_sep' function. */
+#cmakedefine HAVE_ARGZ_CREATE_SEP @HAVE_ARGZ_CREATE_SEP@
+
+/* Define to 1 if you have the <argz.h> header file. */
+#cmakedefine HAVE_ARGZ_H @HAVE_ARGZ_H@
+
+/* Define to 1 if you have the `argz_insert' function. */
+#cmakedefine HAVE_ARGZ_INSERT @HAVE_ARGZ_INSERT@
+
+/* Define to 1 if you have the `argz_next' function. */
+#cmakedefine HAVE_ARGZ_NEXT @HAVE_ARGZ_NEXT@
+
+/* Define to 1 if you have the `argz_stringify' function. */
+#cmakedefine HAVE_ARGZ_STRINGIFY @HAVE_ARGZ_STRINGIFY@
+
+/* Define to 1 if you have the `atoll' function. */
+#cmakedefine HAVE_ATOLL @HAVE_ATOLL@
+
+/* Define to 1 if you have the `closedir' function. */
+#cmakedefine HAVE_CLOSEDIR @HAVE_CLOSEDIR@
+
+/* Define to 1 if you have the <crypt.h> header file. */
+#cmakedefine HAVE_CRYPT_H @HAVE_CRYPT_H@
+
+/* Define to 1 if you have the declaration of `cygwin_conv_path', and to 0 if
+   you don't. */
+#cmakedefine HAVE_DECL_CYGWIN_CONV_PATH @HAVE_DECL_CYGWIN_CONV_PATH@
+
+/* Define to 1 if you have the <dirent.h> header file, and it defines `DIR'.
+   */
+#cmakedefine HAVE_DIRENT_H @HAVE_DIRENT_H@
+
+/* Define if you have the GNU dld library. */
+#cmakedefine HAVE_DLD @HAVE_DLD@
+
+/* Define to 1 if you have the <dld.h> header file. */
+#cmakedefine HAVE_DLD_H @HAVE_DLD_H@
+
+/* Define to 1 if you have the `dlerror' function. */
+#cmakedefine HAVE_DLERROR @HAVE_DLERROR@
+
+/* Define to 1 if you have the <dlfcn.h> header file. */
+#cmakedefine HAVE_DLFCN_H @HAVE_DLFCN_H@
+
+/* Define to 1 if you have the <dl.h> header file. */
+#cmakedefine HAVE_DL_H @HAVE_DL_H@
+
+/* Define to 1 if you don't have `vprintf' but do have `_doprnt.' */
+#cmakedefine HAVE_DOPRNT @HAVE_DOPRNT@
+
+/* Define if you have the _dyld_func_lookup function. */
+#cmakedefine HAVE_DYLD @HAVE_DYLD@
+
+/* Add editline support */
+#cmakedefine HAVE_EDITLINE @HAVE_EDITLINE@
+
+/* Define to 1 if you have the <editline/readline.h> header file. */
+#cmakedefine HAVE_EDITLINE_READLINE_H @HAVE_EDITLINE_READLINE_H@
+
+/* Define to 1 if you have the `endpwent' function. */
+#cmakedefine HAVE_ENDPWENT @HAVE_ENDPWENT@
+
+/* Define to 1 if the system has the type `error_t'. */
+#cmakedefine HAVE_ERROR_T @HAVE_ERROR_T@
+
+/* Define to 1 if you have the `ftime' function. */
+#cmakedefine HAVE_FTIME @HAVE_FTIME@
+
+/* Define to 1 if you have the `ftok' function. */
+#cmakedefine HAVE_FTOK @HAVE_FTOK@
+
+/* Define to 1 if you have the `getpwuid' function. */
+#cmakedefine HAVE_GETPWUID @HAVE_GETPWUID@
+
+/* Define to 1 if you have the `gettimeofday' function. */
+#cmakedefine HAVE_GETTIMEOFDAY @HAVE_GETTIMEOFDAY@
+
+/* Define to 1 if you have the `getuid' function. */
+#cmakedefine HAVE_GETUID @HAVE_GETUID@
+
+/* Define if you have the iconv() function. */
+#cmakedefine HAVE_ICONV @HAVE_ICONV@
+
+/* Define to 1 if you have the <inttypes.h> header file. */
+#cmakedefine HAVE_INTTYPES_H @HAVE_INTTYPES_H@
+
+/* Define if you have <langinfo.h> and nl_langinfo(CODESET). */
+#cmakedefine HAVE_LANGINFO_CODESET @HAVE_LANGINFO_CODESET@
+
+/* Define to 1 if you have the <langinfo.h> header file. */
+#cmakedefine HAVE_LANGINFO_H @HAVE_LANGINFO_H@
+
+/* Add -lcrypt to lib list */
+#cmakedefine HAVE_LIBCRYPT @HAVE_LIBCRYPT@
+
+/* Define if you have the libdl library or equivalent. */
+#cmakedefine HAVE_LIBDL @HAVE_LIBDL@
+
+/* Define if libdlloader will be built on this platform */
+#cmakedefine HAVE_LIBDLLOADER @HAVE_LIBDLLOADER@
+
+/* Use the -lpth thread library */
+#cmakedefine HAVE_LIBPTH @HAVE_LIBPTH@
+
+/* Use -lpthread threading lib */
+#cmakedefine HAVE_LIBPTHREAD @HAVE_LIBPTHREAD@
+
+/* Use the -lthread threading lib */
+#cmakedefine HAVE_LIBTHREAD @HAVE_LIBTHREAD@
+
+/* Define to 1 if you have the <limits.h> header file. */
+#cmakedefine HAVE_LIMITS_H @HAVE_LIMITS_H@
+
+/* Define to 1 if you have the <locale.h> header file. */
+#cmakedefine HAVE_LOCALE_H @HAVE_LOCALE_H@
+
+/* Use rentrant version of localtime */
+#cmakedefine HAVE_LOCALTIME_R @HAVE_LOCALTIME_R@
+
+/* Define if you have long long */
+#cmakedefine HAVE_LONG_LONG @HAVE_LONG_LONG@
+
+/* Define this if a modern libltdl is already installed */
+#cmakedefine HAVE_LTDL @HAVE_LTDL@
+
+/* Define to 1 if you have the <mach-o/dyld.h> header file. */
+#cmakedefine HAVE_MACH_O_DYLD_H @HAVE_MACH_O_DYLD_H@
+
+/* Define to 1 if you have the <malloc.h> header file. */
+#cmakedefine HAVE_MALLOC_H @HAVE_MALLOC_H@
+
+/* Define to 1 if you have the <memory.h> header file. */
+#cmakedefine HAVE_MEMORY_H @HAVE_MEMORY_H@
+
+/* Define to 1 if you have the <msql.h> header file. */
+#cmakedefine HAVE_MSQL_H @HAVE_MSQL_H@
+
+/* Define to 1 if you have the <ndir.h> header file, and it defines `DIR'. */
+#cmakedefine HAVE_NDIR_H @HAVE_NDIR_H@
+
+/* Define to 1 if you have the `nl_langinfo' function. */
+#cmakedefine HAVE_NL_LANGINFO @HAVE_NL_LANGINFO@
+
+/* Define to 1 if you have the `opendir' function. */
+#cmakedefine HAVE_OPENDIR @HAVE_OPENDIR@
+
+/* Define if libtool can extract symbol lists from object files. */
+#cmakedefine HAVE_PRELOADED_SYMBOLS @HAVE_PRELOADED_SYMBOLS@
+
+/* Define to 1 if the system has the type `ptrdiff_t'. */
+#cmakedefine HAVE_PTRDIFF_T @HAVE_PTRDIFF_T@
+
+/* Define to 1 if you have the `putenv' function. */
+#cmakedefine HAVE_PUTENV @HAVE_PUTENV@
+
+/* Define to 1 if you have the <pwd.h> header file. */
+#cmakedefine HAVE_PWD_H @HAVE_PWD_H@
+
+/* Define to 1 if you have the `readdir' function. */
+#cmakedefine HAVE_READDIR @HAVE_READDIR@
+
+/* Add readline support */
+#cmakedefine HAVE_READLINE @HAVE_READLINE@
+
+/* Define to 1 if you have the <readline/history.h> header file. */
+#cmakedefine HAVE_READLINE_HISTORY_H @HAVE_READLINE_HISTORY_H@
+
+/* Use the scandir lib */
+#cmakedefine HAVE_SCANDIR @HAVE_SCANDIR@
+
+/* Define to 1 if you have the `semget' function. */
+#cmakedefine HAVE_SEMGET @HAVE_SEMGET@
+
+/* Define to 1 if you have the `semop' function. */
+#cmakedefine HAVE_SEMOP @HAVE_SEMOP@
+
+/* Define to 1 if you have the `setenv' function. */
+#cmakedefine HAVE_SETENV @HAVE_SETENV@
+
+/* Define to 1 if you have the `setlocale' function. */
+#cmakedefine HAVE_SETLOCALE @HAVE_SETLOCALE@
+
+/* Define if you have the shl_load function. */
+#cmakedefine HAVE_SHL_LOAD @HAVE_SHL_LOAD@
+
+/* Define to 1 if you have the `shmget' function. */
+#cmakedefine HAVE_SHMGET @HAVE_SHMGET@
+
+/* Define to 1 if you have the `snprintf' function. */
+#cmakedefine HAVE_SNPRINTF @HAVE_SNPRINTF@
+
+/* Define to 1 if you have the `socket' function. */
+#cmakedefine HAVE_SOCKET @HAVE_SOCKET@
+
+/* Define to 1 if you have the <stdarg.h> header file. */
+#cmakedefine HAVE_STDARG_H @HAVE_STDARG_H@
+
+/* Define to 1 if you have the <stddef.h> header file. */
+#cmakedefine HAVE_STDDEF_H @HAVE_STDDEF_H@
+
+/* Define to 1 if you have the <stdint.h> header file. */
+#cmakedefine HAVE_STDINT_H @HAVE_STDINT_H@
+
+/* Define to 1 if you have the <stdlib.h> header file. */
+#cmakedefine HAVE_STDLIB_H @HAVE_STDLIB_H@
+
+/* Define to 1 if you have the `strcasecmp' function. */
+#cmakedefine HAVE_STRCASECMP @HAVE_STRCASECMP@
+
+/* Define to 1 if you have the `strchr' function. */
+#cmakedefine HAVE_STRCHR @HAVE_STRCHR@
+
+/* Define to 1 if you have the `strdup' function. */
+#cmakedefine HAVE_STRDUP @HAVE_STRDUP@
+
+/* Define to 1 if you have the `stricmp' function. */
+#cmakedefine HAVE_STRICMP @HAVE_STRICMP@
+
+/* Define to 1 if you have the <strings.h> header file. */
+#cmakedefine HAVE_STRINGS_H @HAVE_STRINGS_H@
+
+/* Define to 1 if you have the <string.h> header file. */
+#cmakedefine HAVE_STRING_H @HAVE_STRING_H@
+
+/* Define to 1 if you have the `strlcat' function. */
+#cmakedefine HAVE_STRLCAT @HAVE_STRLCAT@
+
+/* Define to 1 if you have the `strlcpy' function. */
+#cmakedefine HAVE_STRLCPY @HAVE_STRLCPY@
+
+/* Define to 1 if you have the `strncasecmp' function. */
+#cmakedefine HAVE_STRNCASECMP @HAVE_STRNCASECMP@
+
+/* Define to 1 if you have the `strnicmp' function. */
+#cmakedefine HAVE_STRNICMP @HAVE_STRNICMP@
+
+/* Define to 1 if you have the `strstr' function. */
+#cmakedefine HAVE_STRSTR @HAVE_STRSTR@
+
+/* Define to 1 if you have the `strtol' function. */
+#cmakedefine HAVE_STRTOL @HAVE_STRTOL@
+
+/* Define to 1 if you have the `strtoll' function. */
+#cmakedefine HAVE_STRTOLL @HAVE_STRTOLL@
+
+/* Define to 1 if you have the <synch.h> header file. */
+#cmakedefine HAVE_SYNCH_H @HAVE_SYNCH_H@
+
+/* Define to 1 if you have the <sys/dir.h> header file, and it defines `DIR'.
+   */
+#cmakedefine HAVE_SYS_DIR_H @HAVE_SYS_DIR_H@
+
+/* Define to 1 if you have the <sys/dl.h> header file. */
+#cmakedefine HAVE_SYS_DL_H @HAVE_SYS_DL_H@
+
+/* Define to 1 if you have the <sys/malloc.h> header file. */
+#cmakedefine HAVE_SYS_MALLOC_H @HAVE_SYS_MALLOC_H@
+
+/* Define to 1 if you have the <sys/ndir.h> header file, and it defines `DIR'.
+   */
+#cmakedefine HAVE_SYS_NDIR_H @HAVE_SYS_NDIR_H@
+
+/* Define to 1 if you have the <sys/sem.h> header file. */
+#cmakedefine HAVE_SYS_SEM_H @HAVE_SYS_SEM_H@
+
+/* Define to 1 if you have the <sys/stat.h> header file. */
+#cmakedefine HAVE_SYS_STAT_H @HAVE_SYS_STAT_H@
+
+/* Define to 1 if you have the <sys/timeb.h> header file. */
+#cmakedefine HAVE_SYS_TIMEB_H @HAVE_SYS_TIMEB_H@
+
+/* Define to 1 if you have the <sys/time.h> header file. */
+#cmakedefine HAVE_SYS_TIME_H @HAVE_SYS_TIME_H@
+
+/* Define to 1 if you have the <sys/types.h> header file. */
+#cmakedefine HAVE_SYS_TYPES_H @HAVE_SYS_TYPES_H@
+
+/* Define to 1 if you have the `time' function. */
+#cmakedefine HAVE_TIME @HAVE_TIME@
+
+/* Define to 1 if you have the <time.h> header file. */
+#cmakedefine HAVE_TIME_H @HAVE_TIME_H@
+
+/* Define to 1 if you have the <unistd.h> header file. */
+#cmakedefine HAVE_UNISTD_H @HAVE_UNISTD_H@
+
+/* Define to 1 if you have the <varargs.h> header file. */
+#cmakedefine HAVE_VARARGS_H @HAVE_VARARGS_H@
+
+/* Define to 1 if you have the `vprintf' function. */
+#cmakedefine HAVE_VPRINTF @HAVE_VPRINTF@
+
+/* Define to 1 if you have the `vsnprintf' function. */
+#cmakedefine HAVE_VSNPRINTF @HAVE_VSNPRINTF@
+
+/* This value is set to 1 to indicate that the system argz facility works */
+#cmakedefine HAVE_WORKING_ARGZ @HAVE_WORKING_ARGZ@
+
+/* Define as const if the declaration of iconv() needs const. */
+#cmakedefine ICONV_CONST
+
+/* Install includedir */
+#cmakedefine INCLUDE_PREFIX
+
+/* Lib directory */
+#cmakedefine LIB_PREFIX
+
+/* Define if the OS needs help to load dependent libraries for dlopen(). */
+#cmakedefine LTDL_DLOPEN_DEPLIBS
+
+/* Define to the system default library search path. */
+#cmakedefine LT_DLSEARCH_PATH
+
+/* The archive extension */
+#cmakedefine LT_LIBEXT
+
+/* The archive prefix */
+#cmakedefine LT_LIBPREFIX
+
+/* Define to the extension used for runtime loadable modules, say, ".so". */
+#cmakedefine LT_MODULE_EXT
+
+/* Define to the name of the environment variable that determines the run-time
+   module search path. */
+#cmakedefine LT_MODULE_PATH_VAR
+
+/* Define to the sub-directory where libtool stores uninstalled libraries. */
+#cmakedefine LT_OBJDIR
+
+/* Define to the shared library suffix, say, ".dylib". */
+#cmakedefine LT_SHARED_EXT
+
+/* Define to the shared archive member specification, say "(shr.o)". */
+#cmakedefine LT_SHARED_LIB_MEMBER
+
+/* Define if you need semundo union */
+#cmakedefine NEED_SEMUNDO_UNION
+
+/* Define if dlsym() requires a leading underscore in symbol names. */
+#cmakedefine NEED_USCORE
+
+/* Using OSX */
+#cmakedefine OSXHEADER
+
+/* Name of package */
+#cmakedefine PACKAGE
+
+/* Define to the address where bug reports for this package should be sent. */
+#cmakedefine PACKAGE_BUGREPORT
+
+/* Define to the full name of this package. */
+#cmakedefine PACKAGE_NAME
+
+/* Define to the full name and version of this package. */
+#cmakedefine PACKAGE_STRING
+
+/* Define to the one symbol short name of this package. */
+#cmakedefine PACKAGE_TARNAME
+
+/* Define to the home page for this package. */
+#cmakedefine PACKAGE_URL
+
+/* Define to the version of this package. */
+#cmakedefine PACKAGE_VERSION
+
+/* Platform is 64 bit */
+#cmakedefine PLATFORM64
+
+/* Install prefix */
+#cmakedefine PREFIX
+
+/* Using QNX */
+#cmakedefine QNX_LIBLTDL
+
+/* Shared lib extension */
+#define SHLIBEXT "@SHLIBEXT@"
+
+/* The size of `long', as computed by sizeof. */
+#define SIZEOF_LONG @SIZEOF_LONG@
+
+/* The size of `long int', as computed by sizeof. */
+#define SIZEOF_LONG_INT @SIZEOF_LONG_INT@
+
+/* If using the C implementation of alloca, define if you know the
+   direction of stack growth for your system; otherwise it will be
+   automatically deduced at runtime.
+	STACK_DIRECTION > 0 => grows toward higher addresses
+	STACK_DIRECTION < 0 => grows toward lower addresses
+	STACK_DIRECTION = 0 => direction of growth unknown */
+#cmakedefine STACK_DIRECTION
+
+/* Filename to use for ftok */
+#cmakedefine STATS_FTOK_NAME
+
+/* Define to 1 if you have the ANSI C header files. */
+#cmakedefine STDC_HEADERS @STDC_HEADERS@
+
+/* don't include unixODBC prefix in driver error messages */
+#cmakedefine STRICT_ODBC_ERROR
+
+/* System file path */
+#cmakedefine SYSTEM_FILE_PATH "@SYSTEM_FILE_PATH@"
+
+/* Lib path */
+#cmakedefine SYSTEM_LIB_PATH "@SYSTEM_LIB_PATH@"
+
+/* Define to 1 if you can safely include both <sys/time.h> and <time.h>. */
+#cmakedefine TIME_WITH_SYS_TIME
+
+/* Define to 1 if your <sys/time.h> declares `struct tm'. */
+#cmakedefine TM_IN_SYS_TIME
+
+/* Encoding to use for UNICODE */
+#cmakedefine UNICODE_ENCODING
+
+/* Flag that we are not using another DM */
+#cmakedefine UNIXODBC
+
+/* We are building inside the unixODBC source tree */
+#cmakedefine UNIXODBC_SOURCE
+
+/* Version number of package */
+#define VERSION "@VERSION@"
+
+/* Work with IBM drivers that use 32 bit handles on 64 bit platforms */
+#cmakedefine WITH_HANDLE_REDIRECT
+
+/* Define to 1 if `lex' declares `yytext' as a `char *' by default, not a
+   `char[]'. */
+#cmakedefine YYTEXT_POINTER
+
+/* Build flag for AIX */
+#cmakedefine _ALL_SOURCE
+
+/* Build flag for AIX */
+#cmakedefine _LONG_LONG
+
+/* Build flag for AIX */
+#cmakedefine _THREAD_SAFE
+
+/* Define so that glibc/gnulib argp.h does not typedef error_t. */
+#cmakedefine __error_t_defined
+
+/* Define to empty if `const' does not conform to ANSI C. */
+#undef const
+
+/* Define to a type to use for 'error_t' if it is not otherwise available. */
+#undef error_t
+
+/* Define to `int' if <sys/types.h> doesn't define. */
+#undef gid_t
+
+/* Define to `unsigned int' if <sys/types.h> does not define. */
+#undef size_t
+
+/* Define to `int' if <sys/types.h> doesn't define. */
+#undef uid_t
--- a/SQLite/CMakeLists.txt	Thu Jul 22 20:20:26 2021 +0200
+++ b/SQLite/CMakeLists.txt	Tue Aug 10 20:08:53 2021 +0200
@@ -1,3 +1,22 @@
+# Orthanc - A Lightweight, RESTful DICOM Store
+# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+# Department, University Hospital of Liege, Belgium
+# Copyright (C) 2017-2021 Osimis S.A., Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the GNU Affero General Public License
+# as published by the Free Software Foundation, either version 3 of
+# the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
 cmake_minimum_required(VERSION 2.8)
 project(OrthancSQLite)