changeset 20:aacb651833f5

merge
author am@osimis.io
date Tue, 10 Jul 2018 11:08:19 +0200
parents 38e23471d132 (current diff) c7c54993a92e (diff)
children 2e5d2c69d4f9
files PostgreSQL/Plugins/PostgreSQLIndex.cpp
diffstat 20 files changed, 526 insertions(+), 330 deletions(-) [+]
line wrap: on
line diff
--- a/Framework/Common/DatabaseManager.cpp	Tue Jul 10 11:05:37 2018 +0200
+++ b/Framework/Common/DatabaseManager.cpp	Tue Jul 10 11:08:19 2018 +0200
@@ -307,8 +307,9 @@
                                                     const char* sql) :
     lock_(manager.mutex_),
     manager_(manager),
+    database_(manager_.GetDatabase()),
     location_(location),
-    transaction_(manager.GetTransaction())
+    transaction_(manager_.GetTransaction())
   {
     Setup(sql);
   }
@@ -319,6 +320,7 @@
                                                     const char* sql) :
     lock_(manager_.mutex_),
     manager_(transaction.GetManager()),
+    database_(manager_.GetDatabase()),
     location_(location),
     transaction_(manager_.GetTransaction())
   {
--- a/Framework/Common/DatabaseManager.h	Tue Jul 10 11:05:37 2018 +0200
+++ b/Framework/Common/DatabaseManager.h	Tue Jul 10 11:08:19 2018 +0200
@@ -123,6 +123,7 @@
     private:
       boost::recursive_mutex::scoped_lock  lock_;
       DatabaseManager&                     manager_;
+      IDatabase&                           database_;
       StatementLocation                    location_;
       ITransaction&                        transaction_;
       IPrecompiledStatement*               statement_;
@@ -142,6 +143,11 @@
                       Transaction& transaction,
                       const char* sql);
 
+      IDatabase& GetDatabase()
+      {
+        return database_;
+      }
+
       void SetReadOnly(bool readOnly);
 
       void SetParameterType(const std::string& parameter,
--- a/Framework/MySQL/MySQLDatabase.cpp	Tue Jul 10 11:05:37 2018 +0200
+++ b/Framework/MySQL/MySQLDatabase.cpp	Tue Jul 10 11:08:19 2018 +0200
@@ -145,38 +145,127 @@
       Close();
       throw Orthanc::OrthancException(Orthanc::ErrorCode_Database);        
     }
+  }
 
-    if (parameters_.HasLock())
-    {
-      try
-      {
-        Query query("SELECT GET_LOCK('Lock', 0);", false);
-        MySQLStatement statement(*this, query);
 
-        MySQLTransaction t(*this);
-        Dictionary args;
-
-        std::auto_ptr<IResult> result(t.Execute(statement, args));
+  namespace
+  {
+    class ResultWrapper : public boost::noncopyable
+    {
+    private:
+      MYSQL_RES *result_;
 
-        if (result->IsDone() ||
-            result->GetField(0).GetType() != ValueType_Integer64 ||
-            dynamic_cast<const Integer64Value&>(result->GetField(0)).GetValue() != 1)
+    public:
+      ResultWrapper(MySQLDatabase& mysql,
+                    const std::string& sql) :
+        result_(NULL)
+      {
+        if (mysql_real_query(mysql.GetObject(), sql.c_str(), sql.size()))
         {
+          mysql.LogError();
           throw Orthanc::OrthancException(Orthanc::ErrorCode_Database);
         }
 
-        t.Commit();
+        result_ = mysql_use_result(mysql.GetObject());
+        if (result_ == NULL)
+        {
+          mysql.LogError();
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_Database);
+        }
       }
-      catch (Orthanc::OrthancException&)
+
+      ~ResultWrapper()
+      {
+        if (result_ != NULL)
+        {
+          mysql_free_result(result_);
+          result_ = NULL;
+        }
+      }
+
+      MYSQL_RES *GetObject()
       {
-        LOG(ERROR) << "The MySQL database is locked by another instance of Orthanc";
-        Close();
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_Database);
+        return result_;
       }
+    };
+  }
+
+
+  bool MySQLDatabase::LookupGlobalStringVariable(std::string& value,
+                                                 const std::string& variable)
+  {
+    ResultWrapper result(*this, "SELECT @@global." + variable);
+
+    MYSQL_ROW row = mysql_fetch_row(result.GetObject());
+    if (mysql_errno(mysql_) == 0 &&
+        row &&
+        row[0])
+    {
+      value = std::string(row[0]);
+      return true;
+    }
+    else
+    {
+      return false;
     }
   }
 
   
+  bool MySQLDatabase::LookupGlobalIntegerVariable(int64_t& value,
+                                                  const std::string& variable)
+  {
+    std::string s;
+    
+    if (LookupGlobalStringVariable(s, variable))
+    {
+      try
+      {
+        value = boost::lexical_cast<int64_t>(s);
+        return true;
+      }
+      catch (boost::bad_lexical_cast&)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_Database);
+      }
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
+  void MySQLDatabase::AdvisoryLock(int32_t lock)
+  {
+    try
+    {
+      Query query("SELECT GET_LOCK('Lock" +
+                  boost::lexical_cast<std::string>(lock) + "', 0);", false);
+      MySQLStatement statement(*this, query);
+
+      MySQLTransaction t(*this);
+      Dictionary args;
+
+      std::auto_ptr<IResult> result(t.Execute(statement, args));
+
+      if (result->IsDone() ||
+          result->GetField(0).GetType() != ValueType_Integer64 ||
+          dynamic_cast<const Integer64Value&>(result->GetField(0)).GetValue() != 1)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_Database);
+      }
+
+      t.Commit();
+    }
+    catch (Orthanc::OrthancException&)
+    {
+      LOG(ERROR) << "The MySQL database is locked by another instance of Orthanc";
+      Close();
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_Database);
+    }
+  }
+  
+
   bool MySQLDatabase::DoesTableExist(MySQLTransaction& transaction,
                                      const std::string& name)
   {
@@ -212,7 +301,8 @@
   }
 
 
-  void MySQLDatabase::Execute(const std::string& sql)
+  void MySQLDatabase::Execute(const std::string& sql,
+                              bool arobaseSeparator)
   {
     if (mysql_ == NULL)
     {
@@ -231,8 +321,11 @@
 
       if (!s.empty())
       {
-        // Replace the escape character "@" by a semicolon
-        std::replace(s.begin(), s.end(), '@', ';');
+        if (arobaseSeparator)
+        {
+          // Replace the escape character "@" by a semicolon
+          std::replace(s.begin(), s.end(), '@', ';');
+        }
       
         LOG(TRACE) << "MySQL: " << s;
         CheckErrorCode(mysql_query(mysql_, s.c_str()));
--- a/Framework/MySQL/MySQLDatabase.h	Tue Jul 10 11:05:37 2018 +0200
+++ b/Framework/MySQL/MySQLDatabase.h	Tue Jul 10 11:08:19 2018 +0200
@@ -58,7 +58,16 @@
 
     void Open();
 
-    void Execute(const std::string& sql);
+    bool LookupGlobalStringVariable(std::string& value,
+                                    const std::string& variable);
+    
+    bool LookupGlobalIntegerVariable(int64_t& value,
+                                     const std::string& variable);
+
+    void AdvisoryLock(int32_t lock);
+
+    void Execute(const std::string& sql,
+                 bool arobaseSeparator);
 
     bool DoesTableExist(MySQLTransaction& transaction,
                         const std::string& name);
--- a/Framework/MySQL/MySQLResult.cpp	Tue Jul 10 11:05:37 2018 +0200
+++ b/Framework/MySQL/MySQLResult.cpp	Tue Jul 10 11:08:19 2018 +0200
@@ -36,9 +36,11 @@
     if (code == 1)
     {
       unsigned int error = mysql_errno(database_.GetObject());
+
       if (error == 0)
       {
-        // This case can occur if the SQL request is not a SELECT
+        // This case occurs in requests without a result (e.g. if the
+        // SQL request is not a SELECT)
         done_ = true;
       }
       else if (error == CR_SERVER_GONE_ERROR ||
--- a/Framework/MySQL/MySQLTransaction.cpp	Tue Jul 10 11:05:37 2018 +0200
+++ b/Framework/MySQL/MySQLTransaction.cpp	Tue Jul 10 11:08:19 2018 +0200
@@ -35,7 +35,7 @@
     readOnly_(true),
     active_(false)
   {
-    db_.Execute("START TRANSACTION");
+    db_.Execute("START TRANSACTION", false);
     active_ = true;
   }
 
@@ -48,7 +48,7 @@
 
       try
       {
-        db_.Execute("ROLLBACK");
+        db_.Execute("ROLLBACK", false);
       }
       catch (Orthanc::OrthancException&)
       {
@@ -61,7 +61,7 @@
   {
     if (active_)
     {
-      db_.Execute("ROLLBACK");
+      db_.Execute("ROLLBACK", false);
       active_ = false;
       readOnly_ = true;
     }
@@ -77,7 +77,7 @@
   {
     if (active_)
     {
-      db_.Execute("COMMIT");
+      db_.Execute("COMMIT", false);
       active_ = false;
       readOnly_ = true;
     }
--- a/Framework/Plugins/OrthancCppDatabasePlugin.h	Tue Jul 10 11:05:37 2018 +0200
+++ b/Framework/Plugins/OrthancCppDatabasePlugin.h	Tue Jul 10 11:08:19 2018 +0200
@@ -1504,7 +1504,7 @@
 
       if (performanceWarning)
       {
-        OrthancPluginLogWarning(context, "Performance warning: The database plugin was compiled "
+        OrthancPluginLogWarning(context, "Performance warning: The database index plugin was compiled "
                                 "against an old version of the Orthanc SDK, consider upgrading");
       }
 
--- a/Framework/Plugins/StorageBackend.cpp	Tue Jul 10 11:05:37 2018 +0200
+++ b/Framework/Plugins/StorageBackend.cpp	Tue Jul 10 11:08:19 2018 +0200
@@ -25,6 +25,9 @@
 #  error HAS_ORTHANC_EXCEPTION must be set to 1
 #endif
 
+#include "../../Framework/Common/BinaryStringValue.h"
+#include "../../Framework/Common/FileValue.h"
+
 #include <Core/OrthancException.h>
 
 
@@ -111,6 +114,99 @@
   }
 
 
+  void StorageBackend::Create(DatabaseManager::Transaction& transaction,
+                              const std::string& uuid,
+                              const void* content,
+                              size_t size,
+                              OrthancPluginContentType type)
+  {
+    DatabaseManager::CachedStatement statement(
+      STATEMENT_FROM_HERE, GetManager(),
+      "INSERT INTO StorageArea VALUES (${uuid}, ${content}, ${type})");
+     
+    statement.SetParameterType("uuid", ValueType_Utf8String);
+    statement.SetParameterType("content", ValueType_File);
+    statement.SetParameterType("type", ValueType_Integer64);
+
+    Dictionary args;
+    args.SetUtf8Value("uuid", uuid);
+    args.SetFileValue("content", content, size);
+    args.SetIntegerValue("type", type);
+     
+    statement.Execute(args);
+  }
+
+
+  void StorageBackend::Read(void*& content,
+                            size_t& size,
+                            DatabaseManager::Transaction& transaction, 
+                            const std::string& uuid,
+                            OrthancPluginContentType type) 
+  {
+    DatabaseManager::CachedStatement statement(
+      STATEMENT_FROM_HERE, GetManager(),
+      "SELECT content FROM StorageArea WHERE uuid=${uuid} AND type=${type}");
+     
+    statement.SetParameterType("uuid", ValueType_Utf8String);
+    statement.SetParameterType("type", ValueType_Integer64);
+
+    Dictionary args;
+    args.SetUtf8Value("uuid", uuid);
+    args.SetIntegerValue("type", type);
+     
+    statement.Execute(args);
+
+    if (statement.IsDone())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);
+    }
+    else if (statement.GetResultFieldsCount() != 1)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_Database);        
+    }
+    else
+    {
+      const IValue& value = statement.GetResultField(0);
+      
+      switch (value.GetType())
+      {
+        case ValueType_File:
+          ReadFromString(content, size,
+                         dynamic_cast<const FileValue&>(value).GetContent());
+          break;
+
+        case ValueType_BinaryString:
+          ReadFromString(content, size,
+                         dynamic_cast<const BinaryStringValue&>(value).GetContent());
+          break;
+
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_Database);        
+      }
+    }
+  }
+
+
+  void StorageBackend::Remove(DatabaseManager::Transaction& transaction,
+                              const std::string& uuid,
+                              OrthancPluginContentType type)
+  {
+    DatabaseManager::CachedStatement statement(
+      STATEMENT_FROM_HERE, GetManager(),
+      "DELETE FROM StorageArea WHERE uuid=${uuid} AND type=${type}");
+     
+    statement.SetParameterType("uuid", ValueType_Utf8String);
+    statement.SetParameterType("type", ValueType_Integer64);
+
+    Dictionary args;
+    args.SetUtf8Value("uuid", uuid);
+    args.SetIntegerValue("type", type);
+     
+    statement.Execute(args);
+  }
+
+
+
   static OrthancPluginContext* context_ = NULL;
   static std::auto_ptr<StorageBackend>  backend_;
     
@@ -182,6 +278,7 @@
     {
       context_ = context;
       backend_.reset(backend);
+      backend_->GetManager().Open();
 
       OrthancPluginRegisterStorageArea(context_, StorageCreate, StorageRead, StorageRemove);
     }
--- a/Framework/Plugins/StorageBackend.h	Tue Jul 10 11:05:37 2018 +0200
+++ b/Framework/Plugins/StorageBackend.h	Tue Jul 10 11:08:19 2018 +0200
@@ -56,17 +56,17 @@
                         const std::string& uuid,
                         const void* content,
                         size_t size,
-                        OrthancPluginContentType type) = 0;
+                        OrthancPluginContentType type);
 
     virtual void Read(void*& content,
                       size_t& size,
                       DatabaseManager::Transaction& transaction, 
                       const std::string& uuid,
-                      OrthancPluginContentType type) = 0;
+                      OrthancPluginContentType type);
 
     virtual void Remove(DatabaseManager::Transaction& transaction,
                         const std::string& uuid,
-                        OrthancPluginContentType type) = 0;
+                        OrthancPluginContentType type);
 
     static void Register(OrthancPluginContext* context,
                          StorageBackend* backend);   // Takes ownership
--- a/MySQL/CMakeLists.txt	Tue Jul 10 11:05:37 2018 +0200
+++ b/MySQL/CMakeLists.txt	Tue Jul 10 11:08:19 2018 +0200
@@ -30,6 +30,7 @@
   )
 
 add_library(OrthancMySQLStorage SHARED
+  Plugins/MySQLStorageArea.cpp
   Plugins/StoragePlugin.cpp
   ${DATABASES_SOURCES}
   ${AUTOGENERATED_SOURCES}
@@ -63,7 +64,9 @@
 
 add_executable(UnitTests
   Plugins/MySQLIndex.cpp
+  Plugins/MySQLStorageArea.cpp
   UnitTests/UnitTestsMain.cpp
+  
   ${DATABASES_SOURCES}
   ${GOOGLE_TEST_SOURCES}
   ${AUTOGENERATED_SOURCES}
--- a/MySQL/Plugins/MySQLIndex.cpp	Tue Jul 10 11:05:37 2018 +0200
+++ b/MySQL/Plugins/MySQLIndex.cpp	Tue Jul 10 11:08:19 2018 +0200
@@ -82,39 +82,40 @@
       db.Open();
 
       MySQLTransaction t(db);
-      db.Execute("DROP DATABASE IF EXISTS " + database);
-      db.Execute("CREATE DATABASE " + database);
+      db.Execute("DROP DATABASE IF EXISTS " + database, false);
+      db.Execute("CREATE DATABASE " + database, false);
       t.Commit();
     }
     
     std::auto_ptr<MySQLDatabase> db(new MySQLDatabase(parameters_));
 
     db->Open();
-    db->Execute("ALTER DATABASE " + parameters_.GetDatabase() + 
-                " CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
-    db->Execute("SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE");
+    
+    db->Execute("SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE", false);
 
+    if (parameters_.HasLock())
+    {
+      db->AdvisoryLock(42 /* some arbitrary constant */);
+    }
+    
     {
       MySQLTransaction t(*db);
 
+      db->Execute("ALTER DATABASE " + parameters_.GetDatabase() + 
+                  " CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci", false);
+    
       if (!db->DoesTableExist(t, "Resources"))
       {
         std::string query;
 
         Orthanc::EmbeddedResources::GetFileResource
           (query, Orthanc::EmbeddedResources::MYSQL_PREPARE_INDEX);
-        db->Execute(query);
+        db->Execute(query, true);
 
         SetGlobalIntegerProperty(*db, t, Orthanc::GlobalProperty_DatabaseSchemaVersion, expectedVersion);
         SetGlobalIntegerProperty(*db, t, Orthanc::GlobalProperty_DatabasePatchLevel, 1);
       }
 
-      t.Commit();
-    }
-
-    {
-      MySQLTransaction t(*db);
-
       if (!db->DoesTableExist(t, "Resources"))
       {
         LOG(ERROR) << "Corrupted MySQL database";
@@ -142,7 +143,7 @@
         throw Orthanc::OrthancException(Orthanc::ErrorCode_Database);        
       }
 
-      t.Rollback();
+      t.Commit();
     }
           
     return db.release();
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MySQL/Plugins/MySQLStorageArea.cpp	Tue Jul 10 11:08:19 2018 +0200
@@ -0,0 +1,87 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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 "MySQLStorageArea.h"
+
+#include "../../Framework/MySQL/MySQLDatabase.h"
+#include "../../Framework/MySQL/MySQLTransaction.h"
+
+#include <Core/Logging.h>
+
+#include <boost/math/special_functions/round.hpp>
+
+
+namespace OrthancDatabases
+{
+  IDatabase* MySQLStorageArea::OpenInternal()
+  {
+    std::auto_ptr<MySQLDatabase> db(new MySQLDatabase(parameters_));
+
+    db->Open();
+
+    if (parameters_.HasLock())
+    {
+      db->AdvisoryLock(43 /* some arbitrary constant */);
+    }
+
+    {
+      MySQLTransaction t(*db);
+
+      int64_t size;
+      if (db->LookupGlobalIntegerVariable(size, "max_allowed_packet"))
+      {
+        int mb = boost::math::iround(static_cast<double>(size) /
+                                     static_cast<double>(1024 * 1024));
+        LOG(WARNING) << "Your MySQL server cannot "
+                     << "store DICOM files larger than " << mb << "MB";
+        LOG(WARNING) << "  => Consider increasing \"max_allowed_packet\" "
+                     << "in \"my.cnf\" if this limit is insufficient for your use";
+      }
+      else
+      {
+        LOG(WARNING) << "Unable to auto-detect the maximum size of DICOM "
+                     << "files that can be stored in this MySQL server";
+      }
+               
+      if (clearAll_)
+      {
+        db->Execute("DROP TABLE IF EXISTS StorageArea", false);
+      }
+
+      db->Execute("CREATE TABLE IF NOT EXISTS StorageArea("
+                  "uuid VARCHAR(64) NOT NULL PRIMARY KEY,"
+                  "content LONGBLOB NOT NULL,"
+                  "type INTEGER NOT NULL)", false);
+
+      t.Commit();
+    }
+
+    return db.release();
+  }
+
+
+  MySQLStorageArea::MySQLStorageArea(const MySQLParameters& parameters) :
+    StorageBackend(new Factory(*this)),
+    parameters_(parameters),
+    clearAll_(false)
+  {
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MySQL/Plugins/MySQLStorageArea.h	Tue Jul 10 11:08:19 2018 +0200
@@ -0,0 +1,69 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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/StorageBackend.h"
+#include "../../Framework/MySQL/MySQLParameters.h"
+
+
+namespace OrthancDatabases
+{
+  class MySQLStorageArea : public StorageBackend
+  {
+  private:
+    class Factory : public IDatabaseFactory
+    {
+    private:
+      MySQLStorageArea&  that_;
+
+    public:
+      Factory(MySQLStorageArea& that) :
+        that_(that)
+      {
+      }
+
+      virtual Dialect GetDialect() const
+      {
+        return Dialect_MySQL;
+      }
+
+      virtual IDatabase* Open()
+      {
+        return that_.OpenInternal();
+      }
+    };
+
+    OrthancPluginContext*  context_;
+    MySQLParameters        parameters_;
+    bool                   clearAll_;
+
+    IDatabase* OpenInternal();
+
+  public:
+    MySQLStorageArea(const MySQLParameters& parameters);
+
+    void SetClearAll(bool clear)
+    {
+      clearAll_ = clear;
+    }
+  };
+}
--- a/MySQL/Plugins/PrepareIndex.sql	Tue Jul 10 11:05:37 2018 +0200
+++ b/MySQL/Plugins/PrepareIndex.sql	Tue Jul 10 11:08:19 2018 +0200
@@ -3,9 +3,6 @@
        value TEXT
        );
 
--- Set GlobalProperty_DatabaseSchemaVersion
-INSERT INTO GlobalProperties VALUES (1, '6');
-       
 CREATE TABLE Resources(
        internalId BIGINT NOT NULL AUTO_INCREMENT,
        resourceType INTEGER NOT NULL,
--- a/MySQL/Plugins/StoragePlugin.cpp	Tue Jul 10 11:05:37 2018 +0200
+++ b/MySQL/Plugins/StoragePlugin.cpp	Tue Jul 10 11:08:19 2018 +0200
@@ -19,10 +19,9 @@
  **/
 
 
+#include "MySQLStorageArea.h"
 #include "../../Framework/MySQL/MySQLDatabase.h"
-#include "../../Framework/Plugins/StorageBackend.h"
 
-#include <Plugins/Samples/Common/OrthancPluginCppWrapper.h>
 #include <Core/Logging.h>
 
 
@@ -80,8 +79,9 @@
 
     try
     {
-      // TODO
-      //OrthancDatabases::StorageBackend::Register();
+      OrthancDatabases::MySQLParameters parameters(mysql);
+      OrthancDatabases::StorageBackend::Register
+        (context, new OrthancDatabases::MySQLStorageArea(parameters));
     }
     catch (Orthanc::OrthancException& e)
     {
--- a/MySQL/UnitTests/UnitTestsMain.cpp	Tue Jul 10 11:05:37 2018 +0200
+++ b/MySQL/UnitTests/UnitTestsMain.cpp	Tue Jul 10 11:08:19 2018 +0200
@@ -20,11 +20,15 @@
 
 
 #include "../Plugins/MySQLIndex.h"
+#include "../Plugins/MySQLStorageArea.h"
 
 OrthancDatabases::MySQLParameters globalParameters_;
 
+#include "../../Framework/Common/Integer64Value.h"
+#include "../../Framework/MySQL/MySQLDatabase.h"
+#include "../../Framework/MySQL/MySQLResult.h"
+#include "../../Framework/MySQL/MySQLStatement.h"
 #include "../../Framework/Plugins/IndexUnitTests.h"
-#include "../../Framework/MySQL/MySQLDatabase.h"
 
 #include <Core/Logging.h>
 
@@ -56,21 +60,108 @@
 }
 
 
+static int64_t CountFiles(OrthancDatabases::MySQLDatabase& db)
+{
+  OrthancDatabases::Query query("SELECT COUNT(*) FROM StorageArea", true);
+  OrthancDatabases::MySQLStatement s(db, query);
+  OrthancDatabases::MySQLTransaction t(db);
+  OrthancDatabases::Dictionary d;
+  std::auto_ptr<OrthancDatabases::IResult> result(s.Execute(t, d));
+  return dynamic_cast<const OrthancDatabases::Integer64Value&>(result->GetField(0)).GetValue();
+}
+
+
+TEST(MySQL, StorageArea)
+{
+  OrthancDatabases::MySQLStorageArea storageArea(globalParameters_);
+  storageArea.SetClearAll(true);
+
+  {
+    OrthancDatabases::DatabaseManager::Transaction transaction(storageArea.GetManager());
+    OrthancDatabases::MySQLDatabase& db = 
+      dynamic_cast<OrthancDatabases::MySQLDatabase&>(transaction.GetDatabase());
+
+    ASSERT_EQ(0, CountFiles(db));
+  
+    for (int i = 0; i < 10; i++)
+    {
+      std::string uuid = boost::lexical_cast<std::string>(i);
+      std::string value = "Value " + boost::lexical_cast<std::string>(i * 2);
+      storageArea.Create(transaction, uuid, value.c_str(), value.size(), OrthancPluginContentType_Unknown);
+    }
+
+    std::string tmp;
+    ASSERT_THROW(storageArea.ReadToString(tmp, transaction, "nope", OrthancPluginContentType_Unknown), 
+                 Orthanc::OrthancException);
+  
+    ASSERT_EQ(10, CountFiles(db));
+    storageArea.Remove(transaction, "5", OrthancPluginContentType_Unknown);
+
+    ASSERT_EQ(9, CountFiles(db));
+
+    for (int i = 0; i < 10; i++)
+    {
+      std::string uuid = boost::lexical_cast<std::string>(i);
+      std::string expected = "Value " + boost::lexical_cast<std::string>(i * 2);
+      std::string content;
+
+      if (i == 5)
+      {
+        ASSERT_THROW(storageArea.ReadToString(content, transaction, uuid, OrthancPluginContentType_Unknown), 
+                     Orthanc::OrthancException);
+      }
+      else
+      {
+        storageArea.ReadToString(content, transaction, uuid, OrthancPluginContentType_Unknown);
+        ASSERT_EQ(expected, content);
+      }
+    }
+
+    for (int i = 0; i < 10; i++)
+    {
+      storageArea.Remove(transaction, boost::lexical_cast<std::string>(i),
+                         OrthancPluginContentType_Unknown);
+    }
+
+    ASSERT_EQ(0, CountFiles(db));
+
+    transaction.Commit();
+  }
+}
+
+
 int main(int argc, char **argv)
 {
   if (argc < 5)
   {
-    std::cerr << "Usage: " << argv[0] << " <socket> <username> <password> <database>"
+    std::cerr << "Usage (UNIX):    " << argv[0] << " <socket> <username> <password> <database>"
+              << std::endl
+              << "Usage (Windows): " << argv[0] << " <host> <port> <username> <password> <database>"
               << std::endl << std::endl
-              << "Example: " << argv[0] << " /var/run/mysqld/mysqld.sock root root orthanctest"
+              << "Example (UNIX):    " << argv[0] << " /var/run/mysqld/mysqld.sock root root orthanctest"
+              << std::endl
+              << "Example (Windows): " << argv[0] << " localhost 3306 root root orthanctest"
               << std::endl << std::endl;
     return -1;
   }
 
-  globalParameters_.SetUnixSocket(argv[1]);
-  globalParameters_.SetUsername(argv[2]);
-  globalParameters_.SetPassword(argv[3]);
-  globalParameters_.SetDatabase(argv[4]);
+  if (argc == 5)
+  {
+    // UNIX
+    globalParameters_.SetUnixSocket(argv[1]);
+    globalParameters_.SetUsername(argv[2]);
+    globalParameters_.SetPassword(argv[3]);
+    globalParameters_.SetDatabase(argv[4]);
+  }
+  else
+  {
+    // Windows
+    globalParameters_.SetHost(argv[1]);
+    globalParameters_.SetPort(boost::lexical_cast<unsigned int>(argv[2]));
+    globalParameters_.SetUsername(argv[3]);
+    globalParameters_.SetPassword(argv[4]);
+    globalParameters_.SetDatabase(argv[5]);
+  }
 
   ::testing::InitGoogleTest(&argc, argv);
   Orthanc::Logging::Initialize();
--- a/PostgreSQL/Plugins/PostgreSQLIndex.cpp	Tue Jul 10 11:05:37 2018 +0200
+++ b/PostgreSQL/Plugins/PostgreSQLIndex.cpp	Tue Jul 10 11:08:19 2018 +0200
@@ -129,7 +129,10 @@
          **/
         try
         {
-          LOG(WARNING) << "Trying to enable trigram matching on the PostgreSQL database to speed up wildcard searches.  This may take several minutes";  // we've observed 9 minutes on DB with 100000 studies
+          // We've observed 9 minutes on DB with 100000 studies
+          LOG(WARNING) << "Trying to enable trigram matching on the PostgreSQL database "
+                    << "to speed up wildcard searches. This may take several minutes";
+
           db->Execute(
             "CREATE EXTENSION pg_trgm; "
             "CREATE INDEX DicomIdentifiersIndexValues2 ON DicomIdentifiers USING gin(value gin_trgm_ops);");
@@ -139,8 +142,10 @@
         }
         catch (Orthanc::OrthancException&)
         {
-          LOG(WARNING) << "Performance warning: Your PostgreSQL server does not support trigram matching";
-          LOG(WARNING) << "-> Consider installing the \"pg_trgm\" extension on the PostgreSQL server, e.g. on Debian: sudo apt install postgresql-contrib";
+          LOG(WARNING) << "Performance warning: Your PostgreSQL server does "
+                       << "not support trigram matching";
+          LOG(WARNING) << "-> Consider installing the \"pg_trgm\" extension on the "
+                       << "PostgreSQL server, e.g. on Debian: sudo apt install postgresql-contrib";
         }
       }
 
--- a/PostgreSQL/Plugins/PostgreSQLStorageArea.cpp	Tue Jul 10 11:05:37 2018 +0200
+++ b/PostgreSQL/Plugins/PostgreSQLStorageArea.cpp	Tue Jul 10 11:08:19 2018 +0200
@@ -21,7 +21,6 @@
 
 #include "PostgreSQLStorageArea.h"
 
-#include "../../Framework/Common/FileValue.h"
 #include "../../Framework/PostgreSQL/PostgreSQLTransaction.h"
 
 #include <Plugins/Samples/Common/OrthancPluginCppWrapper.h>
@@ -74,82 +73,4 @@
     clearAll_(false)
   {
   }
-
-
-  void PostgreSQLStorageArea::Create(DatabaseManager::Transaction& transaction,
-                                     const std::string& uuid,
-                                     const void* content,
-                                     size_t size,
-                                     OrthancPluginContentType type)
-  {
-    DatabaseManager::CachedStatement statement(
-      STATEMENT_FROM_HERE, GetManager(),
-      "INSERT INTO StorageArea VALUES (${uuid}, ${content}, ${type})");
-     
-    statement.SetParameterType("uuid", ValueType_Utf8String);
-    statement.SetParameterType("content", ValueType_File);
-    statement.SetParameterType("type", ValueType_Integer64);
-
-    Dictionary args;
-    args.SetUtf8Value("uuid", uuid);
-    args.SetFileValue("content", content, size);
-    args.SetIntegerValue("type", type);
-     
-    statement.Execute(args);
-  }
-
-
-  void PostgreSQLStorageArea::Read(void*& content,
-                                   size_t& size,
-                                   DatabaseManager::Transaction& transaction, 
-                                   const std::string& uuid,
-                                   OrthancPluginContentType type) 
-  {
-    DatabaseManager::CachedStatement statement(
-      STATEMENT_FROM_HERE, GetManager(),
-      "SELECT content FROM StorageArea WHERE uuid=${uuid} AND type=${type}");
-     
-    statement.SetParameterType("uuid", ValueType_Utf8String);
-    statement.SetParameterType("type", ValueType_Integer64);
-
-    Dictionary args;
-    args.SetUtf8Value("uuid", uuid);
-    args.SetIntegerValue("type", type);
-     
-    statement.Execute(args);
-
-    if (statement.IsDone())
-    {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);
-    }
-    else if (statement.GetResultFieldsCount() != 1 ||
-             statement.GetResultField(0).GetType() != ValueType_File)
-    {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_Database);        
-    }
-    else
-    {
-      const FileValue& value = dynamic_cast<const FileValue&>(statement.GetResultField(0));
-      ReadFromString(content, size, value.GetContent());
-    }
-  }
-
-
-  void PostgreSQLStorageArea::Remove(DatabaseManager::Transaction& transaction,
-                                     const std::string& uuid,
-                                     OrthancPluginContentType type)
-  {
-    DatabaseManager::CachedStatement statement(
-      STATEMENT_FROM_HERE, GetManager(),
-      "DELETE FROM StorageArea WHERE uuid=${uuid} AND type=${type}");
-     
-    statement.SetParameterType("uuid", ValueType_Utf8String);
-    statement.SetParameterType("type", ValueType_Integer64);
-
-    Dictionary args;
-    args.SetUtf8Value("uuid", uuid);
-    args.SetIntegerValue("type", type);
-     
-    statement.Execute(args);
-  }
 }
--- a/PostgreSQL/Plugins/PostgreSQLStorageArea.h	Tue Jul 10 11:05:37 2018 +0200
+++ b/PostgreSQL/Plugins/PostgreSQLStorageArea.h	Tue Jul 10 11:08:19 2018 +0200
@@ -64,21 +64,5 @@
     {
       clearAll_ = clear;
     }
-
-    virtual void Create(DatabaseManager::Transaction& transaction,
-                        const std::string& uuid,
-                        const void* content,
-                        size_t size,
-                        OrthancPluginContentType type);
-
-    virtual void Read(void*& content,
-                      size_t& size,
-                      DatabaseManager::Transaction& transaction, 
-                      const std::string& uuid,
-                      OrthancPluginContentType type);
-
-    virtual void Remove(DatabaseManager::Transaction& transaction,
-                        const std::string& uuid,
-                        OrthancPluginContentType type);
   };
 }
--- a/PostgreSQL/Plugins/StoragePlugin.cpp	Tue Jul 10 11:05:37 2018 +0200
+++ b/PostgreSQL/Plugins/StoragePlugin.cpp	Tue Jul 10 11:08:19 2018 +0200
@@ -20,180 +20,9 @@
 
 
 #include "../../Framework/Plugins/StorageBackend.h"
-
-#include "../../Framework/Common/FileValue.h"
-#include "../../Framework/PostgreSQL/PostgreSQLDatabase.h"
-#include "../../Framework/PostgreSQL/PostgreSQLLargeObject.h"
-#include "../../Framework/PostgreSQL/PostgreSQLTransaction.h"
-
-#include <Plugins/Samples/Common/OrthancPluginCppWrapper.h>
-#include <Core/Logging.h>
-
-
-namespace OrthancDatabases
-{
-  class PostgreSQLStorageArea : public StorageBackend
-  {
-  private:
-    class Factory : public IDatabaseFactory
-    {
-    private:
-      PostgreSQLStorageArea&  that_;
-
-    public:
-      Factory(PostgreSQLStorageArea& that) :
-      that_(that)
-      {
-      }
-
-      virtual Dialect GetDialect() const
-      {
-        return Dialect_PostgreSQL;
-      }
-
-      virtual IDatabase* Open()
-      {
-        return that_.OpenInternal();
-      }
-    };
-
-    OrthancPluginContext*  context_;
-    PostgreSQLParameters   parameters_;
-    bool                   clearAll_;
-
-    IDatabase* OpenInternal()
-    {
-      std::auto_ptr<PostgreSQLDatabase> db(new PostgreSQLDatabase(parameters_));
-
-      db->Open();
-
-      if (parameters_.HasLock())
-      {
-        db->AdvisoryLock(43 /* some arbitrary constant */);
-      }
-
-      if (clearAll_)
-      {
-        db->ClearAll();
-      }
-
-      {
-        PostgreSQLTransaction t(*db);
-
-        if (!db->DoesTableExist("StorageArea"))
-        {
-          db->Execute("CREATE TABLE IF NOT EXISTS StorageArea("
-                      "uuid VARCHAR NOT NULL PRIMARY KEY,"
-                      "content OID NOT NULL,"
-                      "type INTEGER NOT NULL)");
-
-          // Automatically remove the large objects associated with the table
-          db->Execute("CREATE OR REPLACE RULE StorageAreaDelete AS ON DELETE "
-                      "TO StorageArea DO SELECT lo_unlink(old.content);");
-        }
-
-        t.Commit();
-      }
-
-      return db.release();
-    }
-
-  public:
-    PostgreSQLStorageArea(const PostgreSQLParameters& parameters) :
-    StorageBackend(new Factory(*this)),
-    parameters_(parameters),
-    clearAll_(false)
-    {
-    }
+#include "PostgreSQLStorageArea.h"
 
-    void SetClearAll(bool clear)
-    {
-      clearAll_ = clear;
-    }
-
-
-    virtual void Create(DatabaseManager::Transaction& transaction,
-                        const std::string& uuid,
-                        const void* content,
-                        size_t size,
-                        OrthancPluginContentType type)
-    {
-      std::auto_ptr<FileValue> file(new FileValue(content, size));
-      
-      {
-        DatabaseManager::CachedStatement statement(
-          STATEMENT_FROM_HERE, GetManager(),
-          "INSERT INTO StorageArea VALUES (${uuid}, ${content}, ${type})");
-     
-        statement.SetParameterType("uuid", ValueType_Utf8String);
-        statement.SetParameterType("content", ValueType_File);
-        statement.SetParameterType("type", ValueType_Integer64);
-
-        Dictionary args;
-        args.SetUtf8Value("uuid", uuid);
-        args.SetValue("content", file.release());
-        args.SetIntegerValue("type", type);
-     
-        statement.Execute(args);
-      }
-    }
-
-
-    virtual void Read(void*& content,
-                      size_t& size,
-                      DatabaseManager::Transaction& transaction, 
-                      const std::string& uuid,
-                      OrthancPluginContentType type) 
-    {
-      DatabaseManager::CachedStatement statement(
-        STATEMENT_FROM_HERE, GetManager(),
-        "SELECT content FROM StorageArea WHERE uuid=$1 AND type=$2");
-     
-      statement.SetParameterType("uuid", ValueType_Utf8String);
-      statement.SetParameterType("type", ValueType_Integer64);
-
-      Dictionary args;
-      args.SetUtf8Value("uuid", uuid);
-      args.SetIntegerValue("type", type);
-     
-      statement.Execute(args);
-
-      if (statement.IsDone())
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);
-      }
-      else if (statement.GetResultFieldsCount() != 1 ||
-               statement.GetResultField(0).GetType() != ValueType_File)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_Database);        
-      }
-      else
-      {
-        const FileValue& value = dynamic_cast<const FileValue&>(statement.GetResultField(0));
-        ReadFromString(content, size, value.GetContent());
-      }
-    }
-
-
-    virtual void Remove(DatabaseManager::Transaction& transaction,
-                        const std::string& uuid,
-                        OrthancPluginContentType type)
-    {
-      DatabaseManager::CachedStatement statement(
-        STATEMENT_FROM_HERE, GetManager(),
-        "DELETE FROM StorageArea WHERE uuid=${uuid} AND type=${type}");
-     
-      statement.SetParameterType("uuid", ValueType_Utf8String);
-      statement.SetParameterType("type", ValueType_Integer64);
-
-      Dictionary args;
-      args.SetUtf8Value("uuid", uuid);
-      args.SetIntegerValue("type", type);
-     
-      statement.Execute(args);
-    }
-  };
-}
+#include <Core/Logging.h>
 
 
 static bool DisplayPerformanceWarning()