changeset 3037:d23c84b1c0f5 db-changes

integration mainline->db-changes
author Sebastien Jodogne <s.jodogne@gmail.com>
date Wed, 19 Dec 2018 16:31:10 +0100
parents 8fd203510d8b (diff) 3254b36a5f9b (current diff)
children 53d583d2c775
files
diffstat 51 files changed, 3875 insertions(+), 2986 deletions(-) [+]
line wrap: on
line diff
--- a/CMakeLists.txt	Wed Dec 19 14:35:55 2018 +0100
+++ b/CMakeLists.txt	Wed Dec 19 16:31:10 2018 +0100
@@ -53,7 +53,7 @@
 #####################################################################
 
 set(ORTHANC_SERVER_SOURCES
-  OrthancServer/DatabaseWrapper.cpp
+  OrthancServer/SQLiteDatabaseWrapper.cpp
   OrthancServer/DicomInstanceOrigin.cpp
   OrthancServer/DicomInstanceToStore.cpp
   OrthancServer/ExportedResource.cpp
@@ -71,13 +71,12 @@
   OrthancServer/OrthancRestApi/OrthancRestResources.cpp
   OrthancServer/OrthancRestApi/OrthancRestSystem.cpp
   OrthancServer/QueryRetrieveHandler.cpp
+  OrthancServer/Search/DatabaseConstraint.cpp
   OrthancServer/Search/DatabaseLookup.cpp
   OrthancServer/Search/DicomTagConstraint.cpp
   OrthancServer/Search/HierarchicalMatcher.cpp
   OrthancServer/Search/IFindConstraint.cpp
   OrthancServer/Search/ListConstraint.cpp
-  OrthancServer/Search/LookupIdentifierQuery.cpp
-  OrthancServer/Search/LookupResource.cpp
   OrthancServer/Search/RangeConstraint.cpp
   OrthancServer/Search/SetOfResources.cpp
   OrthancServer/Search/ValueConstraint.cpp
@@ -168,13 +167,14 @@
 #####################################################################
 
 set(ORTHANC_EMBEDDED_FILES
-  PREPARE_DATABASE            ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/PrepareDatabase.sql
-  UPGRADE_DATABASE_3_TO_4     ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/Upgrade3To4.sql
-  UPGRADE_DATABASE_4_TO_5     ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/Upgrade4To5.sql
-  CONFIGURATION_SAMPLE        ${CMAKE_CURRENT_SOURCE_DIR}/Resources/Configuration.json
-  DICOM_CONFORMANCE_STATEMENT ${CMAKE_CURRENT_SOURCE_DIR}/Resources/DicomConformanceStatement.txt
-  LUA_TOOLBOX                 ${CMAKE_CURRENT_SOURCE_DIR}/Resources/Toolbox.lua
-  FONT_UBUNTU_MONO_BOLD_16    ${CMAKE_CURRENT_SOURCE_DIR}/Resources/Fonts/UbuntuMonoBold-16.json
+  PREPARE_DATABASE                ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/PrepareDatabase.sql
+  UPGRADE_DATABASE_3_TO_4         ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/Upgrade3To4.sql
+  UPGRADE_DATABASE_4_TO_5         ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/Upgrade4To5.sql
+  CONFIGURATION_SAMPLE            ${CMAKE_CURRENT_SOURCE_DIR}/Resources/Configuration.json
+  DICOM_CONFORMANCE_STATEMENT     ${CMAKE_CURRENT_SOURCE_DIR}/Resources/DicomConformanceStatement.txt
+  LUA_TOOLBOX                     ${CMAKE_CURRENT_SOURCE_DIR}/Resources/Toolbox.lua
+  FONT_UBUNTU_MONO_BOLD_16        ${CMAKE_CURRENT_SOURCE_DIR}/Resources/Fonts/UbuntuMonoBold-16.json
+  INSTALL_TRACK_ATTACHMENTS_SIZE  ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/InstallTrackAttachmentsSize.sql
   )
 
 if (STANDALONE_BUILD)
--- a/Core/Enumerations.cpp	Wed Dec 19 14:35:55 2018 +0100
+++ b/Core/Enumerations.cpp	Wed Dec 19 16:31:10 2018 +0100
@@ -1897,6 +1897,35 @@
   }
 
 
+  bool IsResourceLevelAboveOrEqual(ResourceType level,
+                                   ResourceType reference)
+  {
+    switch (reference)
+    {
+      case ResourceType_Patient:
+        return (level == ResourceType_Patient);
+
+      case ResourceType_Study:
+        return (level == ResourceType_Patient ||
+                level == ResourceType_Study);
+
+      case ResourceType_Series:
+        return (level == ResourceType_Patient ||
+                level == ResourceType_Study ||
+                level == ResourceType_Series);
+
+      case ResourceType_Instance:
+        return (level == ResourceType_Patient ||
+                level == ResourceType_Study ||
+                level == ResourceType_Series ||
+                level == ResourceType_Instance);
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
   DicomModule GetModule(ResourceType type)
   {
     switch (type)
--- a/Core/Enumerations.h	Wed Dec 19 14:35:55 2018 +0100
+++ b/Core/Enumerations.h	Wed Dec 19 16:31:10 2018 +0100
@@ -758,6 +758,9 @@
 
   ResourceType GetParentResourceType(ResourceType type);
 
+  bool IsResourceLevelAboveOrEqual(ResourceType level,
+                                   ResourceType reference);
+
   DicomModule GetModule(ResourceType type);
 
   const char* GetDicomSpecificCharacterSet(Encoding encoding);
--- a/OrthancServer/DatabaseWrapper.cpp	Wed Dec 19 14:35:55 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1131 +0,0 @@
-/**
- * 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "PrecompiledHeadersServer.h"
-#include "DatabaseWrapper.h"
-
-#include "../Core/DicomFormat/DicomArray.h"
-#include "../Core/Logging.h"
-#include "EmbeddedResources.h"
-#include "ServerToolbox.h"
-
-#include <stdio.h>
-#include <boost/lexical_cast.hpp>
-
-namespace Orthanc
-{
-  namespace Internals
-  {
-    class SignalFileDeleted : public SQLite::IScalarFunction
-    {
-    private:
-      IDatabaseListener& listener_;
-
-    public:
-      SignalFileDeleted(IDatabaseListener& listener) :
-        listener_(listener)
-      {
-      }
-
-      virtual const char* GetName() const
-      {
-        return "SignalFileDeleted";
-      }
-
-      virtual unsigned int GetCardinality() const
-      {
-        return 7;
-      }
-
-      virtual void Compute(SQLite::FunctionContext& context)
-      {
-        std::string uncompressedMD5, compressedMD5;
-
-        if (!context.IsNullValue(5))
-        {
-          uncompressedMD5 = context.GetStringValue(5);
-        }
-
-        if (!context.IsNullValue(6))
-        {
-          compressedMD5 = context.GetStringValue(6);
-        }
-
-        FileInfo info(context.GetStringValue(0),
-                      static_cast<FileContentType>(context.GetIntValue(1)),
-                      static_cast<uint64_t>(context.GetInt64Value(2)),
-                      uncompressedMD5,
-                      static_cast<CompressionType>(context.GetIntValue(3)),
-                      static_cast<uint64_t>(context.GetInt64Value(4)),
-                      compressedMD5);
-        
-        listener_.SignalFileDeleted(info);
-      }
-    };
-
-    class SignalResourceDeleted : public SQLite::IScalarFunction
-    {
-    private:
-      IDatabaseListener& listener_;
-
-    public:
-      SignalResourceDeleted(IDatabaseListener& listener) :
-        listener_(listener)
-      {
-      }
-
-      virtual const char* GetName() const
-      {
-        return "SignalResourceDeleted";
-      }
-
-      virtual unsigned int GetCardinality() const
-      {
-        return 2;
-      }
-
-      virtual void Compute(SQLite::FunctionContext& context)
-      {
-        ResourceType type = static_cast<ResourceType>(context.GetIntValue(1));
-        ServerIndexChange change(ChangeType_Deleted, type, context.GetStringValue(0));
-        listener_.SignalChange(change);
-      }
-    };
-
-    class SignalRemainingAncestor : public SQLite::IScalarFunction
-    {
-    private:
-      bool hasRemainingAncestor_;
-      std::string remainingPublicId_;
-      ResourceType remainingType_;
-
-    public:
-      SignalRemainingAncestor() : 
-        hasRemainingAncestor_(false)
-      {
-      }
-
-      void Reset()
-      {
-        hasRemainingAncestor_ = false;
-      }
-
-      virtual const char* GetName() const
-      {
-        return "SignalRemainingAncestor";
-      }
-
-      virtual unsigned int GetCardinality() const
-      {
-        return 2;
-      }
-
-      virtual void Compute(SQLite::FunctionContext& context)
-      {
-        VLOG(1) << "There exists a remaining ancestor with public ID \""
-                << context.GetStringValue(0)
-                << "\" of type "
-                << context.GetIntValue(1);
-
-        if (!hasRemainingAncestor_ ||
-            remainingType_ >= context.GetIntValue(1))
-        {
-          hasRemainingAncestor_ = true;
-          remainingPublicId_ = context.GetStringValue(0);
-          remainingType_ = static_cast<ResourceType>(context.GetIntValue(1));
-        }
-      }
-
-      bool HasRemainingAncestor() const
-      {
-        return hasRemainingAncestor_;
-      }
-
-      const std::string& GetRemainingAncestorId() const
-      {
-        assert(hasRemainingAncestor_);
-        return remainingPublicId_;
-      }
-
-      ResourceType GetRemainingAncestorType() const
-      {
-        assert(hasRemainingAncestor_);
-        return remainingType_;
-      }
-    };
-  }
-
-
-  void DatabaseWrapper::GetChangesInternal(std::list<ServerIndexChange>& target,
-                                           bool& done,
-                                           SQLite::Statement& s,
-                                           uint32_t maxResults)
-  {
-    target.clear();
-
-    while (target.size() < maxResults && s.Step())
-    {
-      int64_t seq = s.ColumnInt64(0);
-      ChangeType changeType = static_cast<ChangeType>(s.ColumnInt(1));
-      ResourceType resourceType = static_cast<ResourceType>(s.ColumnInt(3));
-      const std::string& date = s.ColumnString(4);
-
-      int64_t internalId = s.ColumnInt64(2);
-      std::string publicId = GetPublicId(internalId);
-
-      target.push_back(ServerIndexChange(seq, changeType, resourceType, publicId, date));
-    }
-
-    done = !(target.size() == maxResults && s.Step());
-  }
-
-
-  void DatabaseWrapper::GetExportedResourcesInternal(std::list<ExportedResource>& target,
-                                                     bool& done,
-                                                     SQLite::Statement& s,
-                                                     uint32_t maxResults)
-  {
-    target.clear();
-
-    while (target.size() < maxResults && s.Step())
-    {
-      int64_t seq = s.ColumnInt64(0);
-      ResourceType resourceType = static_cast<ResourceType>(s.ColumnInt(1));
-      std::string publicId = s.ColumnString(2);
-
-      ExportedResource resource(seq, 
-                                resourceType,
-                                publicId,
-                                s.ColumnString(3),  // modality
-                                s.ColumnString(8),  // date
-                                s.ColumnString(4),  // patient ID
-                                s.ColumnString(5),  // study instance UID
-                                s.ColumnString(6),  // series instance UID
-                                s.ColumnString(7)); // sop instance UID
-
-      target.push_back(resource);
-    }
-
-    done = !(target.size() == maxResults && s.Step());
-  }
-
-
-  void DatabaseWrapper::GetChildren(std::list<std::string>& childrenPublicIds,
-                                    int64_t id)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Resources WHERE parentId=?");
-    s.BindInt64(0, id);
-
-    childrenPublicIds.clear();
-    while (s.Step())
-    {
-      childrenPublicIds.push_back(s.ColumnString(0));
-    }
-  }
-
-
-  void DatabaseWrapper::DeleteResource(int64_t id)
-  {
-    signalRemainingAncestor_->Reset();
-
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM Resources WHERE internalId=?");
-    s.BindInt64(0, id);
-    s.Run();
-
-    if (signalRemainingAncestor_->HasRemainingAncestor() &&
-        listener_ != NULL)
-    {
-      listener_->SignalRemainingAncestor(signalRemainingAncestor_->GetRemainingAncestorType(),
-                                         signalRemainingAncestor_->GetRemainingAncestorId());
-    }
-  }
-
-
-  bool DatabaseWrapper::GetParentPublicId(std::string& target,
-                                          int64_t id)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.publicId FROM Resources AS a, Resources AS b "
-                        "WHERE a.internalId = b.parentId AND b.internalId = ?");     
-    s.BindInt64(0, id);
-
-    if (s.Step())
-    {
-      target = s.ColumnString(0);
-      return true;
-    }
-    else
-    {
-      return false;
-    }
-  }
-
-
-  int64_t DatabaseWrapper::GetTableRecordCount(const std::string& table)
-  {
-    char buf[128];
-    sprintf(buf, "SELECT COUNT(*) FROM %s", table.c_str());
-    SQLite::Statement s(db_, buf);
-
-    if (!s.Step())
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-
-    int64_t c = s.ColumnInt(0);
-    assert(!s.Step());
-
-    return c;
-  }
-
-    
-  DatabaseWrapper::DatabaseWrapper(const std::string& path) : 
-    listener_(NULL), 
-    signalRemainingAncestor_(NULL),
-    version_(0)
-  {
-    db_.Open(path);
-  }
-
-
-  DatabaseWrapper::DatabaseWrapper() : 
-    listener_(NULL), 
-    signalRemainingAncestor_(NULL),
-    version_(0)
-  {
-    db_.OpenInMemory();
-  }
-
-
-  void DatabaseWrapper::Open()
-  {
-    db_.Execute("PRAGMA ENCODING=\"UTF-8\";");
-
-    // Performance tuning of SQLite with PRAGMAs
-    // http://www.sqlite.org/pragma.html
-    db_.Execute("PRAGMA SYNCHRONOUS=NORMAL;");
-    db_.Execute("PRAGMA JOURNAL_MODE=WAL;");
-    db_.Execute("PRAGMA LOCKING_MODE=EXCLUSIVE;");
-    db_.Execute("PRAGMA WAL_AUTOCHECKPOINT=1000;");
-    //db_.Execute("PRAGMA TEMP_STORE=memory");
-
-    if (!db_.DoesTableExist("GlobalProperties"))
-    {
-      LOG(INFO) << "Creating the database";
-      std::string query;
-      EmbeddedResources::GetFileResource(query, EmbeddedResources::PREPARE_DATABASE);
-      db_.Execute(query);
-    }
-
-    // Check the version of the database
-    std::string tmp;
-    if (!LookupGlobalProperty(tmp, GlobalProperty_DatabaseSchemaVersion))
-    {
-      tmp = "Unknown";
-    }
-
-    bool ok = false;
-    try
-    {
-      LOG(INFO) << "Version of the Orthanc database: " << tmp;
-      version_ = boost::lexical_cast<unsigned int>(tmp);
-      ok = true;
-    }
-    catch (boost::bad_lexical_cast&)
-    {
-    }
-
-    if (!ok)
-    {
-      throw OrthancException(ErrorCode_IncompatibleDatabaseVersion,
-                             "Incompatible version of the Orthanc database: " + tmp);
-    }
-
-    signalRemainingAncestor_ = new Internals::SignalRemainingAncestor;
-    db_.Register(signalRemainingAncestor_);
-  }
-
-
-  static void ExecuteUpgradeScript(SQLite::Connection& db,
-                                   EmbeddedResources::FileResourceId script)
-  {
-    std::string upgrade;
-    EmbeddedResources::GetFileResource(upgrade, script);
-    db.BeginTransaction();
-    db.Execute(upgrade);
-    db.CommitTransaction();    
-  }
-
-
-  void DatabaseWrapper::Upgrade(unsigned int targetVersion,
-                                IStorageArea& storageArea)
-  {
-    if (targetVersion != 6)
-    {
-      throw OrthancException(ErrorCode_IncompatibleDatabaseVersion);
-    }
-
-    // This version of Orthanc is only compatible with versions 3, 4,
-    // 5 and 6 of the DB schema
-    if (version_ != 3 &&
-        version_ != 4 &&
-        version_ != 5 &&
-        version_ != 6)
-    {
-      throw OrthancException(ErrorCode_IncompatibleDatabaseVersion);
-    }
-
-    if (version_ == 3)
-    {
-      LOG(WARNING) << "Upgrading database version from 3 to 4";
-      ExecuteUpgradeScript(db_, EmbeddedResources::UPGRADE_DATABASE_3_TO_4);
-      version_ = 4;
-    }
-
-    if (version_ == 4)
-    {
-      LOG(WARNING) << "Upgrading database version from 4 to 5";
-      ExecuteUpgradeScript(db_, EmbeddedResources::UPGRADE_DATABASE_4_TO_5);
-      version_ = 5;
-    }
-
-    if (version_ == 5)
-    {
-      LOG(WARNING) << "Upgrading database version from 5 to 6";
-      // No change in the DB schema, the step from version 5 to 6 only
-      // consists in reconstructing the main DICOM tags information
-      // (as more tags got included).
-      db_.BeginTransaction();
-      ServerToolbox::ReconstructMainDicomTags(*this, storageArea, ResourceType_Patient);
-      ServerToolbox::ReconstructMainDicomTags(*this, storageArea, ResourceType_Study);
-      ServerToolbox::ReconstructMainDicomTags(*this, storageArea, ResourceType_Series);
-      ServerToolbox::ReconstructMainDicomTags(*this, storageArea, ResourceType_Instance);
-      db_.Execute("UPDATE GlobalProperties SET value=\"6\" WHERE property=" +
-                  boost::lexical_cast<std::string>(GlobalProperty_DatabaseSchemaVersion) + ";");
-      db_.CommitTransaction();
-      version_ = 6;
-    }
-  }
-
-
-  void DatabaseWrapper::SetListener(IDatabaseListener& listener)
-  {
-    listener_ = &listener;
-    db_.Register(new Internals::SignalFileDeleted(listener));
-    db_.Register(new Internals::SignalResourceDeleted(listener));
-  }
-
-
-  void DatabaseWrapper::ClearTable(const std::string& tableName)
-  {
-    db_.Execute("DELETE FROM " + tableName);    
-  }
-
-
-  bool DatabaseWrapper::LookupParent(int64_t& parentId,
-                                     int64_t resourceId)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT parentId FROM Resources WHERE internalId=?");
-    s.BindInt64(0, resourceId);
-
-    if (!s.Step())
-    {
-      throw OrthancException(ErrorCode_UnknownResource);
-    }
-
-    if (s.ColumnIsNull(0))
-    {
-      return false;
-    }
-    else
-    {
-      parentId = s.ColumnInt(0);
-      return true;
-    }
-  }
-
-
-  ResourceType DatabaseWrapper::GetResourceType(int64_t resourceId)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT resourceType FROM Resources WHERE internalId=?");
-    s.BindInt64(0, resourceId);
-    
-    if (s.Step())
-    {
-      return static_cast<ResourceType>(s.ColumnInt(0));
-    }
-    else
-    { 
-      throw OrthancException(ErrorCode_UnknownResource);
-    }
-  }
-
-
-  std::string DatabaseWrapper::GetPublicId(int64_t resourceId)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT publicId FROM Resources WHERE internalId=?");
-    s.BindInt64(0, resourceId);
-    
-    if (s.Step())
-    { 
-      return s.ColumnString(0);
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_UnknownResource);
-    }
-  }
-
-
-  void DatabaseWrapper::GetChanges(std::list<ServerIndexChange>& target /*out*/,
-                                   bool& done /*out*/,
-                                   int64_t since,
-                                   uint32_t maxResults)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes WHERE seq>? ORDER BY seq LIMIT ?");
-    s.BindInt64(0, since);
-    s.BindInt(1, maxResults + 1);
-    GetChangesInternal(target, done, s, maxResults);
-  }
-
-
-  void DatabaseWrapper::GetLastChange(std::list<ServerIndexChange>& target /*out*/)
-  {
-    bool done;  // Ignored
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes ORDER BY seq DESC LIMIT 1");
-    GetChangesInternal(target, done, s, 1);
-  }
-
-
-  void DatabaseWrapper::GetAllMetadata(std::map<MetadataType, std::string>& target,
-                                       int64_t id)
-  {
-    target.clear();
-
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT type, value FROM Metadata WHERE id=?");
-    s.BindInt64(0, id);
-
-    while (s.Step())
-    {
-      MetadataType key = static_cast<MetadataType>(s.ColumnInt(0));
-      target[key] = s.ColumnString(1);
-    }
-  }
-
-
-  void DatabaseWrapper::SetGlobalProperty(GlobalProperty property,
-                                          const std::string& value)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO GlobalProperties VALUES(?, ?)");
-    s.BindInt(0, property);
-    s.BindString(1, value);
-    s.Run();
-  }
-
-
-  bool DatabaseWrapper::LookupGlobalProperty(std::string& target,
-                                             GlobalProperty property)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT value FROM GlobalProperties WHERE property=?");
-    s.BindInt(0, property);
-
-    if (!s.Step())
-    {
-      return false;
-    }
-    else
-    {
-      target = s.ColumnString(0);
-      return true;
-    }
-  }
-
-
-  int64_t DatabaseWrapper::CreateResource(const std::string& publicId,
-                                          ResourceType type)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Resources VALUES(NULL, ?, ?, NULL)");
-    s.BindInt(0, type);
-    s.BindString(1, publicId);
-    s.Run();
-    return db_.GetLastInsertRowId();
-  }
-
-
-  bool DatabaseWrapper::LookupResource(int64_t& id,
-                                       ResourceType& type,
-                                       const std::string& publicId)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT internalId, resourceType FROM Resources WHERE publicId=?");
-    s.BindString(0, publicId);
-
-    if (!s.Step())
-    {
-      return false;
-    }
-    else
-    {
-      id = s.ColumnInt(0);
-      type = static_cast<ResourceType>(s.ColumnInt(1));
-
-      // Check whether there is a single resource with this public id
-      assert(!s.Step());
-
-      return true;
-    }
-  }
-
-
-  void DatabaseWrapper::AttachChild(int64_t parent,
-                                    int64_t child)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "UPDATE Resources SET parentId = ? WHERE internalId = ?");
-    s.BindInt64(0, parent);
-    s.BindInt64(1, child);
-    s.Run();
-  }
-
-
-  void DatabaseWrapper::SetMetadata(int64_t id,
-                                    MetadataType type,
-                                    const std::string& value)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO Metadata VALUES(?, ?, ?)");
-    s.BindInt64(0, id);
-    s.BindInt(1, type);
-    s.BindString(2, value);
-    s.Run();
-  }
-
-
-  void DatabaseWrapper::DeleteMetadata(int64_t id,
-                                       MetadataType type)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM Metadata WHERE id=? and type=?");
-    s.BindInt64(0, id);
-    s.BindInt(1, type);
-    s.Run();
-  }
-
-
-  bool DatabaseWrapper::LookupMetadata(std::string& target,
-                                       int64_t id,
-                                       MetadataType type)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT value FROM Metadata WHERE id=? AND type=?");
-    s.BindInt64(0, id);
-    s.BindInt(1, type);
-
-    if (!s.Step())
-    {
-      return false;
-    }
-    else
-    {
-      target = s.ColumnString(0);
-      return true;
-    }
-  }
-
-
-  void DatabaseWrapper::ListAvailableMetadata(std::list<MetadataType>& target,
-                                              int64_t id)
-  {
-    target.clear();
-
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT type FROM Metadata WHERE id=?");
-    s.BindInt64(0, id);
-
-    while (s.Step())
-    {
-      target.push_back(static_cast<MetadataType>(s.ColumnInt(0)));
-    }
-  }
-
-
-  void DatabaseWrapper::AddAttachment(int64_t id,
-                                      const FileInfo& attachment)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO AttachedFiles VALUES(?, ?, ?, ?, ?, ?, ?, ?)");
-    s.BindInt64(0, id);
-    s.BindInt(1, attachment.GetContentType());
-    s.BindString(2, attachment.GetUuid());
-    s.BindInt64(3, attachment.GetCompressedSize());
-    s.BindInt64(4, attachment.GetUncompressedSize());
-    s.BindInt(5, attachment.GetCompressionType());
-    s.BindString(6, attachment.GetUncompressedMD5());
-    s.BindString(7, attachment.GetCompressedMD5());
-    s.Run();
-  }
-
-
-  void DatabaseWrapper::DeleteAttachment(int64_t id,
-                                         FileContentType attachment)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM AttachedFiles WHERE id=? AND fileType=?");
-    s.BindInt64(0, id);
-    s.BindInt(1, attachment);
-    s.Run();
-  }
-
-
-  void DatabaseWrapper::ListAvailableAttachments(std::list<FileContentType>& target,
-                                                 int64_t id)
-  {
-    target.clear();
-
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT fileType FROM AttachedFiles WHERE id=?");
-    s.BindInt64(0, id);
-
-    while (s.Step())
-    {
-      target.push_back(static_cast<FileContentType>(s.ColumnInt(0)));
-    }
-  }
-
-  bool DatabaseWrapper::LookupAttachment(FileInfo& attachment,
-                                         int64_t id,
-                                         FileContentType contentType)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT uuid, uncompressedSize, compressionType, compressedSize, "
-                        "uncompressedMD5, compressedMD5 FROM AttachedFiles WHERE id=? AND fileType=?");
-    s.BindInt64(0, id);
-    s.BindInt(1, contentType);
-
-    if (!s.Step())
-    {
-      return false;
-    }
-    else
-    {
-      attachment = FileInfo(s.ColumnString(0),
-                            contentType,
-                            s.ColumnInt64(1),
-                            s.ColumnString(4),
-                            static_cast<CompressionType>(s.ColumnInt(2)),
-                            s.ColumnInt64(3),
-                            s.ColumnString(5));
-      return true;
-    }
-  }
-
-
-  void DatabaseWrapper::ClearMainDicomTags(int64_t id)
-  {
-    {
-      SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM DicomIdentifiers WHERE id=?");
-      s.BindInt64(0, id);
-      s.Run();
-    }
-
-    {
-      SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM MainDicomTags WHERE id=?");
-      s.BindInt64(0, id);
-      s.Run();
-    }
-  }
-
-
-  void DatabaseWrapper::SetMainDicomTag(int64_t id,
-                                        const DicomTag& tag,
-                                        const std::string& value)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO MainDicomTags VALUES(?, ?, ?, ?)");
-    s.BindInt64(0, id);
-    s.BindInt(1, tag.GetGroup());
-    s.BindInt(2, tag.GetElement());
-    s.BindString(3, value);
-    s.Run();
-  }
-
-
-  void DatabaseWrapper::SetIdentifierTag(int64_t id,
-                                         const DicomTag& tag,
-                                         const std::string& value)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO DicomIdentifiers VALUES(?, ?, ?, ?)");
-    s.BindInt64(0, id);
-    s.BindInt(1, tag.GetGroup());
-    s.BindInt(2, tag.GetElement());
-    s.BindString(3, value);
-    s.Run();
-  }
-
-
-  void DatabaseWrapper::GetMainDicomTags(DicomMap& map,
-                                         int64_t id)
-  {
-    map.Clear();
-
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM MainDicomTags WHERE id=?");
-    s.BindInt64(0, id);
-    while (s.Step())
-    {
-      map.SetValue(s.ColumnInt(1),
-                   s.ColumnInt(2),
-                   s.ColumnString(3), false);
-    }
-  }
-
-
-  void DatabaseWrapper::GetChildrenPublicId(std::list<std::string>& target,
-                                            int64_t id)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.publicId FROM Resources AS a, Resources AS b  "
-                        "WHERE a.parentId = b.internalId AND b.internalId = ?");     
-    s.BindInt64(0, id);
-
-    target.clear();
-
-    while (s.Step())
-    {
-      target.push_back(s.ColumnString(0));
-    }
-  }
-
-
-  void DatabaseWrapper::GetChildrenInternalId(std::list<int64_t>& target,
-                                              int64_t id)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.internalId FROM Resources AS a, Resources AS b  "
-                        "WHERE a.parentId = b.internalId AND b.internalId = ?");     
-    s.BindInt64(0, id);
-
-    target.clear();
-
-    while (s.Step())
-    {
-      target.push_back(s.ColumnInt64(0));
-    }
-  }
-
-
-  void DatabaseWrapper::LogChange(int64_t internalId,
-                                  const ServerIndexChange& change)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Changes VALUES(NULL, ?, ?, ?, ?)");
-    s.BindInt(0, change.GetChangeType());
-    s.BindInt64(1, internalId);
-    s.BindInt(2, change.GetResourceType());
-    s.BindString(3, change.GetDate());
-    s.Run();
-  }
-
-
-  void DatabaseWrapper::LogExportedResource(const ExportedResource& resource)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "INSERT INTO ExportedResources VALUES(NULL, ?, ?, ?, ?, ?, ?, ?, ?)");
-
-    s.BindInt(0, resource.GetResourceType());
-    s.BindString(1, resource.GetPublicId());
-    s.BindString(2, resource.GetModality());
-    s.BindString(3, resource.GetPatientId());
-    s.BindString(4, resource.GetStudyInstanceUid());
-    s.BindString(5, resource.GetSeriesInstanceUid());
-    s.BindString(6, resource.GetSopInstanceUid());
-    s.BindString(7, resource.GetDate());
-    s.Run();      
-  }
-
-
-  void DatabaseWrapper::GetExportedResources(std::list<ExportedResource>& target,
-                                             bool& done,
-                                             int64_t since,
-                                             uint32_t maxResults)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT * FROM ExportedResources WHERE seq>? ORDER BY seq LIMIT ?");
-    s.BindInt64(0, since);
-    s.BindInt(1, maxResults + 1);
-    GetExportedResourcesInternal(target, done, s, maxResults);
-  }
-
-    
-  void DatabaseWrapper::GetLastExportedResource(std::list<ExportedResource>& target)
-  {
-    bool done;  // Ignored
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT * FROM ExportedResources ORDER BY seq DESC LIMIT 1");
-    GetExportedResourcesInternal(target, done, s, 1);
-  }
-
-    
-  uint64_t DatabaseWrapper::GetTotalCompressedSize()
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(compressedSize) FROM AttachedFiles");
-    s.Run();
-    return static_cast<uint64_t>(s.ColumnInt64(0));
-  }
-
-    
-  uint64_t DatabaseWrapper::GetTotalUncompressedSize()
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(uncompressedSize) FROM AttachedFiles");
-    s.Run();
-    return static_cast<uint64_t>(s.ColumnInt64(0));
-  }
-
-
-  uint64_t DatabaseWrapper::GetResourceCount(ResourceType resourceType)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT COUNT(*) FROM Resources WHERE resourceType=?");
-    s.BindInt(0, resourceType);
-    
-    if (!s.Step())
-    {
-      return 0;
-    }
-    else
-    {
-      int64_t c = s.ColumnInt(0);
-      assert(!s.Step());
-      return c;
-    }
-  }
-
-
-  void DatabaseWrapper::GetAllInternalIds(std::list<int64_t>& target,
-                                          ResourceType resourceType)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT internalId FROM Resources WHERE resourceType=?");
-    s.BindInt(0, resourceType);
-
-    target.clear();
-    while (s.Step())
-    {
-      target.push_back(s.ColumnInt64(0));
-    }
-  }
-
-
-  void DatabaseWrapper::GetAllPublicIds(std::list<std::string>& target,
-                                        ResourceType resourceType)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Resources WHERE resourceType=?");
-    s.BindInt(0, resourceType);
-
-    target.clear();
-    while (s.Step())
-    {
-      target.push_back(s.ColumnString(0));
-    }
-  }
-
-
-  void DatabaseWrapper::GetAllPublicIds(std::list<std::string>& target,
-                                        ResourceType resourceType,
-                                        size_t since,
-                                        size_t limit)
-  {
-    if (limit == 0)
-    {
-      target.clear();
-      return;
-    }
-
-    SQLite::Statement s(db_, SQLITE_FROM_HERE,
-                        "SELECT publicId FROM Resources WHERE "
-                        "resourceType=? LIMIT ? OFFSET ?");
-    s.BindInt(0, resourceType);
-    s.BindInt64(1, limit);
-    s.BindInt64(2, since);
-
-    target.clear();
-    while (s.Step())
-    {
-      target.push_back(s.ColumnString(0));
-    }
-  }
-
-
-  bool DatabaseWrapper::SelectPatientToRecycle(int64_t& internalId)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE,
-                        "SELECT patientId FROM PatientRecyclingOrder ORDER BY seq ASC LIMIT 1");
-   
-    if (!s.Step())
-    {
-      // No patient remaining or all the patients are protected
-      return false;
-    }
-    else
-    {
-      internalId = s.ColumnInt(0);
-      return true;
-    }    
-  }
-
-
-  bool DatabaseWrapper::SelectPatientToRecycle(int64_t& internalId,
-                                               int64_t patientIdToAvoid)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE,
-                        "SELECT patientId FROM PatientRecyclingOrder "
-                        "WHERE patientId != ? ORDER BY seq ASC LIMIT 1");
-    s.BindInt64(0, patientIdToAvoid);
-
-    if (!s.Step())
-    {
-      // No patient remaining or all the patients are protected
-      return false;
-    }
-    else
-    {
-      internalId = s.ColumnInt(0);
-      return true;
-    }   
-  }
-
-
-  bool DatabaseWrapper::IsProtectedPatient(int64_t internalId)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE,
-                        "SELECT * FROM PatientRecyclingOrder WHERE patientId = ?");
-    s.BindInt64(0, internalId);
-    return !s.Step();
-  }
-
-
-  void DatabaseWrapper::SetProtectedPatient(int64_t internalId, 
-                                            bool isProtected)
-  {
-    if (isProtected)
-    {
-      SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM PatientRecyclingOrder WHERE patientId=?");
-      s.BindInt64(0, internalId);
-      s.Run();
-    }
-    else if (IsProtectedPatient(internalId))
-    {
-      SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO PatientRecyclingOrder VALUES(NULL, ?)");
-      s.BindInt64(0, internalId);
-      s.Run();
-    }
-    else
-    {
-      // Nothing to do: The patient is already unprotected
-    }
-  }
-
-
-  bool DatabaseWrapper::IsExistingResource(int64_t internalId)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT * FROM Resources WHERE internalId=?");
-    s.BindInt64(0, internalId);
-    return s.Step();
-  }
-
-
-  void DatabaseWrapper::LookupIdentifier(std::list<int64_t>& target,
-                                         ResourceType level,
-                                         const DicomTag& tag,
-                                         IdentifierConstraintType type,
-                                         const std::string& value)
-  {
-    static const char* COMMON = ("SELECT d.id FROM DicomIdentifiers AS d, Resources AS r WHERE "
-                                 "d.id = r.internalId AND r.resourceType=? AND "
-                                 "d.tagGroup=? AND d.tagElement=? AND ");
-
-    std::auto_ptr<SQLite::Statement> s;
-
-    switch (type)
-    {
-      case IdentifierConstraintType_GreaterOrEqual:
-        s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value>=?"));
-        break;
-
-      case IdentifierConstraintType_SmallerOrEqual:
-        s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value<=?"));
-        break;
-
-      case IdentifierConstraintType_Wildcard:
-        s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value GLOB ?"));
-        break;
-
-      case IdentifierConstraintType_Equal:
-      default:
-        s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value=?"));
-        break;
-    }
-
-    assert(s.get() != NULL);
-
-    s->BindInt(0, level);
-    s->BindInt(1, tag.GetGroup());
-    s->BindInt(2, tag.GetElement());
-    s->BindString(3, value);
-
-    target.clear();
-
-    while (s->Step())
-    {
-      target.push_back(s->ColumnInt64(0));
-    }    
-  }
-
-
-  void DatabaseWrapper::LookupIdentifierRange(std::list<int64_t>& target,
-                                              ResourceType level,
-                                              const DicomTag& tag,
-                                              const std::string& start,
-                                              const std::string& end)
-  {
-    SQLite::Statement statement(db_, SQLITE_FROM_HERE,
-                                "SELECT d.id FROM DicomIdentifiers AS d, Resources AS r WHERE "
-                                "d.id = r.internalId AND r.resourceType=? AND "
-                                "d.tagGroup=? AND d.tagElement=? AND d.value>=? AND d.value<=?");
-
-    statement.BindInt(0, level);
-    statement.BindInt(1, tag.GetGroup());
-    statement.BindInt(2, tag.GetElement());
-    statement.BindString(3, start);
-    statement.BindString(4, end);
-
-    target.clear();
-
-    while (statement.Step())
-    {
-      target.push_back(statement.ColumnInt64(0));
-    }    
-  }
-}
--- a/OrthancServer/DatabaseWrapper.h	Wed Dec 19 14:35:55 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,280 +0,0 @@
-/**
- * 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "IDatabaseWrapper.h"
-
-#include "../Core/SQLite/Connection.h"
-#include "../Core/SQLite/Transaction.h"
-
-namespace Orthanc
-{
-  namespace Internals
-  {
-    class SignalRemainingAncestor;
-  }
-
-  /**
-   * This class manages an instance of the Orthanc SQLite database. It
-   * translates low-level requests into SQL statements. Mutual
-   * exclusion MUST be implemented at a higher level.
-   **/
-  class DatabaseWrapper : public IDatabaseWrapper
-  {
-  private:
-    IDatabaseListener* listener_;
-    SQLite::Connection db_;
-    Internals::SignalRemainingAncestor* signalRemainingAncestor_;
-    unsigned int version_;
-
-    void GetChangesInternal(std::list<ServerIndexChange>& target,
-                            bool& done,
-                            SQLite::Statement& s,
-                            uint32_t maxResults);
-
-    void GetExportedResourcesInternal(std::list<ExportedResource>& target,
-                                      bool& done,
-                                      SQLite::Statement& s,
-                                      uint32_t maxResults);
-
-    void ClearTable(const std::string& tableName);
-
-  public:
-    DatabaseWrapper(const std::string& path);
-
-    DatabaseWrapper();
-
-    virtual void Open();
-
-    virtual void Close()
-    {
-      db_.Close();
-    }
-
-    virtual void SetListener(IDatabaseListener& listener);
-
-    virtual bool LookupParent(int64_t& parentId,
-                              int64_t resourceId);
-
-    virtual std::string GetPublicId(int64_t resourceId);
-
-    virtual ResourceType GetResourceType(int64_t resourceId);
-
-    virtual void DeleteResource(int64_t id);
-
-    virtual void GetChanges(std::list<ServerIndexChange>& target /*out*/,
-                            bool& done /*out*/,
-                            int64_t since,
-                            uint32_t maxResults);
-
-    virtual void GetLastChange(std::list<ServerIndexChange>& target /*out*/);
-
-    virtual SQLite::ITransaction* StartTransaction()
-    {
-      return new SQLite::Transaction(db_);
-    }
-
-    virtual void FlushToDisk()
-    {
-      db_.FlushToDisk();
-    }
-
-    virtual bool HasFlushToDisk() const
-    {
-      return true;
-    }
-
-    virtual void ClearChanges()
-    {
-      ClearTable("Changes");
-    }
-
-    virtual void ClearExportedResources()
-    {
-      ClearTable("ExportedResources");
-    }
-
-    virtual void GetAllMetadata(std::map<MetadataType, std::string>& target,
-                                int64_t id);
-
-    virtual unsigned int GetDatabaseVersion()
-    {
-      return version_;
-    }
-
-    virtual void Upgrade(unsigned int targetVersion,
-                         IStorageArea& storageArea);
-
-
-    /**
-     * The methods declared below are for unit testing only!
-     **/
-
-    const char* GetErrorMessage() const
-    {
-      return db_.GetErrorMessage();
-    }
-
-    void GetChildren(std::list<std::string>& childrenPublicIds,
-                     int64_t id);
-
-    int64_t GetTableRecordCount(const std::string& table);
-    
-    bool GetParentPublicId(std::string& target,
-                           int64_t id);
-
-
-
-    /**
-     * Until Orthanc 1.4.0, the methods below were part of the
-     * "DatabaseWrapperBase" class, that is now placed in the
-     * graveyard.
-     **/
-
-    virtual void SetGlobalProperty(GlobalProperty property,
-                                   const std::string& value);
-
-    virtual bool LookupGlobalProperty(std::string& target,
-                                      GlobalProperty property);
-
-    virtual int64_t CreateResource(const std::string& publicId,
-                                   ResourceType type);
-
-    virtual bool LookupResource(int64_t& id,
-                                ResourceType& type,
-                                const std::string& publicId);
-
-    virtual void AttachChild(int64_t parent,
-                             int64_t child);
-
-    virtual void SetMetadata(int64_t id,
-                             MetadataType type,
-                             const std::string& value);
-
-    virtual void DeleteMetadata(int64_t id,
-                                MetadataType type);
-
-    virtual bool LookupMetadata(std::string& target,
-                                int64_t id,
-                                MetadataType type);
-
-    virtual void ListAvailableMetadata(std::list<MetadataType>& target,
-                                       int64_t id);
-
-    virtual void AddAttachment(int64_t id,
-                               const FileInfo& attachment);
-
-    virtual void DeleteAttachment(int64_t id,
-                                  FileContentType attachment);
-
-    virtual void ListAvailableAttachments(std::list<FileContentType>& target,
-                                          int64_t id);
-
-    virtual bool LookupAttachment(FileInfo& attachment,
-                                  int64_t id,
-                                  FileContentType contentType);
-
-    virtual void ClearMainDicomTags(int64_t id);
-
-    virtual void SetMainDicomTag(int64_t id,
-                                 const DicomTag& tag,
-                                 const std::string& value);
-
-    virtual void SetIdentifierTag(int64_t id,
-                                  const DicomTag& tag,
-                                  const std::string& value);
-
-    virtual void GetMainDicomTags(DicomMap& map,
-                                  int64_t id);
-
-    virtual void GetChildrenPublicId(std::list<std::string>& target,
-                                     int64_t id);
-
-    virtual void GetChildrenInternalId(std::list<int64_t>& target,
-                                       int64_t id);
-
-    virtual void LogChange(int64_t internalId,
-                           const ServerIndexChange& change);
-
-    virtual void LogExportedResource(const ExportedResource& resource);
-    
-    virtual void GetExportedResources(std::list<ExportedResource>& target /*out*/,
-                                      bool& done /*out*/,
-                                      int64_t since,
-                                      uint32_t maxResults);
-
-    virtual void GetLastExportedResource(std::list<ExportedResource>& target /*out*/);
-
-    virtual uint64_t GetTotalCompressedSize();
-    
-    virtual uint64_t GetTotalUncompressedSize();
-
-    virtual uint64_t GetResourceCount(ResourceType resourceType);
-
-    virtual void GetAllInternalIds(std::list<int64_t>& target,
-                                   ResourceType resourceType);
-
-    virtual void GetAllPublicIds(std::list<std::string>& target,
-                                 ResourceType resourceType);
-
-    virtual void GetAllPublicIds(std::list<std::string>& target,
-                                 ResourceType resourceType,
-                                 size_t since,
-                                 size_t limit);
-
-    virtual bool SelectPatientToRecycle(int64_t& internalId);
-
-    virtual bool SelectPatientToRecycle(int64_t& internalId,
-                                        int64_t patientIdToAvoid);
-
-    virtual bool IsProtectedPatient(int64_t internalId);
-
-    virtual void SetProtectedPatient(int64_t internalId, 
-                                     bool isProtected);
-
-    virtual bool IsExistingResource(int64_t internalId);
-
-    virtual void LookupIdentifier(std::list<int64_t>& result,
-                                  ResourceType level,
-                                  const DicomTag& tag,
-                                  IdentifierConstraintType type,
-                                  const std::string& value);
-
-    virtual void LookupIdentifierRange(std::list<int64_t>& result,
-                                       ResourceType level,
-                                       const DicomTag& tag,
-                                       const std::string& start,
-                                       const std::string& end);
-  };
-}
--- a/OrthancServer/IDatabaseWrapper.h	Wed Dec 19 14:35:55 2018 +0100
+++ b/OrthancServer/IDatabaseWrapper.h	Wed Dec 19 16:31:10 2018 +0100
@@ -34,11 +34,14 @@
 #pragma once
 
 #include "../Core/DicomFormat/DicomMap.h"
-#include "../Core/SQLite/ITransaction.h"
+#include "../Core/FileStorage/FileInfo.h"
 #include "../Core/FileStorage/IStorageArea.h"
-#include "../Core/FileStorage/FileInfo.h"
+#include "../Core/SQLite/ITransaction.h"
+
+#include "ExportedResource.h"
 #include "IDatabaseListener.h"
-#include "ExportedResource.h"
+#include "Search/DatabaseConstraint.h"
+#include "Search/DatabaseLookup.h"
 
 #include <list>
 #include <boost/noncopyable.hpp>
@@ -48,6 +51,21 @@
   class IDatabaseWrapper : public boost::noncopyable
   {
   public:
+    class ITransaction : public boost::noncopyable
+    {
+    public:
+      virtual ~ITransaction()
+      {
+      }
+
+      virtual void Begin() = 0;
+
+      virtual void Rollback() = 0;
+
+      virtual void Commit(int64_t fileSizeDelta) = 0;
+    };
+
+
     virtual ~IDatabaseWrapper()
     {
     }
@@ -198,7 +216,7 @@
     virtual void SetProtectedPatient(int64_t internalId, 
                                      bool isProtected) = 0;
 
-    virtual SQLite::ITransaction* StartTransaction() = 0;
+    virtual ITransaction* StartTransaction() = 0;
 
     virtual void SetListener(IDatabaseListener& listener) = 0;
 
@@ -206,5 +224,13 @@
 
     virtual void Upgrade(unsigned int targetVersion,
                          IStorageArea& storageArea) = 0;
+
+    virtual bool IsDiskSizeAbove(uint64_t threshold) = 0;
+
+    virtual void ApplyLookupResources(std::vector<std::string>& resourcesId,
+                                      std::vector<std::string>* instancesId,   // Can be NULL if not needed
+                                      const std::vector<DatabaseConstraint>& lookup,
+                                      ResourceType queryLevel,
+                                      size_t limit) = 0;
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/InstallTrackAttachmentsSize.sql	Wed Dec 19 16:31:10 2018 +0100
@@ -0,0 +1,22 @@
+CREATE TABLE GlobalIntegers(
+       key INTEGER PRIMARY KEY,
+       value INTEGER);
+
+INSERT INTO GlobalProperties VALUES (6, 1);  -- GlobalProperty_GetTotalSizeIsFast
+
+INSERT INTO GlobalIntegers SELECT 0, IFNULL(SUM(compressedSize), 0) FROM AttachedFiles;
+INSERT INTO GlobalIntegers SELECT 1, IFNULL(SUM(uncompressedSize), 0) FROM AttachedFiles;
+
+CREATE TRIGGER AttachedFileIncrementSize
+AFTER INSERT ON AttachedFiles
+BEGIN
+  UPDATE GlobalIntegers SET value = value + new.compressedSize WHERE key = 0;
+  UPDATE GlobalIntegers SET value = value + new.uncompressedSize WHERE key = 1;
+END;
+
+CREATE TRIGGER AttachedFileDecrementSize
+AFTER DELETE ON AttachedFiles
+BEGIN
+  UPDATE GlobalIntegers SET value = value - old.compressedSize WHERE key = 0;
+  UPDATE GlobalIntegers SET value = value - old.uncompressedSize WHERE key = 1;
+END;
--- a/OrthancServer/OrthancFindRequestHandler.cpp	Wed Dec 19 14:35:55 2018 +0100
+++ b/OrthancServer/OrthancFindRequestHandler.cpp	Wed Dec 19 16:31:10 2018 +0100
@@ -39,7 +39,6 @@
 #include "../Core/Logging.h"
 #include "../Core/DicomParsing/FromDcmtkBridge.h"
 #include "OrthancConfiguration.h"
-#include "Search/LookupResource.h"
 #include "ServerToolbox.h"
 
 #include <boost/regex.hpp> 
@@ -614,7 +613,7 @@
      * Build up the query object.
      **/
 
-    LookupResource lookup(level);
+    DatabaseLookup lookup;
 
     bool caseSensitivePN;
 
@@ -654,7 +653,7 @@
           sensitive = caseSensitivePN;
         }
 
-        lookup.AddDicomConstraint(tag, value, sensitive);
+        lookup.AddDicomConstraint(tag, value, sensitive, true /* mandatory */);
       }
       else
       {
@@ -672,7 +671,7 @@
 
 
     LookupVisitor visitor(answers, context_, level, *filteredInput, sequencesToReturn);
-    context_.Apply(visitor, lookup, 0 /* "since" is not relevant to C-FIND */, limit);
+    context_.Apply(visitor, lookup, level, 0 /* "since" is not relevant to C-FIND */, limit);
   }
 
 
--- a/OrthancServer/OrthancInitialization.cpp	Wed Dec 19 14:35:55 2018 +0100
+++ b/OrthancServer/OrthancInitialization.cpp	Wed Dec 19 16:31:10 2018 +0100
@@ -45,7 +45,7 @@
 #include "../Core/Logging.h"
 #include "../Core/OrthancException.h"
 
-#include "DatabaseWrapper.h"
+#include "SQLiteDatabaseWrapper.h"
 #include "OrthancConfiguration.h"
 
 #include <dcmtk/dcmnet/dul.h>   // For dcmDisableGethostbyaddr()
@@ -308,7 +308,7 @@
     {
     }
 
-    return new DatabaseWrapper(indexDirectory.string() + "/index");
+    return new SQLiteDatabaseWrapper(indexDirectory.string() + "/index");
   }
 
 
--- a/OrthancServer/OrthancMoveRequestHandler.cpp	Wed Dec 19 14:35:55 2018 +0100
+++ b/OrthancServer/OrthancMoveRequestHandler.cpp	Wed Dec 19 16:31:10 2018 +0100
@@ -226,7 +226,7 @@
 
     const std::string& content = value.GetContent();
 
-    std::list<std::string> ids;
+    std::vector<std::string> ids;
     context_.GetIndex().LookupIdentifierExact(ids, level, tag, content);
 
     if (ids.size() != 1)
@@ -235,7 +235,7 @@
     }
     else
     {
-      publicId = ids.front();
+      publicId = ids[0];
       return true;
     }
   }
--- a/OrthancServer/OrthancRestApi/OrthancRestResources.cpp	Wed Dec 19 14:35:55 2018 +0100
+++ b/OrthancServer/OrthancRestApi/OrthancRestResources.cpp	Wed Dec 19 16:31:10 2018 +0100
@@ -40,7 +40,6 @@
 #include "../../Core/HttpServer/HttpContentNegociation.h"
 #include "../../Core/Logging.h"
 #include "../OrthancConfiguration.h"
-#include "../Search/LookupResource.h"
 #include "../ServerContext.h"
 #include "../ServerToolbox.h"
 #include "../SliceOrdering.h"
@@ -1227,13 +1226,12 @@
                                       const std::string& value,
                                       ResourceType level)
   {
-    std::list<std::string> tmp;
+    std::vector<std::string> tmp;
     index.LookupIdentifierExact(tmp, level, tag, value);
 
-    for (std::list<std::string>::const_iterator
-           it = tmp.begin(); it != tmp.end(); ++it)
+    for (size_t i = 0; i < tmp.size(); i++)
     {
-      result.push_back(std::make_pair(level, *it));
+      result.push_back(std::make_pair(level, tmp[i]));
     }
   }
 
@@ -1400,9 +1398,9 @@
         since = static_cast<size_t>(tmp);
       }
 
-      std::string level = request[KEY_LEVEL].asString();
+      ResourceType level = StringToResourceType(request[KEY_LEVEL].asCString());
 
-      LookupResource query(StringToResourceType(level.c_str()));
+      DatabaseLookup query;
 
       Json::Value::Members members = request[KEY_QUERY].getMemberNames();
       for (size_t i = 0; i < members.size(); i++)
@@ -1413,14 +1411,14 @@
                                  "Tag \"" + members[i] + "\" should be associated with a string");
         }
 
-        query.AddDicomConstraint(FromDcmtkBridge::ParseTag(members[i]), 
-                                 request[KEY_QUERY][members[i]].asString(),
-                                 caseSensitive);
+        query.AddRestConstraint(FromDcmtkBridge::ParseTag(members[i]), 
+                                request[KEY_QUERY][members[i]].asString(),
+                                caseSensitive, true);
       }
 
       FindVisitor visitor;
-      context.Apply(visitor, query, since, limit);
-      visitor.Answer(call.GetOutput(), context.GetIndex(), query.GetLevel(), expand);
+      context.Apply(visitor, query, level, since, limit);
+      visitor.Answer(call.GetOutput(), context.GetIndex(), level, expand);
     }
   }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/SQLiteDatabaseWrapper.cpp	Wed Dec 19 16:31:10 2018 +0100
@@ -0,0 +1,1622 @@
+/**
+ * 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 General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "PrecompiledHeadersServer.h"
+#include "SQLiteDatabaseWrapper.h"
+
+#include "../Core/DicomFormat/DicomArray.h"
+#include "../Core/Logging.h"
+#include "../Core/SQLite/Transaction.h"
+#include "EmbeddedResources.h"
+#include "ServerToolbox.h"
+
+#include <stdio.h>
+#include <boost/lexical_cast.hpp>
+
+namespace Orthanc
+{
+  namespace Internals
+  {
+    class SignalFileDeleted : public SQLite::IScalarFunction
+    {
+    private:
+      IDatabaseListener& listener_;
+
+    public:
+      SignalFileDeleted(IDatabaseListener& listener) :
+        listener_(listener)
+      {
+      }
+
+      virtual const char* GetName() const
+      {
+        return "SignalFileDeleted";
+      }
+
+      virtual unsigned int GetCardinality() const
+      {
+        return 7;
+      }
+
+      virtual void Compute(SQLite::FunctionContext& context)
+      {
+        std::string uncompressedMD5, compressedMD5;
+
+        if (!context.IsNullValue(5))
+        {
+          uncompressedMD5 = context.GetStringValue(5);
+        }
+
+        if (!context.IsNullValue(6))
+        {
+          compressedMD5 = context.GetStringValue(6);
+        }
+
+        FileInfo info(context.GetStringValue(0),
+                      static_cast<FileContentType>(context.GetIntValue(1)),
+                      static_cast<uint64_t>(context.GetInt64Value(2)),
+                      uncompressedMD5,
+                      static_cast<CompressionType>(context.GetIntValue(3)),
+                      static_cast<uint64_t>(context.GetInt64Value(4)),
+                      compressedMD5);
+        
+        listener_.SignalFileDeleted(info);
+      }
+    };
+
+    class SignalResourceDeleted : public SQLite::IScalarFunction
+    {
+    private:
+      IDatabaseListener& listener_;
+
+    public:
+      SignalResourceDeleted(IDatabaseListener& listener) :
+        listener_(listener)
+      {
+      }
+
+      virtual const char* GetName() const
+      {
+        return "SignalResourceDeleted";
+      }
+
+      virtual unsigned int GetCardinality() const
+      {
+        return 2;
+      }
+
+      virtual void Compute(SQLite::FunctionContext& context)
+      {
+        ResourceType type = static_cast<ResourceType>(context.GetIntValue(1));
+        ServerIndexChange change(ChangeType_Deleted, type, context.GetStringValue(0));
+        listener_.SignalChange(change);
+      }
+    };
+
+    class SignalRemainingAncestor : public SQLite::IScalarFunction
+    {
+    private:
+      bool hasRemainingAncestor_;
+      std::string remainingPublicId_;
+      ResourceType remainingType_;
+
+    public:
+      SignalRemainingAncestor() : 
+        hasRemainingAncestor_(false)
+      {
+      }
+
+      void Reset()
+      {
+        hasRemainingAncestor_ = false;
+      }
+
+      virtual const char* GetName() const
+      {
+        return "SignalRemainingAncestor";
+      }
+
+      virtual unsigned int GetCardinality() const
+      {
+        return 2;
+      }
+
+      virtual void Compute(SQLite::FunctionContext& context)
+      {
+        VLOG(1) << "There exists a remaining ancestor with public ID \""
+                << context.GetStringValue(0)
+                << "\" of type "
+                << context.GetIntValue(1);
+
+        if (!hasRemainingAncestor_ ||
+            remainingType_ >= context.GetIntValue(1))
+        {
+          hasRemainingAncestor_ = true;
+          remainingPublicId_ = context.GetStringValue(0);
+          remainingType_ = static_cast<ResourceType>(context.GetIntValue(1));
+        }
+      }
+
+      bool HasRemainingAncestor() const
+      {
+        return hasRemainingAncestor_;
+      }
+
+      const std::string& GetRemainingAncestorId() const
+      {
+        assert(hasRemainingAncestor_);
+        return remainingPublicId_;
+      }
+
+      ResourceType GetRemainingAncestorType() const
+      {
+        assert(hasRemainingAncestor_);
+        return remainingType_;
+      }
+    };
+  }
+
+
+  void SQLiteDatabaseWrapper::GetChangesInternal(std::list<ServerIndexChange>& target,
+                                                 bool& done,
+                                                 SQLite::Statement& s,
+                                                 uint32_t maxResults)
+  {
+    target.clear();
+
+    while (target.size() < maxResults && s.Step())
+    {
+      int64_t seq = s.ColumnInt64(0);
+      ChangeType changeType = static_cast<ChangeType>(s.ColumnInt(1));
+      ResourceType resourceType = static_cast<ResourceType>(s.ColumnInt(3));
+      const std::string& date = s.ColumnString(4);
+
+      int64_t internalId = s.ColumnInt64(2);
+      std::string publicId = GetPublicId(internalId);
+
+      target.push_back(ServerIndexChange(seq, changeType, resourceType, publicId, date));
+    }
+
+    done = !(target.size() == maxResults && s.Step());
+  }
+
+
+  void SQLiteDatabaseWrapper::GetExportedResourcesInternal(std::list<ExportedResource>& target,
+                                                           bool& done,
+                                                           SQLite::Statement& s,
+                                                           uint32_t maxResults)
+  {
+    target.clear();
+
+    while (target.size() < maxResults && s.Step())
+    {
+      int64_t seq = s.ColumnInt64(0);
+      ResourceType resourceType = static_cast<ResourceType>(s.ColumnInt(1));
+      std::string publicId = s.ColumnString(2);
+
+      ExportedResource resource(seq, 
+                                resourceType,
+                                publicId,
+                                s.ColumnString(3),  // modality
+                                s.ColumnString(8),  // date
+                                s.ColumnString(4),  // patient ID
+                                s.ColumnString(5),  // study instance UID
+                                s.ColumnString(6),  // series instance UID
+                                s.ColumnString(7)); // sop instance UID
+
+      target.push_back(resource);
+    }
+
+    done = !(target.size() == maxResults && s.Step());
+  }
+
+
+  void SQLiteDatabaseWrapper::GetChildren(std::list<std::string>& childrenPublicIds,
+                                          int64_t id)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Resources WHERE parentId=?");
+    s.BindInt64(0, id);
+
+    childrenPublicIds.clear();
+    while (s.Step())
+    {
+      childrenPublicIds.push_back(s.ColumnString(0));
+    }
+  }
+
+
+  void SQLiteDatabaseWrapper::DeleteResource(int64_t id)
+  {
+    signalRemainingAncestor_->Reset();
+
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM Resources WHERE internalId=?");
+    s.BindInt64(0, id);
+    s.Run();
+
+    if (signalRemainingAncestor_->HasRemainingAncestor() &&
+        listener_ != NULL)
+    {
+      listener_->SignalRemainingAncestor(signalRemainingAncestor_->GetRemainingAncestorType(),
+                                         signalRemainingAncestor_->GetRemainingAncestorId());
+    }
+  }
+
+
+  bool SQLiteDatabaseWrapper::GetParentPublicId(std::string& target,
+                                                int64_t id)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.publicId FROM Resources AS a, Resources AS b "
+                        "WHERE a.internalId = b.parentId AND b.internalId = ?");     
+    s.BindInt64(0, id);
+
+    if (s.Step())
+    {
+      target = s.ColumnString(0);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
+  int64_t SQLiteDatabaseWrapper::GetTableRecordCount(const std::string& table)
+  {
+    char buf[128];
+    sprintf(buf, "SELECT COUNT(*) FROM %s", table.c_str());
+    SQLite::Statement s(db_, buf);
+
+    if (!s.Step())
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    int64_t c = s.ColumnInt(0);
+    assert(!s.Step());
+
+    return c;
+  }
+
+    
+  SQLiteDatabaseWrapper::SQLiteDatabaseWrapper(const std::string& path) : 
+    listener_(NULL), 
+    signalRemainingAncestor_(NULL),
+    version_(0)
+  {
+    db_.Open(path);
+  }
+
+
+  SQLiteDatabaseWrapper::SQLiteDatabaseWrapper() : 
+    listener_(NULL), 
+    signalRemainingAncestor_(NULL),
+    version_(0)
+  {
+    db_.OpenInMemory();
+  }
+
+
+  void SQLiteDatabaseWrapper::Open()
+  {
+    db_.Execute("PRAGMA ENCODING=\"UTF-8\";");
+
+    // Performance tuning of SQLite with PRAGMAs
+    // http://www.sqlite.org/pragma.html
+    db_.Execute("PRAGMA SYNCHRONOUS=NORMAL;");
+    db_.Execute("PRAGMA JOURNAL_MODE=WAL;");
+    db_.Execute("PRAGMA LOCKING_MODE=EXCLUSIVE;");
+    db_.Execute("PRAGMA WAL_AUTOCHECKPOINT=1000;");
+    //db_.Execute("PRAGMA TEMP_STORE=memory");
+
+    // Make "LIKE" case-sensitive in SQLite 
+    db_.Execute("PRAGMA case_sensitive_like = true;");
+    
+    if (!db_.DoesTableExist("GlobalProperties"))
+    {
+      LOG(INFO) << "Creating the database";
+      std::string query;
+      EmbeddedResources::GetFileResource(query, EmbeddedResources::PREPARE_DATABASE);
+      db_.Execute(query);
+    }
+
+    // Check the version of the database
+    std::string tmp;
+    if (!LookupGlobalProperty(tmp, GlobalProperty_DatabaseSchemaVersion))
+    {
+      tmp = "Unknown";
+    }
+
+    bool ok = false;
+    try
+    {
+      LOG(INFO) << "Version of the Orthanc database: " << tmp;
+      version_ = boost::lexical_cast<unsigned int>(tmp);
+      ok = true;
+    }
+    catch (boost::bad_lexical_cast&)
+    {
+    }
+
+    if (!ok)
+    {
+      throw OrthancException(ErrorCode_IncompatibleDatabaseVersion,
+                             "Incompatible version of the Orthanc database: " + tmp);
+    }
+
+    // New in Orthanc 1.5.1
+    if (version_ == 6)
+    {
+      if (!LookupGlobalProperty(tmp, GlobalProperty_GetTotalSizeIsFast) ||
+          tmp != "1")
+      {
+        LOG(INFO) << "Installing the SQLite triggers to track the size of the attachments";
+        std::string query;
+        EmbeddedResources::GetFileResource(query, EmbeddedResources::INSTALL_TRACK_ATTACHMENTS_SIZE);
+        db_.Execute(query);
+      }
+    }
+
+    signalRemainingAncestor_ = new Internals::SignalRemainingAncestor;
+    db_.Register(signalRemainingAncestor_);
+  }
+
+
+  static void ExecuteUpgradeScript(SQLite::Connection& db,
+                                   EmbeddedResources::FileResourceId script)
+  {
+    std::string upgrade;
+    EmbeddedResources::GetFileResource(upgrade, script);
+    db.BeginTransaction();
+    db.Execute(upgrade);
+    db.CommitTransaction();    
+  }
+
+
+  void SQLiteDatabaseWrapper::Upgrade(unsigned int targetVersion,
+                                      IStorageArea& storageArea)
+  {
+    if (targetVersion != 6)
+    {
+      throw OrthancException(ErrorCode_IncompatibleDatabaseVersion);
+    }
+
+    // This version of Orthanc is only compatible with versions 3, 4,
+    // 5 and 6 of the DB schema
+    if (version_ != 3 &&
+        version_ != 4 &&
+        version_ != 5 &&
+        version_ != 6)
+    {
+      throw OrthancException(ErrorCode_IncompatibleDatabaseVersion);
+    }
+
+    if (version_ == 3)
+    {
+      LOG(WARNING) << "Upgrading database version from 3 to 4";
+      ExecuteUpgradeScript(db_, EmbeddedResources::UPGRADE_DATABASE_3_TO_4);
+      version_ = 4;
+    }
+
+    if (version_ == 4)
+    {
+      LOG(WARNING) << "Upgrading database version from 4 to 5";
+      ExecuteUpgradeScript(db_, EmbeddedResources::UPGRADE_DATABASE_4_TO_5);
+      version_ = 5;
+    }
+
+    if (version_ == 5)
+    {
+      LOG(WARNING) << "Upgrading database version from 5 to 6";
+      // No change in the DB schema, the step from version 5 to 6 only
+      // consists in reconstructing the main DICOM tags information
+      // (as more tags got included).
+      db_.BeginTransaction();
+      ServerToolbox::ReconstructMainDicomTags(*this, storageArea, ResourceType_Patient);
+      ServerToolbox::ReconstructMainDicomTags(*this, storageArea, ResourceType_Study);
+      ServerToolbox::ReconstructMainDicomTags(*this, storageArea, ResourceType_Series);
+      ServerToolbox::ReconstructMainDicomTags(*this, storageArea, ResourceType_Instance);
+      db_.Execute("UPDATE GlobalProperties SET value=\"6\" WHERE property=" +
+                  boost::lexical_cast<std::string>(GlobalProperty_DatabaseSchemaVersion) + ";");
+      db_.CommitTransaction();
+      version_ = 6;
+    }
+  }
+
+
+  void SQLiteDatabaseWrapper::SetListener(IDatabaseListener& listener)
+  {
+    listener_ = &listener;
+    db_.Register(new Internals::SignalFileDeleted(listener));
+    db_.Register(new Internals::SignalResourceDeleted(listener));
+  }
+
+
+  void SQLiteDatabaseWrapper::ClearTable(const std::string& tableName)
+  {
+    db_.Execute("DELETE FROM " + tableName);    
+  }
+
+
+  bool SQLiteDatabaseWrapper::LookupParent(int64_t& parentId,
+                                           int64_t resourceId)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                        "SELECT parentId FROM Resources WHERE internalId=?");
+    s.BindInt64(0, resourceId);
+
+    if (!s.Step())
+    {
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+
+    if (s.ColumnIsNull(0))
+    {
+      return false;
+    }
+    else
+    {
+      parentId = s.ColumnInt(0);
+      return true;
+    }
+  }
+
+
+  ResourceType SQLiteDatabaseWrapper::GetResourceType(int64_t resourceId)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                        "SELECT resourceType FROM Resources WHERE internalId=?");
+    s.BindInt64(0, resourceId);
+    
+    if (s.Step())
+    {
+      return static_cast<ResourceType>(s.ColumnInt(0));
+    }
+    else
+    { 
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+  }
+
+
+  std::string SQLiteDatabaseWrapper::GetPublicId(int64_t resourceId)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                        "SELECT publicId FROM Resources WHERE internalId=?");
+    s.BindInt64(0, resourceId);
+    
+    if (s.Step())
+    { 
+      return s.ColumnString(0);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+  }
+
+
+  void SQLiteDatabaseWrapper::GetChanges(std::list<ServerIndexChange>& target /*out*/,
+                                         bool& done /*out*/,
+                                         int64_t since,
+                                         uint32_t maxResults)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes WHERE seq>? ORDER BY seq LIMIT ?");
+    s.BindInt64(0, since);
+    s.BindInt(1, maxResults + 1);
+    GetChangesInternal(target, done, s, maxResults);
+  }
+
+
+  void SQLiteDatabaseWrapper::GetLastChange(std::list<ServerIndexChange>& target /*out*/)
+  {
+    bool done;  // Ignored
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes ORDER BY seq DESC LIMIT 1");
+    GetChangesInternal(target, done, s, 1);
+  }
+
+
+  class SQLiteDatabaseWrapper::Transaction : public IDatabaseWrapper::ITransaction
+  {
+  private:
+    SQLiteDatabaseWrapper&              that_;
+    std::auto_ptr<SQLite::Transaction>  transaction_;
+    int64_t                             initialDiskSize_;
+
+  public:
+    Transaction(SQLiteDatabaseWrapper& that) :
+      that_(that),
+      transaction_(new SQLite::Transaction(that_.db_))
+    {
+#if defined(NDEBUG)
+      // Release mode
+      initialDiskSize_ = 0;
+#else
+      // Debug mode
+      initialDiskSize_ = static_cast<int64_t>(that_.GetTotalCompressedSize());
+#endif
+    }
+
+    virtual void Begin()
+    {
+      transaction_->Begin();
+    }
+
+    virtual void Rollback() 
+    {
+      transaction_->Rollback();
+    }
+
+    virtual void Commit(int64_t fileSizeDelta /* only used in debug */)
+    {
+      transaction_->Commit();
+
+      assert(initialDiskSize_ + fileSizeDelta >= 0 &&
+             initialDiskSize_ + fileSizeDelta == static_cast<int64_t>(that_.GetTotalCompressedSize()));
+    }
+  };
+
+
+  IDatabaseWrapper::ITransaction* SQLiteDatabaseWrapper::StartTransaction()
+  {
+    return new Transaction(*this);
+  }
+
+
+  void SQLiteDatabaseWrapper::GetAllMetadata(std::map<MetadataType, std::string>& target,
+                                             int64_t id)
+  {
+    target.clear();
+
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT type, value FROM Metadata WHERE id=?");
+    s.BindInt64(0, id);
+
+    while (s.Step())
+    {
+      MetadataType key = static_cast<MetadataType>(s.ColumnInt(0));
+      target[key] = s.ColumnString(1);
+    }
+  }
+
+
+  void SQLiteDatabaseWrapper::SetGlobalProperty(GlobalProperty property,
+                                                const std::string& value)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO GlobalProperties VALUES(?, ?)");
+    s.BindInt(0, property);
+    s.BindString(1, value);
+    s.Run();
+  }
+
+
+  bool SQLiteDatabaseWrapper::LookupGlobalProperty(std::string& target,
+                                                   GlobalProperty property)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                        "SELECT value FROM GlobalProperties WHERE property=?");
+    s.BindInt(0, property);
+
+    if (!s.Step())
+    {
+      return false;
+    }
+    else
+    {
+      target = s.ColumnString(0);
+      return true;
+    }
+  }
+
+
+  int64_t SQLiteDatabaseWrapper::CreateResource(const std::string& publicId,
+                                                ResourceType type)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Resources VALUES(NULL, ?, ?, NULL)");
+    s.BindInt(0, type);
+    s.BindString(1, publicId);
+    s.Run();
+    return db_.GetLastInsertRowId();
+  }
+
+
+  bool SQLiteDatabaseWrapper::LookupResource(int64_t& id,
+                                             ResourceType& type,
+                                             const std::string& publicId)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                        "SELECT internalId, resourceType FROM Resources WHERE publicId=?");
+    s.BindString(0, publicId);
+
+    if (!s.Step())
+    {
+      return false;
+    }
+    else
+    {
+      id = s.ColumnInt(0);
+      type = static_cast<ResourceType>(s.ColumnInt(1));
+
+      // Check whether there is a single resource with this public id
+      assert(!s.Step());
+
+      return true;
+    }
+  }
+
+
+  void SQLiteDatabaseWrapper::AttachChild(int64_t parent,
+                                          int64_t child)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "UPDATE Resources SET parentId = ? WHERE internalId = ?");
+    s.BindInt64(0, parent);
+    s.BindInt64(1, child);
+    s.Run();
+  }
+
+
+  void SQLiteDatabaseWrapper::SetMetadata(int64_t id,
+                                          MetadataType type,
+                                          const std::string& value)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO Metadata VALUES(?, ?, ?)");
+    s.BindInt64(0, id);
+    s.BindInt(1, type);
+    s.BindString(2, value);
+    s.Run();
+  }
+
+
+  void SQLiteDatabaseWrapper::DeleteMetadata(int64_t id,
+                                             MetadataType type)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM Metadata WHERE id=? and type=?");
+    s.BindInt64(0, id);
+    s.BindInt(1, type);
+    s.Run();
+  }
+
+
+  bool SQLiteDatabaseWrapper::LookupMetadata(std::string& target,
+                                             int64_t id,
+                                             MetadataType type)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                        "SELECT value FROM Metadata WHERE id=? AND type=?");
+    s.BindInt64(0, id);
+    s.BindInt(1, type);
+
+    if (!s.Step())
+    {
+      return false;
+    }
+    else
+    {
+      target = s.ColumnString(0);
+      return true;
+    }
+  }
+
+
+  void SQLiteDatabaseWrapper::ListAvailableMetadata(std::list<MetadataType>& target,
+                                                    int64_t id)
+  {
+    target.clear();
+
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT type FROM Metadata WHERE id=?");
+    s.BindInt64(0, id);
+
+    while (s.Step())
+    {
+      target.push_back(static_cast<MetadataType>(s.ColumnInt(0)));
+    }
+  }
+
+
+  void SQLiteDatabaseWrapper::AddAttachment(int64_t id,
+                                            const FileInfo& attachment)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO AttachedFiles VALUES(?, ?, ?, ?, ?, ?, ?, ?)");
+    s.BindInt64(0, id);
+    s.BindInt(1, attachment.GetContentType());
+    s.BindString(2, attachment.GetUuid());
+    s.BindInt64(3, attachment.GetCompressedSize());
+    s.BindInt64(4, attachment.GetUncompressedSize());
+    s.BindInt(5, attachment.GetCompressionType());
+    s.BindString(6, attachment.GetUncompressedMD5());
+    s.BindString(7, attachment.GetCompressedMD5());
+    s.Run();
+  }
+
+
+  void SQLiteDatabaseWrapper::DeleteAttachment(int64_t id,
+                                               FileContentType attachment)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM AttachedFiles WHERE id=? AND fileType=?");
+    s.BindInt64(0, id);
+    s.BindInt(1, attachment);
+    s.Run();
+  }
+
+
+  void SQLiteDatabaseWrapper::ListAvailableAttachments(std::list<FileContentType>& target,
+                                                       int64_t id)
+  {
+    target.clear();
+
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                        "SELECT fileType FROM AttachedFiles WHERE id=?");
+    s.BindInt64(0, id);
+
+    while (s.Step())
+    {
+      target.push_back(static_cast<FileContentType>(s.ColumnInt(0)));
+    }
+  }
+
+  bool SQLiteDatabaseWrapper::LookupAttachment(FileInfo& attachment,
+                                               int64_t id,
+                                               FileContentType contentType)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                        "SELECT uuid, uncompressedSize, compressionType, compressedSize, "
+                        "uncompressedMD5, compressedMD5 FROM AttachedFiles WHERE id=? AND fileType=?");
+    s.BindInt64(0, id);
+    s.BindInt(1, contentType);
+
+    if (!s.Step())
+    {
+      return false;
+    }
+    else
+    {
+      attachment = FileInfo(s.ColumnString(0),
+                            contentType,
+                            s.ColumnInt64(1),
+                            s.ColumnString(4),
+                            static_cast<CompressionType>(s.ColumnInt(2)),
+                            s.ColumnInt64(3),
+                            s.ColumnString(5));
+      return true;
+    }
+  }
+
+
+  void SQLiteDatabaseWrapper::ClearMainDicomTags(int64_t id)
+  {
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM DicomIdentifiers WHERE id=?");
+      s.BindInt64(0, id);
+      s.Run();
+    }
+
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM MainDicomTags WHERE id=?");
+      s.BindInt64(0, id);
+      s.Run();
+    }
+  }
+
+
+  void SQLiteDatabaseWrapper::SetMainDicomTag(int64_t id,
+                                              const DicomTag& tag,
+                                              const std::string& value)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO MainDicomTags VALUES(?, ?, ?, ?)");
+    s.BindInt64(0, id);
+    s.BindInt(1, tag.GetGroup());
+    s.BindInt(2, tag.GetElement());
+    s.BindString(3, value);
+    s.Run();
+  }
+
+
+  void SQLiteDatabaseWrapper::SetIdentifierTag(int64_t id,
+                                               const DicomTag& tag,
+                                               const std::string& value)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO DicomIdentifiers VALUES(?, ?, ?, ?)");
+    s.BindInt64(0, id);
+    s.BindInt(1, tag.GetGroup());
+    s.BindInt(2, tag.GetElement());
+    s.BindString(3, value);
+    s.Run();
+  }
+
+
+  void SQLiteDatabaseWrapper::GetMainDicomTags(DicomMap& map,
+                                               int64_t id)
+  {
+    map.Clear();
+
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM MainDicomTags WHERE id=?");
+    s.BindInt64(0, id);
+    while (s.Step())
+    {
+      map.SetValue(s.ColumnInt(1),
+                   s.ColumnInt(2),
+                   s.ColumnString(3), false);
+    }
+  }
+
+
+  void SQLiteDatabaseWrapper::GetChildrenPublicId(std::list<std::string>& target,
+                                                  int64_t id)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.publicId FROM Resources AS a, Resources AS b  "
+                        "WHERE a.parentId = b.internalId AND b.internalId = ?");     
+    s.BindInt64(0, id);
+
+    target.clear();
+
+    while (s.Step())
+    {
+      target.push_back(s.ColumnString(0));
+    }
+  }
+
+
+  void SQLiteDatabaseWrapper::GetChildrenInternalId(std::list<int64_t>& target,
+                                                    int64_t id)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.internalId FROM Resources AS a, Resources AS b  "
+                        "WHERE a.parentId = b.internalId AND b.internalId = ?");     
+    s.BindInt64(0, id);
+
+    target.clear();
+
+    while (s.Step())
+    {
+      target.push_back(s.ColumnInt64(0));
+    }
+  }
+
+
+  void SQLiteDatabaseWrapper::LogChange(int64_t internalId,
+                                        const ServerIndexChange& change)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Changes VALUES(NULL, ?, ?, ?, ?)");
+    s.BindInt(0, change.GetChangeType());
+    s.BindInt64(1, internalId);
+    s.BindInt(2, change.GetResourceType());
+    s.BindString(3, change.GetDate());
+    s.Run();
+  }
+
+
+  void SQLiteDatabaseWrapper::LogExportedResource(const ExportedResource& resource)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                        "INSERT INTO ExportedResources VALUES(NULL, ?, ?, ?, ?, ?, ?, ?, ?)");
+
+    s.BindInt(0, resource.GetResourceType());
+    s.BindString(1, resource.GetPublicId());
+    s.BindString(2, resource.GetModality());
+    s.BindString(3, resource.GetPatientId());
+    s.BindString(4, resource.GetStudyInstanceUid());
+    s.BindString(5, resource.GetSeriesInstanceUid());
+    s.BindString(6, resource.GetSopInstanceUid());
+    s.BindString(7, resource.GetDate());
+    s.Run();      
+  }
+
+
+  void SQLiteDatabaseWrapper::GetExportedResources(std::list<ExportedResource>& target,
+                                                   bool& done,
+                                                   int64_t since,
+                                                   uint32_t maxResults)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                        "SELECT * FROM ExportedResources WHERE seq>? ORDER BY seq LIMIT ?");
+    s.BindInt64(0, since);
+    s.BindInt(1, maxResults + 1);
+    GetExportedResourcesInternal(target, done, s, maxResults);
+  }
+
+    
+  void SQLiteDatabaseWrapper::GetLastExportedResource(std::list<ExportedResource>& target)
+  {
+    bool done;  // Ignored
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                        "SELECT * FROM ExportedResources ORDER BY seq DESC LIMIT 1");
+    GetExportedResourcesInternal(target, done, s, 1);
+  }
+
+    
+  uint64_t SQLiteDatabaseWrapper::GetTotalCompressedSize()
+  {
+    // Old SQL query that was used in Orthanc <= 1.5.0:
+    // SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(compressedSize) FROM AttachedFiles");
+
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT value FROM GlobalIntegers WHERE key=0");
+    s.Run();
+    return static_cast<uint64_t>(s.ColumnInt64(0));
+  }
+
+    
+  uint64_t SQLiteDatabaseWrapper::GetTotalUncompressedSize()
+  {
+    // Old SQL query that was used in Orthanc <= 1.5.0:
+    // SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(uncompressedSize) FROM AttachedFiles");
+
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT value FROM GlobalIntegers WHERE key=1");
+    s.Run();
+    return static_cast<uint64_t>(s.ColumnInt64(0));
+  }
+
+
+  uint64_t SQLiteDatabaseWrapper::GetResourceCount(ResourceType resourceType)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                        "SELECT COUNT(*) FROM Resources WHERE resourceType=?");
+    s.BindInt(0, resourceType);
+    
+    if (!s.Step())
+    {
+      return 0;
+    }
+    else
+    {
+      int64_t c = s.ColumnInt(0);
+      assert(!s.Step());
+      return c;
+    }
+  }
+
+
+  void SQLiteDatabaseWrapper::GetAllInternalIds(std::list<int64_t>& target,
+                                                ResourceType resourceType)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT internalId FROM Resources WHERE resourceType=?");
+    s.BindInt(0, resourceType);
+
+    target.clear();
+    while (s.Step())
+    {
+      target.push_back(s.ColumnInt64(0));
+    }
+  }
+
+
+  void SQLiteDatabaseWrapper::GetAllPublicIds(std::list<std::string>& target,
+                                              ResourceType resourceType)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Resources WHERE resourceType=?");
+    s.BindInt(0, resourceType);
+
+    target.clear();
+    while (s.Step())
+    {
+      target.push_back(s.ColumnString(0));
+    }
+  }
+
+
+  void SQLiteDatabaseWrapper::GetAllPublicIds(std::list<std::string>& target,
+                                              ResourceType resourceType,
+                                              size_t since,
+                                              size_t limit)
+  {
+    if (limit == 0)
+    {
+      target.clear();
+      return;
+    }
+
+    SQLite::Statement s(db_, SQLITE_FROM_HERE,
+                        "SELECT publicId FROM Resources WHERE "
+                        "resourceType=? LIMIT ? OFFSET ?");
+    s.BindInt(0, resourceType);
+    s.BindInt64(1, limit);
+    s.BindInt64(2, since);
+
+    target.clear();
+    while (s.Step())
+    {
+      target.push_back(s.ColumnString(0));
+    }
+  }
+
+
+  bool SQLiteDatabaseWrapper::SelectPatientToRecycle(int64_t& internalId)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE,
+                        "SELECT patientId FROM PatientRecyclingOrder ORDER BY seq ASC LIMIT 1");
+   
+    if (!s.Step())
+    {
+      // No patient remaining or all the patients are protected
+      return false;
+    }
+    else
+    {
+      internalId = s.ColumnInt(0);
+      return true;
+    }    
+  }
+
+
+  bool SQLiteDatabaseWrapper::SelectPatientToRecycle(int64_t& internalId,
+                                                     int64_t patientIdToAvoid)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE,
+                        "SELECT patientId FROM PatientRecyclingOrder "
+                        "WHERE patientId != ? ORDER BY seq ASC LIMIT 1");
+    s.BindInt64(0, patientIdToAvoid);
+
+    if (!s.Step())
+    {
+      // No patient remaining or all the patients are protected
+      return false;
+    }
+    else
+    {
+      internalId = s.ColumnInt(0);
+      return true;
+    }   
+  }
+
+
+  bool SQLiteDatabaseWrapper::IsProtectedPatient(int64_t internalId)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE,
+                        "SELECT * FROM PatientRecyclingOrder WHERE patientId = ?");
+    s.BindInt64(0, internalId);
+    return !s.Step();
+  }
+
+
+  void SQLiteDatabaseWrapper::SetProtectedPatient(int64_t internalId, 
+                                                  bool isProtected)
+  {
+    if (isProtected)
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM PatientRecyclingOrder WHERE patientId=?");
+      s.BindInt64(0, internalId);
+      s.Run();
+    }
+    else if (IsProtectedPatient(internalId))
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO PatientRecyclingOrder VALUES(NULL, ?)");
+      s.BindInt64(0, internalId);
+      s.Run();
+    }
+    else
+    {
+      // Nothing to do: The patient is already unprotected
+    }
+  }
+
+
+  bool SQLiteDatabaseWrapper::IsExistingResource(int64_t internalId)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                        "SELECT * FROM Resources WHERE internalId=?");
+    s.BindInt64(0, internalId);
+    return s.Step();
+  }
+
+
+  void SQLiteDatabaseWrapper::LookupIdentifier(std::list<int64_t>& target,
+                                               ResourceType level,
+                                               const DicomTag& tag,
+                                               IdentifierConstraintType type,
+                                               const std::string& value)
+  {
+    static const char* COMMON = ("SELECT d.id FROM DicomIdentifiers AS d, Resources AS r WHERE "
+                                 "d.id = r.internalId AND r.resourceType=? AND "
+                                 "d.tagGroup=? AND d.tagElement=? AND ");
+
+    std::auto_ptr<SQLite::Statement> s;
+
+    switch (type)
+    {
+      case IdentifierConstraintType_GreaterOrEqual:
+        s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value>=?"));
+        break;
+
+      case IdentifierConstraintType_SmallerOrEqual:
+        s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value<=?"));
+        break;
+
+      case IdentifierConstraintType_Wildcard:
+        s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value GLOB ?"));
+        break;
+
+      case IdentifierConstraintType_Equal:
+      default:
+        s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value=?"));
+        break;
+    }
+
+    assert(s.get() != NULL);
+
+    s->BindInt(0, level);
+    s->BindInt(1, tag.GetGroup());
+    s->BindInt(2, tag.GetElement());
+    s->BindString(3, value);
+
+    target.clear();
+
+    while (s->Step())
+    {
+      target.push_back(s->ColumnInt64(0));
+    }    
+  }
+
+
+  void SQLiteDatabaseWrapper::LookupIdentifierRange(std::list<int64_t>& target,
+                                                    ResourceType level,
+                                                    const DicomTag& tag,
+                                                    const std::string& start,
+                                                    const std::string& end)
+  {
+    SQLite::Statement statement(db_, SQLITE_FROM_HERE,
+                                "SELECT d.id FROM DicomIdentifiers AS d, Resources AS r WHERE "
+                                "d.id = r.internalId AND r.resourceType=? AND "
+                                "d.tagGroup=? AND d.tagElement=? AND d.value>=? AND d.value<=?");
+
+    statement.BindInt(0, level);
+    statement.BindInt(1, tag.GetGroup());
+    statement.BindInt(2, tag.GetElement());
+    statement.BindString(3, start);
+    statement.BindString(4, end);
+
+    target.clear();
+
+    while (statement.Step())
+    {
+      target.push_back(statement.ColumnInt64(0));
+    }    
+  }
+
+
+  bool SQLiteDatabaseWrapper::IsDiskSizeAbove(uint64_t threshold)
+  {
+    return GetTotalCompressedSize() > threshold;
+  }
+
+
+  static std::string FormatLevel(ResourceType level)
+  {
+    switch (level)
+    {
+      case ResourceType_Patient:
+        return "patients";
+        
+      case ResourceType_Study:
+        return "studies";
+        
+      case ResourceType_Series:
+        return "series";
+        
+      case ResourceType_Instance:
+        return "instances";
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+  }      
+  
+
+  static bool FormatComparison(std::string& target,
+                               const DatabaseConstraint& constraint,
+                               size_t index,
+                               std::vector<std::string>& parameters)
+  {
+    std::string tag = "t" + boost::lexical_cast<std::string>(index);
+
+    std::string comparison;
+    
+    switch (constraint.GetConstraintType())
+    {
+      case ConstraintType_Equal:
+      case ConstraintType_SmallerOrEqual:
+      case ConstraintType_GreaterOrEqual:
+      {
+        std::string op;
+        switch (constraint.GetConstraintType())
+        {
+          case ConstraintType_Equal:
+            op = "=";
+            break;
+          
+          case ConstraintType_SmallerOrEqual:
+            op = "<=";
+            break;
+          
+          case ConstraintType_GreaterOrEqual:
+            op = ">=";
+            break;
+          
+          default:
+            throw OrthancException(ErrorCode_InternalError);
+        }
+          
+        parameters.push_back(constraint.GetSingleValue());
+
+        if (constraint.IsCaseSensitive())
+        {
+          comparison = tag + ".value " + op + " ?";
+        }
+        else
+        {
+          comparison = "lower(" + tag + ".value) " + op + " lower(?)";
+        }
+
+        break;
+      }
+
+      case ConstraintType_List:
+      {
+        for (size_t i = 0; i < constraint.GetValuesCount(); i++)
+        {
+          parameters.push_back(constraint.GetValue(i));
+
+          if (!comparison.empty())
+          {
+            comparison += ", ";
+          }
+            
+          if (constraint.IsCaseSensitive())
+          {
+            comparison += "?";
+          }
+          else
+          {
+            comparison += "lower(?)";
+          }
+        }
+
+        if (constraint.IsCaseSensitive())
+        {
+          comparison = tag + ".value IN (" + comparison + ")";
+        }
+        else
+        {
+          comparison = "lower(" +  tag + ".value) IN (" + comparison + ")";
+        }
+            
+        break;
+      }
+
+      case ConstraintType_Wildcard:
+      {
+        const std::string value = constraint.GetSingleValue();
+
+        if (value == "*")
+        {
+          if (!constraint.IsMandatory())
+          {
+            // Universal constraint on an optional tag, ignore it
+            return false;
+          }
+        }
+        else
+        {
+          std::string escaped;
+          escaped.reserve(value.size());
+
+          for (size_t i = 0; i < value.size(); i++)
+          {
+            if (value[i] == '*')
+            {
+              escaped += "%";
+            }
+            else if (value[i] == '?')
+            {
+              escaped += "_";
+            }
+            else if (value[i] == '%')
+            {
+              escaped += "\\%";
+            }
+            else if (value[i] == '_')
+            {
+              escaped += "\\_";
+            }
+            else if (value[i] == '\\')
+            {
+              escaped += "\\\\";
+            }
+            else
+            {
+              escaped += value[i];
+            }               
+          }
+
+          parameters.push_back(escaped);
+
+          if (constraint.IsCaseSensitive())
+          {
+            comparison = tag + ".value LIKE ? ESCAPE '\\'";
+          }
+          else
+          {
+            comparison = "lower(" + tag + ".value) LIKE lower(?) ESCAPE '\\'";
+          }
+        }
+          
+        break;
+      }
+
+      default:
+        // Don't modify "parameters" in this case!
+        return false;
+    }
+
+    if (constraint.IsMandatory())
+    {
+      target = comparison;
+    }
+    else if (comparison.empty())
+    {
+      target = tag + ".value IS NULL";
+    }
+    else
+    {
+      target = tag + ".value IS NULL OR " + comparison;
+    }
+
+    return true;
+  }
+
+
+  static void FormatJoin(std::string& target,
+                         const DatabaseConstraint& constraint,
+                         size_t index)
+  {
+    std::string tag = "t" + boost::lexical_cast<std::string>(index);
+
+    if (constraint.IsMandatory())
+    {
+      target = " INNER JOIN ";
+    }
+    else
+    {
+      target = " LEFT JOIN ";
+    }
+
+    if (constraint.IsIdentifier())
+    {
+      target += "DicomIdentifiers ";
+    }
+    else
+    {
+      target += "MainDicomTags ";
+    }
+
+    target += (tag + " ON " + tag + ".id = " + FormatLevel(constraint.GetLevel()) +
+               ".internalId AND " + tag + ".tagGroup = " +
+               boost::lexical_cast<std::string>(constraint.GetTag().GetGroup()) +
+               " AND " + tag + ".tagElement = " +
+               boost::lexical_cast<std::string>(constraint.GetTag().GetElement()));
+  }
+  
+
+  static void AnswerLookup(std::vector<std::string>& resourcesId,
+                           std::vector<std::string>& instancesId,
+                           SQLite::Connection& db,
+                           ResourceType level)
+  {
+    resourcesId.clear();
+    instancesId.clear();
+    
+    std::auto_ptr<SQLite::Statement> statement;
+    
+    switch (level)
+    {
+      case ResourceType_Patient:
+      {
+        statement.reset(
+          new SQLite::Statement(
+            db, SQLITE_FROM_HERE,
+            "SELECT patients.publicId, instances.publicID FROM Lookup AS patients "
+            "INNER JOIN Resources studies ON patients.internalId=studies.parentId "
+            "INNER JOIN Resources series ON studies.internalId=series.parentId "
+            "INNER JOIN Resources instances ON series.internalId=instances.parentId "
+            "GROUP BY patients.publicId"));
+      
+        break;
+      }
+
+      case ResourceType_Study:
+      {
+        statement.reset(
+          new SQLite::Statement(
+            db, SQLITE_FROM_HERE,
+            "SELECT studies.publicId, instances.publicID FROM Lookup AS studies "
+            "INNER JOIN Resources series ON studies.internalId=series.parentId "
+            "INNER JOIN Resources instances ON series.internalId=instances.parentId "
+            "GROUP BY studies.publicId"));
+      
+        break;
+      }
+
+      case ResourceType_Series:
+      {
+        statement.reset(
+          new SQLite::Statement(
+            db, SQLITE_FROM_HERE,
+            "SELECT series.publicId, instances.publicID FROM Lookup AS series "
+            "INNER JOIN Resources instances ON series.internalId=instances.parentId "
+            "GROUP BY series.publicId"));
+      
+        break;
+      }
+
+      case ResourceType_Instance:
+      {
+        statement.reset(
+          new SQLite::Statement(
+            db, SQLITE_FROM_HERE, "SELECT publicId, publicId FROM Lookup"));
+        
+        break;
+      }
+      
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+    assert(statement.get() != NULL);
+      
+    while (statement->Step())
+    {
+      resourcesId.push_back(statement->ColumnString(0));
+      instancesId.push_back(statement->ColumnString(1));
+    }
+  }
+
+
+  void SQLiteDatabaseWrapper::ApplyLookupResources(std::vector<std::string>& resourcesId,
+                                                   std::vector<std::string>* instancesId,
+                                                   const std::vector<DatabaseConstraint>& lookup,
+                                                   ResourceType queryLevel,
+                                                   size_t limit)
+  {
+    for (size_t i = 0; i < lookup.size(); i++)
+    {
+      std::cout << i << ": " << lookup[i].GetTag() << " - " << EnumerationToString(lookup[i].GetLevel());
+      std::cout << std::endl;
+    }
+    
+    assert(ResourceType_Patient < ResourceType_Study &&
+           ResourceType_Study < ResourceType_Series &&
+           ResourceType_Series < ResourceType_Instance);
+    
+    ResourceType upperLevel = queryLevel;
+    ResourceType lowerLevel = queryLevel;
+
+    for (size_t i = 0; i < lookup.size(); i++)
+    {
+      ResourceType level = lookup[i].GetLevel();
+
+      if (level < upperLevel)
+      {
+        upperLevel = level;
+      }
+
+      if (level > lowerLevel)
+      {
+        lowerLevel = level;
+      }
+    }
+    
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "DROP TABLE IF EXISTS Lookup");
+      s.Run();
+    }
+    
+    std::string joins, comparisons;
+    std::vector<std::string> parameters;
+
+    size_t count = 0;
+    
+    for (size_t i = 0; i < lookup.size(); i++)
+    {
+      std::string comparison;
+      
+      if (FormatComparison(comparison, lookup[i], count, parameters))
+      {
+        std::string join;
+        FormatJoin(join, lookup[i], count);
+        joins += join;
+
+        if (!comparison.empty())
+        {
+          comparisons += " AND " + comparison;
+        }
+        
+        count ++;
+      }
+    }
+
+    {
+      std::string sql = ("CREATE TEMPORARY TABLE Lookup AS SELECT " +
+                         FormatLevel(queryLevel) + ".publicId, " +
+                         FormatLevel(queryLevel) + ".internalId" +
+                         " FROM Resources AS " + FormatLevel(queryLevel));
+
+      for (int level = queryLevel - 1; level >= upperLevel; level--)
+      {
+        sql += (" INNER JOIN Resources " +
+                FormatLevel(static_cast<ResourceType>(level)) + " ON " +
+                FormatLevel(static_cast<ResourceType>(level)) + ".internalId=" +
+                FormatLevel(static_cast<ResourceType>(level + 1)) + ".parentId");
+      }
+      
+      for (int level = queryLevel + 1; level <= lowerLevel; level++)
+      {
+        sql += (" INNER JOIN Resources " +
+                FormatLevel(static_cast<ResourceType>(level)) + " ON " +
+                FormatLevel(static_cast<ResourceType>(level - 1)) + ".internalId=" +
+                FormatLevel(static_cast<ResourceType>(level)) + ".parentId");
+      }
+      
+      sql += (joins + " WHERE " + FormatLevel(queryLevel) + ".resourceType = " +
+              boost::lexical_cast<std::string>(queryLevel) + comparisons);
+
+      if (limit != 0)
+      {
+        sql += " LIMIT " + boost::lexical_cast<std::string>(limit);
+      }
+
+      printf("[%s]\n", sql.c_str());
+
+      SQLite::Statement s(db_, sql);
+
+      for (size_t i = 0; i < parameters.size(); i++)
+      {
+        printf("   %lu = '%s'\n", i, parameters[i].c_str());
+        s.BindString(i, parameters[i]);
+      }
+
+      s.Run();
+    }
+
+    if (instancesId != NULL)
+    {
+      AnswerLookup(resourcesId, *instancesId, db_, queryLevel);
+    }
+    else
+    {
+      resourcesId.clear();
+    
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Lookup");
+        
+      while (s.Step())
+      {
+        resourcesId.push_back(s.ColumnString(0));
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/SQLiteDatabaseWrapper.h	Wed Dec 19 16:31:10 2018 +0100
@@ -0,0 +1,286 @@
+/**
+ * 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 General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "IDatabaseWrapper.h"
+
+#include "../Core/SQLite/Connection.h"
+
+namespace Orthanc
+{
+  namespace Internals
+  {
+    class SignalRemainingAncestor;
+  }
+
+  /**
+   * This class manages an instance of the Orthanc SQLite database. It
+   * translates low-level requests into SQL statements. Mutual
+   * exclusion MUST be implemented at a higher level.
+   **/
+  class SQLiteDatabaseWrapper : public IDatabaseWrapper
+  {
+  private:
+    class Transaction;
+
+    IDatabaseListener* listener_;
+    SQLite::Connection db_;
+    Internals::SignalRemainingAncestor* signalRemainingAncestor_;
+    unsigned int version_;
+
+    void GetChangesInternal(std::list<ServerIndexChange>& target,
+                            bool& done,
+                            SQLite::Statement& s,
+                            uint32_t maxResults);
+
+    void GetExportedResourcesInternal(std::list<ExportedResource>& target,
+                                      bool& done,
+                                      SQLite::Statement& s,
+                                      uint32_t maxResults);
+
+    void ClearTable(const std::string& tableName);
+
+  public:
+    SQLiteDatabaseWrapper(const std::string& path);
+
+    SQLiteDatabaseWrapper();
+
+    virtual void Open();
+
+    virtual void Close()
+    {
+      db_.Close();
+    }
+
+    virtual void SetListener(IDatabaseListener& listener);
+
+    virtual bool LookupParent(int64_t& parentId,
+                              int64_t resourceId);
+
+    virtual std::string GetPublicId(int64_t resourceId);
+
+    virtual ResourceType GetResourceType(int64_t resourceId);
+
+    virtual void DeleteResource(int64_t id);
+
+    virtual void GetChanges(std::list<ServerIndexChange>& target /*out*/,
+                            bool& done /*out*/,
+                            int64_t since,
+                            uint32_t maxResults);
+
+    virtual void GetLastChange(std::list<ServerIndexChange>& target /*out*/);
+
+    virtual IDatabaseWrapper::ITransaction* StartTransaction();
+
+    virtual void FlushToDisk()
+    {
+      db_.FlushToDisk();
+    }
+
+    virtual bool HasFlushToDisk() const
+    {
+      return true;
+    }
+
+    virtual void ClearChanges()
+    {
+      ClearTable("Changes");
+    }
+
+    virtual void ClearExportedResources()
+    {
+      ClearTable("ExportedResources");
+    }
+
+    virtual void GetAllMetadata(std::map<MetadataType, std::string>& target,
+                                int64_t id);
+
+    virtual unsigned int GetDatabaseVersion()
+    {
+      return version_;
+    }
+
+    virtual void Upgrade(unsigned int targetVersion,
+                         IStorageArea& storageArea);
+
+
+    /**
+     * The methods declared below are for unit testing only!
+     **/
+
+    const char* GetErrorMessage() const
+    {
+      return db_.GetErrorMessage();
+    }
+
+    void GetChildren(std::list<std::string>& childrenPublicIds,
+                     int64_t id);
+
+    int64_t GetTableRecordCount(const std::string& table);
+    
+    bool GetParentPublicId(std::string& target,
+                           int64_t id);
+
+
+
+    /**
+     * Until Orthanc 1.4.0, the methods below were part of the
+     * "DatabaseWrapperBase" class, that is now placed in the
+     * graveyard.
+     **/
+
+    virtual void SetGlobalProperty(GlobalProperty property,
+                                   const std::string& value);
+
+    virtual bool LookupGlobalProperty(std::string& target,
+                                      GlobalProperty property);
+
+    virtual int64_t CreateResource(const std::string& publicId,
+                                   ResourceType type);
+
+    virtual bool LookupResource(int64_t& id,
+                                ResourceType& type,
+                                const std::string& publicId);
+
+    virtual void AttachChild(int64_t parent,
+                             int64_t child);
+
+    virtual void SetMetadata(int64_t id,
+                             MetadataType type,
+                             const std::string& value);
+
+    virtual void DeleteMetadata(int64_t id,
+                                MetadataType type);
+
+    virtual bool LookupMetadata(std::string& target,
+                                int64_t id,
+                                MetadataType type);
+
+    virtual void ListAvailableMetadata(std::list<MetadataType>& target,
+                                       int64_t id);
+
+    virtual void AddAttachment(int64_t id,
+                               const FileInfo& attachment);
+
+    virtual void DeleteAttachment(int64_t id,
+                                  FileContentType attachment);
+
+    virtual void ListAvailableAttachments(std::list<FileContentType>& target,
+                                          int64_t id);
+
+    virtual bool LookupAttachment(FileInfo& attachment,
+                                  int64_t id,
+                                  FileContentType contentType);
+
+    virtual void ClearMainDicomTags(int64_t id);
+
+    virtual void SetMainDicomTag(int64_t id,
+                                 const DicomTag& tag,
+                                 const std::string& value);
+
+    virtual void SetIdentifierTag(int64_t id,
+                                  const DicomTag& tag,
+                                  const std::string& value);
+
+    virtual void GetMainDicomTags(DicomMap& map,
+                                  int64_t id);
+
+    virtual void GetChildrenPublicId(std::list<std::string>& target,
+                                     int64_t id);
+
+    virtual void GetChildrenInternalId(std::list<int64_t>& target,
+                                       int64_t id);
+
+    virtual void LogChange(int64_t internalId,
+                           const ServerIndexChange& change);
+
+    virtual void LogExportedResource(const ExportedResource& resource);
+    
+    virtual void GetExportedResources(std::list<ExportedResource>& target /*out*/,
+                                      bool& done /*out*/,
+                                      int64_t since,
+                                      uint32_t maxResults);
+
+    virtual void GetLastExportedResource(std::list<ExportedResource>& target /*out*/);
+
+    virtual uint64_t GetTotalCompressedSize();
+    
+    virtual uint64_t GetTotalUncompressedSize();
+
+    virtual uint64_t GetResourceCount(ResourceType resourceType);
+
+    virtual void GetAllInternalIds(std::list<int64_t>& target,
+                                   ResourceType resourceType);
+
+    virtual void GetAllPublicIds(std::list<std::string>& target,
+                                 ResourceType resourceType);
+
+    virtual void GetAllPublicIds(std::list<std::string>& target,
+                                 ResourceType resourceType,
+                                 size_t since,
+                                 size_t limit);
+
+    virtual bool SelectPatientToRecycle(int64_t& internalId);
+
+    virtual bool SelectPatientToRecycle(int64_t& internalId,
+                                        int64_t patientIdToAvoid);
+
+    virtual bool IsProtectedPatient(int64_t internalId);
+
+    virtual void SetProtectedPatient(int64_t internalId, 
+                                     bool isProtected);
+
+    virtual bool IsExistingResource(int64_t internalId);
+
+    virtual void LookupIdentifier(std::list<int64_t>& result,
+                                  ResourceType level,
+                                  const DicomTag& tag,
+                                  IdentifierConstraintType type,
+                                  const std::string& value);
+
+    virtual void LookupIdentifierRange(std::list<int64_t>& result,
+                                       ResourceType level,
+                                       const DicomTag& tag,
+                                       const std::string& start,
+                                       const std::string& end);
+
+    virtual bool IsDiskSizeAbove(uint64_t threshold);
+
+    virtual void ApplyLookupResources(std::vector<std::string>& resourcesId,
+                                      std::vector<std::string>* instancesId,
+                                      const std::vector<DatabaseConstraint>& lookup,
+                                      ResourceType queryLevel,
+                                      size_t limit);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Search/DatabaseConstraint.cpp	Wed Dec 19 16:31:10 2018 +0100
@@ -0,0 +1,108 @@
+/**
+ * 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 General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeadersServer.h"
+#include "DatabaseConstraint.h"
+
+#include "../../Core/OrthancException.h"
+#include "../ServerToolbox.h"
+
+namespace Orthanc
+{
+  DatabaseConstraint::DatabaseConstraint(const DicomTagConstraint& constraint,
+                                         ResourceType level,
+                                         DicomTagType tagType) :
+    level_(level),
+    tag_(constraint.GetTag()),
+    constraintType_(constraint.GetConstraintType()),
+    mandatory_(constraint.IsMandatory())
+  {
+    switch (tagType)
+    {
+      case DicomTagType_Identifier:
+        isIdentifier_ = true;
+        caseSensitive_ = true;
+        break;
+
+      case DicomTagType_Main:
+        isIdentifier_ = false;
+        caseSensitive_ = constraint.IsCaseSensitive();
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+    values_.reserve(constraint.GetValues().size());
+      
+    for (std::set<std::string>::const_iterator
+           it = constraint.GetValues().begin();
+         it != constraint.GetValues().end(); ++it)
+    {
+      if (isIdentifier_)
+      {
+        values_.push_back(ServerToolbox::NormalizeIdentifier(*it));
+      }
+      else
+      {
+        values_.push_back(*it);
+      }
+    }
+  }
+
+  
+  const std::string& DatabaseConstraint::GetValue(size_t index) const
+  {
+    if (index >= values_.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      return values_[index];
+    }
+  }
+
+
+  const std::string& DatabaseConstraint::GetSingleValue() const
+  {
+    if (values_.size() != 1)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return values_[0];
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Search/DatabaseConstraint.h	Wed Dec 19 16:31:10 2018 +0100
@@ -0,0 +1,95 @@
+/**
+ * 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 General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "DicomTagConstraint.h"
+
+namespace Orthanc
+{
+  class DatabaseConstraint
+  {
+  private:
+    ResourceType              level_;
+    DicomTag                  tag_;
+    bool                      isIdentifier_;
+    ConstraintType            constraintType_;
+    std::vector<std::string>  values_;
+    bool                      caseSensitive_;
+    bool                      mandatory_;
+
+  public:
+    DatabaseConstraint(const DicomTagConstraint& constraint,
+                       ResourceType level,
+                       DicomTagType tagType);
+
+    ResourceType GetLevel() const
+    {
+      return level_;
+    }
+
+    const DicomTag& GetTag() const
+    {
+      return tag_;
+    }
+
+    bool IsIdentifier() const
+    {
+      return isIdentifier_;
+    }
+
+    ConstraintType GetConstraintType() const
+    {
+      return constraintType_;
+    }
+
+    size_t GetValuesCount() const
+    {
+      return values_.size();
+    }
+
+    const std::string& GetValue(size_t index) const;
+
+    const std::string& GetSingleValue() const;
+
+    bool IsCaseSensitive() const
+    {
+      return caseSensitive_;
+    }
+
+    bool IsMandatory() const
+    {
+      return mandatory_;
+    }
+  };
+}
--- a/OrthancServer/Search/DatabaseLookup.cpp	Wed Dec 19 14:35:55 2018 +0100
+++ b/OrthancServer/Search/DatabaseLookup.cpp	Wed Dec 19 16:31:10 2018 +0100
@@ -39,50 +39,6 @@
 
 namespace Orthanc
 {
-  void DatabaseLookup::LoadTags(ResourceType level)
-  {
-    const DicomTag* tags = NULL;
-    size_t size;
-    
-    ServerToolbox::LoadIdentifiers(tags, size, level);
-    
-    for (size_t i = 0; i < size; i++)
-    {
-      if (tags_.find(tags[i]) == tags_.end())
-      {
-        tags_[tags[i]] = TagInfo(DicomTagType_Identifier, level);
-      }
-      else
-      {
-        // These patient-level tags are copied in the study level
-        assert(level == ResourceType_Study &&
-               (tags[i] == DICOM_TAG_PATIENT_ID ||
-                tags[i] == DICOM_TAG_PATIENT_NAME ||
-                tags[i] == DICOM_TAG_PATIENT_BIRTH_DATE));
-      }
-    }
-    
-    DicomMap::LoadMainDicomTags(tags, size, level);
-    
-    for (size_t i = 0; i < size; i++)
-    {
-      if (tags_.find(tags[i]) == tags_.end())
-      {
-        tags_[tags[i]] = TagInfo(DicomTagType_Main, level);
-      }
-    }
-  }
-
-
-  DatabaseLookup::DatabaseLookup()
-  {
-    LoadTags(ResourceType_Patient);
-    LoadTags(ResourceType_Study);
-    LoadTags(ResourceType_Series);
-    LoadTags(ResourceType_Instance);
-  }
-
-
   DatabaseLookup::~DatabaseLookup()
   {
     for (size_t i = 0; i < constraints_.size(); i++)
@@ -116,22 +72,11 @@
     else
     {
       constraints_.push_back(constraint);
-
-      std::map<DicomTag, TagInfo>::const_iterator tag = tags_.find(constraint->GetTag());
-
-      if (tag == tags_.end())
-      {
-        constraint->SetTagInfo(DicomTagType_Generic, ResourceType_Instance);
-      }
-      else
-      {
-        constraint->SetTagInfo(tag->second.GetType(), tag->second.GetLevel());
-      }
     }
   }
 
 
-  bool DatabaseLookup::IsMatch(const DicomMap& value)
+  bool DatabaseLookup::IsMatch(const DicomMap& value) const
   {
     for (size_t i = 0; i < constraints_.size(); i++)
     {
@@ -146,9 +91,83 @@
   }
 
 
+  void DatabaseLookup::AddDicomConstraintInternal(const DicomTag& tag,
+                                                  ValueRepresentation vr,
+                                                  const std::string& dicomQuery,
+                                                  bool caseSensitive,
+                                                  bool mandatoryTag)
+  {
+    if ((vr == ValueRepresentation_Date ||
+         vr == ValueRepresentation_DateTime ||
+         vr == ValueRepresentation_Time) &&
+        dicomQuery.find('-') != std::string::npos)
+    {
+      /**
+       * Range matching is only defined for TM, DA and DT value
+       * representations. This code fixes issues 35 and 37.
+       *
+       * Reference: "Range matching is not defined for types of
+       * Attributes other than dates and times", DICOM PS 3.4,
+       * C.2.2.2.5 ("Range Matching").
+       **/
+      size_t separator = dicomQuery.find('-');
+      std::string lower = dicomQuery.substr(0, separator);
+      std::string upper = dicomQuery.substr(separator + 1);
+
+      if (!lower.empty())
+      {
+        AddConstraint(new DicomTagConstraint
+                      (tag, ConstraintType_GreaterOrEqual, lower, caseSensitive, mandatoryTag));
+      }
+
+      if (!upper.empty())
+      {
+        AddConstraint(new DicomTagConstraint
+                      (tag, ConstraintType_SmallerOrEqual, upper, caseSensitive, mandatoryTag));
+      }
+    }
+    else if (dicomQuery.find('\\') != std::string::npos)
+    {
+      DicomTag fixedTag(tag);
+
+      if (tag == DICOM_TAG_MODALITIES_IN_STUDY)
+      {
+        // http://www.itk.org/Wiki/DICOM_QueryRetrieve_Explained
+        // http://dicomiseasy.blogspot.be/2012/01/dicom-queryretrieve-part-i.html  
+        fixedTag = DICOM_TAG_MODALITY;
+      }
+
+      std::auto_ptr<DicomTagConstraint> constraint
+        (new DicomTagConstraint(fixedTag, ConstraintType_List, caseSensitive, mandatoryTag));
+
+      std::vector<std::string> items;
+      Toolbox::TokenizeString(items, dicomQuery, '\\');
+
+      for (size_t i = 0; i < items.size(); i++)
+      {
+        constraint->AddValue(items[i]);
+      }
+
+      AddConstraint(constraint.release());
+    }
+    else if (dicomQuery.find('*') != std::string::npos ||
+             dicomQuery.find('?') != std::string::npos)
+    {
+      AddConstraint(new DicomTagConstraint
+                    (tag, ConstraintType_Wildcard, dicomQuery, caseSensitive, mandatoryTag));
+    }
+    else
+    {
+      AddConstraint(new DicomTagConstraint
+                    (tag, ConstraintType_Equal, dicomQuery, caseSensitive, mandatoryTag));
+    }
+  }
+
+
   void DatabaseLookup::AddDicomConstraint(const DicomTag& tag,
                                           const std::string& dicomQuery,
-                                          bool caseSensitivePN)
+                                          bool caseSensitivePN,
+                                          bool mandatoryTag)
   {
     ValueRepresentation vr = FromDcmtkBridge::LookupValueRepresentation(tag);
 
@@ -188,75 +207,44 @@
      * (0020,000D) UI StudyInstanceUID   => Case-sensitive
      * (0020,000E) UI SeriesInstanceUID  => Case-sensitive
      **/
-    bool caseSensitive = true;
+    
     if (vr == ValueRepresentation_PersonName)
     {
-      caseSensitive = caseSensitivePN;
-    }
-
-    if ((vr == ValueRepresentation_Date ||
-         vr == ValueRepresentation_DateTime ||
-         vr == ValueRepresentation_Time) &&
-        dicomQuery.find('-') != std::string::npos)
-    {
-      /**
-       * Range matching is only defined for TM, DA and DT value
-       * representations. This code fixes issues 35 and 37.
-       *
-       * Reference: "Range matching is not defined for types of
-       * Attributes other than dates and times", DICOM PS 3.4,
-       * C.2.2.2.5 ("Range Matching").
-       **/
-      size_t separator = dicomQuery.find('-');
-      std::string lower = dicomQuery.substr(0, separator);
-      std::string upper = dicomQuery.substr(separator + 1);
-
-      if (!lower.empty())
-      {
-        AddConstraint(new DicomTagConstraint
-                      (tag, ConstraintType_GreaterOrEqual, lower, caseSensitive));
-      }
-
-      if (!upper.empty())
-      {
-        AddConstraint(new DicomTagConstraint
-                      (tag, ConstraintType_SmallerOrEqual, upper, caseSensitive));
-      }
-    }
-    else if (dicomQuery.find('\\') != std::string::npos)
-    {
-      DicomTag fixedTag(tag);
-
-      if (tag == DICOM_TAG_MODALITIES_IN_STUDY)
-      {
-        // http://www.itk.org/Wiki/DICOM_QueryRetrieve_Explained
-        // http://dicomiseasy.blogspot.be/2012/01/dicom-queryretrieve-part-i.html  
-        fixedTag = DICOM_TAG_MODALITY;
-      }
-
-      std::auto_ptr<DicomTagConstraint> constraint
-        (new DicomTagConstraint(fixedTag, ConstraintType_List, caseSensitive));
-
-      std::vector<std::string> items;
-      Toolbox::TokenizeString(items, dicomQuery, '\\');
-
-      for (size_t i = 0; i < items.size(); i++)
-      {
-        constraint->AddValue(items[i]);
-      }
-
-      AddConstraint(constraint.release());
-    }
-    else if (dicomQuery.find('*') != std::string::npos ||
-             dicomQuery.find('?') != std::string::npos)
-    {
-      AddConstraint(new DicomTagConstraint
-                    (tag, ConstraintType_Wildcard, dicomQuery, caseSensitive));
+      AddDicomConstraintInternal(tag, vr, dicomQuery, caseSensitivePN, mandatoryTag);
     }
     else
     {
-      AddConstraint(new DicomTagConstraint
-                    (tag, ConstraintType_Equal, dicomQuery, caseSensitive));
+      AddDicomConstraintInternal(tag, vr, dicomQuery, true /* case sensitive */, mandatoryTag);
     }
   }
+
+
+  void DatabaseLookup::AddRestConstraint(const DicomTag& tag,
+                                         const std::string& dicomQuery,
+                                         bool caseSensitive,
+                                         bool mandatoryTag)
+  {
+    AddDicomConstraintInternal(tag, FromDcmtkBridge::LookupValueRepresentation(tag),
+                               dicomQuery, caseSensitive, mandatoryTag);
+  }
+
+
+  bool DatabaseLookup::HasOnlyMainDicomTags() const
+  {
+    std::set<DicomTag> mainTags;
+    DicomMap::GetMainDicomTags(mainTags);
+
+    for (size_t i = 0; i < constraints_.size(); i++)
+    {
+      assert(constraints_[i] != NULL);
+      
+      if (mainTags.find(constraints_[i]->GetTag()) == mainTags.end())
+      {
+        // This is not a main DICOM tag
+        return false;
+      }
+    }
+
+    return true;
+  }
 }
--- a/OrthancServer/Search/DatabaseLookup.h	Wed Dec 19 14:35:55 2018 +0100
+++ b/OrthancServer/Search/DatabaseLookup.h	Wed Dec 19 16:31:10 2018 +0100
@@ -40,44 +40,17 @@
   class DatabaseLookup : public boost::noncopyable
   {
   private:
-    class TagInfo
-    {
-    private:
-      DicomTagType  type_;
-      ResourceType  level_;
-
-    public:
-      TagInfo() :
-        type_(DicomTagType_Generic),
-        level_(ResourceType_Instance)
-      {
-      }
+    std::vector<DicomTagConstraint*>  constraints_;
 
-      TagInfo(DicomTagType type,
-              ResourceType level) :
-        type_(type),
-        level_(level)
-      {
-      }
-
-      DicomTagType GetType() const
-      {
-        return type_;
-      }
-
-      ResourceType GetLevel() const
-      {
-        return level_;
-      }
-    };
-
-    std::vector<DicomTagConstraint*>  constraints_;
-    std::map<DicomTag, TagInfo>       tags_;
-
-    void LoadTags(ResourceType level);
-
+    void AddDicomConstraintInternal(const DicomTag& tag,
+                                    ValueRepresentation vr,
+                                    const std::string& dicomQuery,
+                                    bool caseSensitive,
+                                    bool mandatoryTag);
   public:
-    DatabaseLookup();
+    DatabaseLookup()
+    {
+    }
 
     ~DatabaseLookup();
 
@@ -95,10 +68,18 @@
 
     void AddConstraint(DicomTagConstraint* constraint);  // Takes ownership
 
-    bool IsMatch(const DicomMap& value);
+    bool IsMatch(const DicomMap& value) const;
 
     void AddDicomConstraint(const DicomTag& tag,
                             const std::string& dicomQuery,
-                            bool caseSensitivePN);
+                            bool caseSensitivePN,
+                            bool mandatoryTag);
+
+    void AddRestConstraint(const DicomTag& tag,
+                           const std::string& dicomQuery,
+                           bool caseSensitive,
+                           bool mandatoryTag);
+
+    bool HasOnlyMainDicomTags() const;
   };
 }
--- a/OrthancServer/Search/DicomTagConstraint.cpp	Wed Dec 19 14:35:55 2018 +0100
+++ b/OrthancServer/Search/DicomTagConstraint.cpp	Wed Dec 19 16:31:10 2018 +0100
@@ -97,13 +97,12 @@
   DicomTagConstraint::DicomTagConstraint(const DicomTag& tag,
                                          ConstraintType type,
                                          const std::string& value,
-                                         bool caseSensitive) :
-    hasTagInfo_(false),
-    tagType_(DicomTagType_Generic),  // Dummy initialization
-    level_(ResourceType_Patient),    // Dummy initialization
+                                         bool caseSensitive,
+                                         bool mandatory) :
     tag_(tag),
     constraintType_(type),
-    caseSensitive_(caseSensitive)
+    caseSensitive_(caseSensitive),
+    mandatory_(mandatory)
   {
     if (type == ConstraintType_Equal ||
         type == ConstraintType_SmallerOrEqual ||
@@ -128,13 +127,12 @@
 
   DicomTagConstraint::DicomTagConstraint(const DicomTag& tag,
                                          ConstraintType type,
-                                         bool caseSensitive) :
-    hasTagInfo_(false),
-    tagType_(DicomTagType_Generic),  // Dummy initialization
-    level_(ResourceType_Patient),    // Dummy initialization
+                                         bool caseSensitive,
+                                         bool mandatory) :
     tag_(tag),
     constraintType_(type),
-    caseSensitive_(caseSensitive)
+    caseSensitive_(caseSensitive),
+    mandatory_(mandatory)
   {
     if (type != ConstraintType_List)
     {
@@ -143,41 +141,6 @@
   }
 
 
-  void DicomTagConstraint::SetTagInfo(DicomTagType tagType,
-                                      ResourceType level)
-  {
-    hasTagInfo_ = true;
-    tagType_ = tagType;
-    level_ = level;
-  }
-
-
-  DicomTagType DicomTagConstraint::GetTagType() const
-  {
-    if (!hasTagInfo_)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      return tagType_;
-    }
-  }
-
-
-  const ResourceType DicomTagConstraint::GetLevel() const
-  {
-    if (!hasTagInfo_)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      return level_;
-    }
-  }
-
-
   void DicomTagConstraint::AddValue(const std::string& value)
   {
     if (constraintType_ != ConstraintType_List)
@@ -268,8 +231,18 @@
     const DicomValue* tmp = value.TestAndGetValue(tag_);
 
     if (tmp == NULL ||
-        tmp->IsNull() ||
-        tmp->IsBinary())
+        tmp->IsNull())
+    {
+      if (mandatory_)
+      {
+        return false;
+      }
+      else
+      {
+        return true;
+      }
+    }
+    else if (tmp->IsBinary())
     {
       return false;
     }
--- a/OrthancServer/Search/DicomTagConstraint.h	Wed Dec 19 14:35:55 2018 +0100
+++ b/OrthancServer/Search/DicomTagConstraint.h	Wed Dec 19 16:31:10 2018 +0100
@@ -46,13 +46,11 @@
     class NormalizedString;
     class RegularExpression;
 
-    bool                    hasTagInfo_;
-    DicomTagType            tagType_;
-    ResourceType            level_;
     DicomTag                tag_;
     ConstraintType          constraintType_;
     std::set<std::string>   values_;
     bool                    caseSensitive_;
+    bool                    mandatory_;
 
     boost::shared_ptr<RegularExpression>  regex_;
 
@@ -60,23 +58,14 @@
     DicomTagConstraint(const DicomTag& tag,
                        ConstraintType type,
                        const std::string& value,
-                       bool caseSensitive);
+                       bool caseSensitive,
+                       bool mandatory);
 
+    // For list search
     DicomTagConstraint(const DicomTag& tag,
                        ConstraintType type,
-                       bool caseSensitive);
-
-    bool HasTagInfo() const
-    {
-      return hasTagInfo_;
-    }
-
-    void SetTagInfo(DicomTagType tagType,
-                    ResourceType level);
-
-    DicomTagType GetTagType() const;
-
-    const ResourceType GetLevel() const;
+                       bool caseSensitive,
+                       bool mandatory);
 
     const DicomTag& GetTag() const
     {
@@ -93,6 +82,16 @@
       return caseSensitive_;
     }
 
+    void SetCaseSensitive(bool caseSensitive)
+    {
+      caseSensitive_ = caseSensitive;
+    }
+
+    bool IsMandatory() const
+    {
+      return mandatory_;
+    }
+
     void AddValue(const std::string& value);
 
     const std::string& GetValue() const;
--- a/OrthancServer/Search/IFindConstraint.cpp	Wed Dec 19 14:35:55 2018 +0100
+++ b/OrthancServer/Search/IFindConstraint.cpp	Wed Dec 19 16:31:10 2018 +0100
@@ -41,6 +41,7 @@
 
 #include "../../Core/DicomParsing/FromDcmtkBridge.h"
 #include "../../Core/OrthancException.h"
+#include "../../Core/Toolbox.h"
 
 namespace Orthanc
 {
--- a/OrthancServer/Search/IFindConstraint.h	Wed Dec 19 14:35:55 2018 +0100
+++ b/OrthancServer/Search/IFindConstraint.h	Wed Dec 19 16:31:10 2018 +0100
@@ -33,7 +33,9 @@
 
 #pragma once
 
-#include "LookupIdentifierQuery.h"
+#include "../../Core/DicomFormat/DicomTag.h"
+
+#include <boost/noncopyable.hpp>
 
 namespace Orthanc
 {
@@ -46,9 +48,6 @@
 
     virtual IFindConstraint* Clone() const = 0;
 
-    virtual void Setup(LookupIdentifierQuery& lookup,
-                       const DicomTag& tag) const = 0;
-
     virtual bool Match(const std::string& value) const = 0;
 
     virtual std::string Format() const = 0;
--- a/OrthancServer/Search/ListConstraint.cpp	Wed Dec 19 14:35:55 2018 +0100
+++ b/OrthancServer/Search/ListConstraint.cpp	Wed Dec 19 16:31:10 2018 +0100
@@ -34,6 +34,7 @@
 #include "../PrecompiledHeadersServer.h"
 #include "ListConstraint.h"
 
+#include "../../Core/Toolbox.h"
 
 namespace Orthanc
 {
@@ -50,19 +51,6 @@
   }
 
 
-  void ListConstraint::Setup(LookupIdentifierQuery& lookup, 
-                             const DicomTag& tag) const
-  {
-    LookupIdentifierQuery::Disjunction& target = lookup.AddDisjunction();
-
-    for (std::set<std::string>::const_iterator
-           it = allowedValues_.begin(); it != allowedValues_.end(); ++it)
-    {
-      target.Add(tag, IdentifierConstraintType_Equal, *it);
-    }
-  }
-
-
   bool ListConstraint::Match(const std::string& value) const
   {
     std::string s;
--- a/OrthancServer/Search/ListConstraint.h	Wed Dec 19 14:35:55 2018 +0100
+++ b/OrthancServer/Search/ListConstraint.h	Wed Dec 19 16:31:10 2018 +0100
@@ -64,9 +64,6 @@
       return new ListConstraint(*this);
     }
 
-    virtual void Setup(LookupIdentifierQuery& lookup,
-                       const DicomTag& tag) const;
-
     virtual bool Match(const std::string& value) const;
 
     virtual std::string Format() const;
--- a/OrthancServer/Search/LookupIdentifierQuery.cpp	Wed Dec 19 14:35:55 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,215 +0,0 @@
-/**
- * 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "../PrecompiledHeadersServer.h"
-#include "LookupIdentifierQuery.h"
-
-#include "../../Core/DicomParsing/FromDcmtkBridge.h"
-#include "../../Core/OrthancException.h"
-#include "../ServerToolbox.h"
-#include "SetOfResources.h"
-
-#include <cassert>
-
-
-
-namespace Orthanc
-{
-  LookupIdentifierQuery::SingleConstraint::
-  SingleConstraint(const DicomTag& tag,
-                   IdentifierConstraintType type,
-                   const std::string& value) : 
-    tag_(tag),
-    type_(type),
-    value_(ServerToolbox::NormalizeIdentifier(value))
-  {
-  }
-
-
-  LookupIdentifierQuery::RangeConstraint::
-  RangeConstraint(const DicomTag& tag,
-                  const std::string& start,
-                  const std::string& end) : 
-    tag_(tag),
-    start_(ServerToolbox::NormalizeIdentifier(start)),
-    end_(ServerToolbox::NormalizeIdentifier(end))
-  {
-  }
-
-
-  LookupIdentifierQuery::Disjunction::~Disjunction()
-  {
-    for (size_t i = 0; i < singleConstraints_.size(); i++)
-    {
-      delete singleConstraints_[i];
-    }
-
-    for (size_t i = 0; i < rangeConstraints_.size(); i++)
-    {
-      delete rangeConstraints_[i];
-    }
-  }
-
-
-  void LookupIdentifierQuery::Disjunction::Add(const DicomTag& tag,
-                                               IdentifierConstraintType type,
-                                               const std::string& value)
-  {
-    singleConstraints_.push_back(new SingleConstraint(tag, type, value));
-  }
-
-
-  void LookupIdentifierQuery::Disjunction::AddRange(const DicomTag& tag,
-                                                    const std::string& start,
-                                                    const std::string& end)
-  {
-    rangeConstraints_.push_back(new RangeConstraint(tag, start, end));
-  }
-
-
-  LookupIdentifierQuery::~LookupIdentifierQuery()
-  {
-    for (Disjunctions::iterator it = disjunctions_.begin();
-         it != disjunctions_.end(); ++it)
-    {
-      delete *it;
-    }
-  }
-
-
-  bool LookupIdentifierQuery::IsIdentifier(const DicomTag& tag)
-  {
-    return ServerToolbox::IsIdentifier(tag, level_);
-  }
-
-
-  void LookupIdentifierQuery::AddConstraint(DicomTag tag,
-                                            IdentifierConstraintType type,
-                                            const std::string& value)
-  {
-    assert(IsIdentifier(tag));
-    disjunctions_.push_back(new Disjunction);
-    disjunctions_.back()->Add(tag, type, value);
-  }
-
-
-  void LookupIdentifierQuery::AddRange(DicomTag tag,
-                                       const std::string& start,
-                                       const std::string& end)
-  {
-    assert(IsIdentifier(tag));
-    disjunctions_.push_back(new Disjunction);
-    disjunctions_.back()->AddRange(tag, start, end);
-  }
-
-
-  LookupIdentifierQuery::Disjunction& LookupIdentifierQuery::AddDisjunction()
-  {
-    disjunctions_.push_back(new Disjunction);
-    return *disjunctions_.back();
-  }
-
-
-  void LookupIdentifierQuery::Apply(std::list<std::string>& result,
-                                    IDatabaseWrapper& database)
-  {
-    SetOfResources resources(database, level_);
-    Apply(resources, database);
-
-    resources.Flatten(result);
-  }
-
-
-  void LookupIdentifierQuery::Apply(SetOfResources& result,
-                                    IDatabaseWrapper& database)
-  {
-    for (size_t i = 0; i < disjunctions_.size(); i++)
-    {
-      std::list<int64_t> a;
-
-      for (size_t j = 0; j < disjunctions_[i]->GetSingleConstraintsCount(); j++)
-      {
-        const SingleConstraint& constraint = disjunctions_[i]->GetSingleConstraint(j);
-        std::list<int64_t> b;
-        database.LookupIdentifier(b, level_, constraint.GetTag(), 
-                                  constraint.GetType(), constraint.GetValue());
-
-        a.splice(a.end(), b);
-      }
-
-      for (size_t j = 0; j < disjunctions_[i]->GetRangeConstraintsCount(); j++)
-      {
-        const RangeConstraint& constraint = disjunctions_[i]->GetRangeConstraint(j);
-        std::list<int64_t> b;
-        database.LookupIdentifierRange(b, level_, constraint.GetTag(), 
-                                       constraint.GetStart(), constraint.GetEnd());
-
-        a.splice(a.end(), b);
-      }
-
-      result.Intersect(a);
-    }
-  }
-
-
-  void LookupIdentifierQuery::Print(std::ostream& s) const
-  {
-    s << "Constraint: " << std::endl;
-    for (Disjunctions::const_iterator
-           it = disjunctions_.begin(); it != disjunctions_.end(); ++it)
-    {
-      if (it == disjunctions_.begin())
-        s << "   ";
-      else
-        s << "OR ";
-
-      for (size_t j = 0; j < (*it)->GetSingleConstraintsCount(); j++)
-      {
-        const SingleConstraint& c = (*it)->GetSingleConstraint(j);
-        s << FromDcmtkBridge::GetTagName(c.GetTag(), "");
-
-        switch (c.GetType())
-        {
-          case IdentifierConstraintType_Equal: s << " == "; break;
-          case IdentifierConstraintType_SmallerOrEqual: s << " <= "; break;
-          case IdentifierConstraintType_GreaterOrEqual: s << " >= "; break;
-          case IdentifierConstraintType_Wildcard: s << " ~= "; break;
-          default:
-            s << " ? ";
-        }
-
-        s << c.GetValue() << std::endl;
-      }
-    }
-  }
-}
--- a/OrthancServer/Search/LookupIdentifierQuery.h	Wed Dec 19 14:35:55 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,207 +0,0 @@
-/**
- * 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "../IDatabaseWrapper.h"
-
-#include "SetOfResources.h"
-
-#include <vector>
-#include <boost/noncopyable.hpp>
-
-namespace Orthanc
-{
-  /**
-   * Primitive for wildcard matching, as defined in DICOM:
-   * http://dicom.nema.org/dicom/2013/output/chtml/part04/sect_C.2.html#sect_C.2.2.2.4
-   * 
-   * "Any occurrence of an "*" or a "?", then "*" shall match any
-   * sequence of characters (including a zero length value) and "?"
-   * shall match any single character. This matching is case
-   * sensitive, except for Attributes with an PN Value
-   * Representation (e.g., Patient Name (0010,0010))."
-   * 
-   * Pay attention to the fact that "*" (resp. "?") generally
-   * corresponds to "%" (resp. "_") in primitive LIKE of SQL. The
-   * values "%", "_", "\" should in the user request should
-   * respectively be escaped as "\%", "\_" and "\\".
-   *
-   * This matching must be case sensitive: The special case of PN VR
-   * is taken into consideration by normalizing the query string in
-   * method "NormalizeIdentifier()".
-   **/
-
-  class LookupIdentifierQuery : public boost::noncopyable
-  {
-    // This class encodes a conjunction ("AND") of disjunctions. Each
-    // disjunction represents an "OR" of several constraints.
-
-  public:
-    class SingleConstraint
-    {
-    private:
-      DicomTag                  tag_;
-      IdentifierConstraintType  type_;
-      std::string               value_;
-
-    public:
-      SingleConstraint(const DicomTag& tag,
-                       IdentifierConstraintType type,
-                       const std::string& value);
-
-      const DicomTag& GetTag() const
-      {
-        return tag_;
-      }
-
-      IdentifierConstraintType GetType() const
-      {
-        return type_;
-      }
-      
-      const std::string& GetValue() const
-      {
-        return value_;
-      }
-    };
-
-
-    class RangeConstraint
-    {
-    private:
-      DicomTag     tag_;
-      std::string  start_;
-      std::string  end_;
-
-    public:
-      RangeConstraint(const DicomTag& tag,
-                      const std::string& start,
-                      const std::string& end);
-
-      const DicomTag& GetTag() const
-      {
-        return tag_;
-      }
-
-      const std::string& GetStart() const
-      {
-        return start_;
-      }
-
-      const std::string& GetEnd() const
-      {
-        return end_;
-      }
-    };
-
-
-    class Disjunction : public boost::noncopyable
-    {
-    private:
-      std::vector<SingleConstraint*>  singleConstraints_;
-      std::vector<RangeConstraint*>   rangeConstraints_;
-
-    public:
-      ~Disjunction();
-
-      void Add(const DicomTag& tag,
-               IdentifierConstraintType type,
-               const std::string& value);
-
-      void AddRange(const DicomTag& tag,
-                    const std::string& start,
-                    const std::string& end);
-
-      size_t GetSingleConstraintsCount() const
-      {
-        return singleConstraints_.size();
-      }
-
-      const SingleConstraint&  GetSingleConstraint(size_t i) const
-      {
-        return *singleConstraints_[i];
-      }
-
-      size_t GetRangeConstraintsCount() const
-      {
-        return rangeConstraints_.size();
-      }
-
-      const RangeConstraint&  GetRangeConstraint(size_t i) const
-      {
-        return *rangeConstraints_[i];
-      }
-    };
-
-
-  private:
-    typedef std::vector<Disjunction*>  Disjunctions;
-
-    ResourceType  level_;
-    Disjunctions  disjunctions_;
-
-  public:
-    LookupIdentifierQuery(ResourceType level) : level_(level)
-    {
-    }
-
-    ~LookupIdentifierQuery();
-
-    bool IsIdentifier(const DicomTag& tag);
-
-    void AddConstraint(DicomTag tag,
-                       IdentifierConstraintType type,
-                       const std::string& value);
-
-    void AddRange(DicomTag tag,
-                  const std::string& start,
-                  const std::string& end);
-
-    Disjunction& AddDisjunction();
-
-    ResourceType GetLevel() const
-    {
-      return level_;
-    }
-
-    // The database must be locked
-    void Apply(std::list<std::string>& result,
-               IDatabaseWrapper& database);
-
-    void Apply(SetOfResources& result,
-               IDatabaseWrapper& database);
-
-    void Print(std::ostream& s) const;
-  };
-}
--- a/OrthancServer/Search/LookupResource.cpp	Wed Dec 19 14:35:55 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,479 +0,0 @@
-/**
- * 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "../PrecompiledHeadersServer.h"
-#include "LookupResource.h"
-
-#include "../../Core/OrthancException.h"
-#include "../../Core/FileStorage/StorageAccessor.h"
-#include "../ServerToolbox.h"
-#include "../../Core/DicomParsing/FromDcmtkBridge.h"
-
-
-namespace Orthanc
-{
-  static bool DoesDicomMapMatch(const DicomMap& dicom,
-                                const DicomTag& tag,
-                                const IFindConstraint& constraint)
-  {
-    const DicomValue* value = dicom.TestAndGetValue(tag);
-
-    return (value != NULL &&
-            !value->IsNull() &&
-            !value->IsBinary() &&
-            constraint.Match(value->GetContent()));
-  }
-
-  
-  LookupResource::Level::Level(ResourceType level) : level_(level)
-  {
-    const DicomTag* tags = NULL;
-    size_t size;
-    
-    ServerToolbox::LoadIdentifiers(tags, size, level);
-    
-    for (size_t i = 0; i < size; i++)
-    {
-      identifiers_.insert(tags[i]);
-    }
-    
-    DicomMap::LoadMainDicomTags(tags, size, level);
-    
-    for (size_t i = 0; i < size; i++)
-    {
-      if (identifiers_.find(tags[i]) == identifiers_.end())
-      {
-        mainTags_.insert(tags[i]);
-      }
-    }    
-  }
-
-  LookupResource::Level::~Level()
-  {
-    for (Constraints::iterator it = mainTagsConstraints_.begin();
-         it != mainTagsConstraints_.end(); ++it)
-    {
-      delete it->second;
-    }
-
-    for (Constraints::iterator it = identifiersConstraints_.begin();
-         it != identifiersConstraints_.end(); ++it)
-    {
-      delete it->second;
-    }
-  }
-
-  bool LookupResource::Level::Add(const DicomTag& tag,
-                                  std::auto_ptr<IFindConstraint>& constraint)
-  {
-    if (identifiers_.find(tag) != identifiers_.end())
-    {
-      if (level_ == ResourceType_Patient)
-      {
-        // The filters on the patient level must be cloned to the study level
-        identifiersConstraints_[tag] = constraint->Clone();
-      }
-      else
-      {
-        identifiersConstraints_[tag] = constraint.release();
-      }
-
-      return true;
-    }
-    else if (mainTags_.find(tag) != mainTags_.end())
-    {
-      if (level_ == ResourceType_Patient)
-      {
-        // The filters on the patient level must be cloned to the study level
-        mainTagsConstraints_[tag] = constraint->Clone();
-      }
-      else
-      {
-        mainTagsConstraints_[tag] = constraint.release();
-      }
-
-      return true;
-    }
-    else
-    {
-      // This is not a main DICOM tag
-      return false;
-    }
-  }
-
-
-  bool LookupResource::Level::IsMatch(const DicomMap& dicom) const
-  {
-    for (Constraints::const_iterator it = identifiersConstraints_.begin();
-         it != identifiersConstraints_.end(); ++it)
-    {
-      assert(it->second != NULL);
-
-      if (!DoesDicomMapMatch(dicom, it->first, *it->second))
-      {
-        return false;
-      }
-    }
-
-    for (Constraints::const_iterator it = mainTagsConstraints_.begin();
-         it != mainTagsConstraints_.end(); ++it)
-    {
-      assert(it->second != NULL);
-
-      if (!DoesDicomMapMatch(dicom, it->first, *it->second))
-      {
-        return false;
-      }
-    }
-
-    return true;
-  }
-  
-
-  LookupResource::LookupResource(ResourceType level) : level_(level)
-  {
-    switch (level)
-    {
-      case ResourceType_Patient:
-        levels_[ResourceType_Patient] = new Level(ResourceType_Patient);
-        break;
-
-      case ResourceType_Instance:
-        levels_[ResourceType_Instance] = new Level(ResourceType_Instance);
-        // Do not add "break" here
-
-      case ResourceType_Series:
-        levels_[ResourceType_Series] = new Level(ResourceType_Series);
-        // Do not add "break" here
-
-      case ResourceType_Study:
-        levels_[ResourceType_Study] = new Level(ResourceType_Study);
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-    }
-  }
-
-
-  LookupResource::~LookupResource()
-  {
-    for (Levels::iterator it = levels_.begin();
-         it != levels_.end(); ++it)
-    {
-      delete it->second;
-    }
-
-    for (Constraints::iterator it = unoptimizedConstraints_.begin();
-         it != unoptimizedConstraints_.end(); ++it)
-    {
-      delete it->second;
-    }    
-  }
-
-
-
-  bool LookupResource::AddInternal(ResourceType level,
-                                   const DicomTag& tag,
-                                   std::auto_ptr<IFindConstraint>& constraint)
-  {
-    Levels::iterator it = levels_.find(level);
-    if (it != levels_.end())
-    {
-      if (it->second->Add(tag, constraint))
-      {
-        return true;
-      }
-    }
-
-    return false;
-  }
-
-
-  void LookupResource::Add(const DicomTag& tag,
-                           IFindConstraint* constraint)
-  {
-    std::auto_ptr<IFindConstraint> c(constraint);
-
-    if (!AddInternal(ResourceType_Patient, tag, c) &&
-        !AddInternal(ResourceType_Study, tag, c) &&
-        !AddInternal(ResourceType_Series, tag, c) &&
-        !AddInternal(ResourceType_Instance, tag, c))
-    {
-      unoptimizedConstraints_[tag] = c.release();
-    }
-  }
-
-
-  static bool Match(const DicomMap& tags,
-                    const DicomTag& tag,
-                    const IFindConstraint& constraint)
-  {
-    const DicomValue* value = tags.TestAndGetValue(tag);
-
-    if (value == NULL ||
-        value->IsNull() ||
-        value->IsBinary())
-    {
-      return false;
-    }
-    else
-    {
-      return constraint.Match(value->GetContent());
-    }
-  }
-
-
-  void LookupResource::Level::Apply(SetOfResources& candidates,
-                                    IDatabaseWrapper& database) const
-  {
-    // First, use the indexed identifiers
-    LookupIdentifierQuery query(level_);
-
-    for (Constraints::const_iterator it = identifiersConstraints_.begin(); 
-         it != identifiersConstraints_.end(); ++it)
-    {
-      it->second->Setup(query, it->first);
-    }
-
-    query.Apply(candidates, database);
-
-    /*{
-      query.Print(std::cout);
-      std::list<int64_t>  source;
-      candidates.Flatten(source);
-      printf("=> %d\n", source.size());
-      }*/
-
-    // Secondly, filter using the main DICOM tags
-    if (!identifiersConstraints_.empty() ||
-        !mainTagsConstraints_.empty())
-    {
-      std::list<int64_t>  source;
-      candidates.Flatten(source);
-      candidates.Clear();
-
-      std::list<int64_t>  filtered;
-      for (std::list<int64_t>::const_iterator candidate = source.begin(); 
-           candidate != source.end(); ++candidate)
-      {
-        DicomMap tags;
-        database.GetMainDicomTags(tags, *candidate);
-
-        bool match = true;
-
-        // Re-apply the identifier constraints, as their "Setup"
-        // method is less restrictive than their "Match" method
-        for (Constraints::const_iterator it = identifiersConstraints_.begin(); 
-             match && it != identifiersConstraints_.end(); ++it)
-        {
-          if (!Match(tags, it->first, *it->second))
-          {
-            match = false;
-          }
-        }
-
-        for (Constraints::const_iterator it = mainTagsConstraints_.begin(); 
-             match && it != mainTagsConstraints_.end(); ++it)
-        {
-          if (!Match(tags, it->first, *it->second))
-          {
-            match = false;
-          }
-        }
-
-        if (match)
-        {
-          filtered.push_back(*candidate);
-        }
-      }
-      
-      candidates.Intersect(filtered);
-    }
-  }
-
-
-
-  bool LookupResource::IsMatch(const DicomMap& dicom) const
-  {
-    for (Levels::const_iterator it = levels_.begin(); it != levels_.end(); ++it)
-    {
-      if (!it->second->IsMatch(dicom))
-      {
-        return false;
-      }
-    }
-
-    for (Constraints::const_iterator it = unoptimizedConstraints_.begin(); 
-         it != unoptimizedConstraints_.end(); ++it)
-    {
-      assert(it->second != NULL);
-
-      if (!DoesDicomMapMatch(dicom, it->first, *it->second))
-      {
-        return false;
-      }
-    }
-
-    return true;
-  }
-
-
-  void LookupResource::ApplyLevel(SetOfResources& candidates,
-                                  ResourceType level,
-                                  IDatabaseWrapper& database) const
-  {
-    Levels::const_iterator it = levels_.find(level);
-    if (it != levels_.end())
-    {
-      it->second->Apply(candidates, database);
-    }
-
-    if (level == ResourceType_Study &&
-        modalitiesInStudy_.get() != NULL)
-    {
-      // There is a constraint on the "ModalitiesInStudy" DICOM
-      // extension. Check out whether one child series has one of the
-      // allowed modalities
-      std::list<int64_t> allStudies, matchingStudies;
-      candidates.Flatten(allStudies);
- 
-      for (std::list<int64_t>::const_iterator
-             study = allStudies.begin(); study != allStudies.end(); ++study)
-      {
-        std::list<int64_t> childrenSeries;
-        database.GetChildrenInternalId(childrenSeries, *study);
-
-        for (std::list<int64_t>::const_iterator
-               series = childrenSeries.begin(); series != childrenSeries.end(); ++series)
-        {
-          DicomMap tags;
-          database.GetMainDicomTags(tags, *series);
-
-          const DicomValue* value = tags.TestAndGetValue(DICOM_TAG_MODALITY);
-          if (value != NULL &&
-              !value->IsNull() &&
-              !value->IsBinary())
-          {
-            if (modalitiesInStudy_->Match(value->GetContent()))
-            {
-              matchingStudies.push_back(*study);
-              break;
-            }
-          }
-        }
-      }
-
-      candidates.Intersect(matchingStudies);
-    }
-  }
-
-
-  void LookupResource::FindCandidates(std::list<int64_t>& result,
-                                      IDatabaseWrapper& database) const
-  {
-    ResourceType startingLevel;
-    if (level_ == ResourceType_Patient)
-    {
-      startingLevel = ResourceType_Patient;
-    }
-    else
-    {
-      startingLevel = ResourceType_Study;
-    }
-
-    SetOfResources candidates(database, startingLevel);
-
-    switch (level_)
-    {
-      case ResourceType_Patient:
-        ApplyLevel(candidates, ResourceType_Patient, database);
-        break;
-
-      case ResourceType_Study:
-        ApplyLevel(candidates, ResourceType_Study, database);
-        break;
-
-      case ResourceType_Series:
-        ApplyLevel(candidates, ResourceType_Study, database);
-        candidates.GoDown();
-        ApplyLevel(candidates, ResourceType_Series, database);
-        break;
-
-      case ResourceType_Instance:
-        ApplyLevel(candidates, ResourceType_Study, database);
-        candidates.GoDown();
-        ApplyLevel(candidates, ResourceType_Series, database);
-        candidates.GoDown();
-        ApplyLevel(candidates, ResourceType_Instance, database);
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-    }
-
-    candidates.Flatten(result);
-  }
-
-
-  void LookupResource::SetModalitiesInStudy(const std::string& modalities)
-  {
-    modalitiesInStudy_.reset(new ListConstraint(true /* case sensitive */));
-    
-    std::vector<std::string> items;
-    Toolbox::TokenizeString(items, modalities, '\\');
-    
-    for (size_t i = 0; i < items.size(); i++)
-    {
-      modalitiesInStudy_->AddAllowedValue(items[i]);
-    }
-  }
-
-
-  void LookupResource::AddDicomConstraint(const DicomTag& tag,
-                                          const std::string& dicomQuery,
-                                          bool caseSensitive)
-  {
-    // http://www.itk.org/Wiki/DICOM_QueryRetrieve_Explained
-    // http://dicomiseasy.blogspot.be/2012/01/dicom-queryretrieve-part-i.html  
-    if (tag == DICOM_TAG_MODALITIES_IN_STUDY)
-    {
-      SetModalitiesInStudy(dicomQuery);
-    }
-    else 
-    {
-      Add(tag, IFindConstraint::ParseDicomConstraint(tag, dicomQuery, caseSensitive));
-    }
-  }
-
-}
--- a/OrthancServer/Search/LookupResource.h	Wed Dec 19 14:35:55 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,115 +0,0 @@
-/**
- * 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 General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "ListConstraint.h"
-#include "SetOfResources.h"
-
-#include <memory>
-
-namespace Orthanc
-{
-  class LookupResource : public boost::noncopyable
-  {
-  private:
-    typedef std::map<DicomTag, IFindConstraint*>  Constraints;
-    
-    class Level
-    {
-    private:
-      ResourceType        level_;
-      std::set<DicomTag>  identifiers_;
-      std::set<DicomTag>  mainTags_;
-      Constraints         identifiersConstraints_;
-      Constraints         mainTagsConstraints_;
-
-    public:
-      Level(ResourceType level);
-
-      ~Level();
-
-      bool Add(const DicomTag& tag,
-               std::auto_ptr<IFindConstraint>& constraint);
-
-      void Apply(SetOfResources& candidates,
-                 IDatabaseWrapper& database) const;
-
-      bool IsMatch(const DicomMap& dicom) const;
-    };
-
-    typedef std::map<ResourceType, Level*>  Levels;
-
-    ResourceType                    level_;
-    Levels                          levels_;
-    Constraints                     unoptimizedConstraints_;   // Constraints on non-main DICOM tags
-    std::auto_ptr<ListConstraint>   modalitiesInStudy_;
-
-    bool AddInternal(ResourceType level,
-                     const DicomTag& tag,
-                     std::auto_ptr<IFindConstraint>& constraint);
-
-    void ApplyLevel(SetOfResources& candidates,
-                    ResourceType level,
-                    IDatabaseWrapper& database) const;
-
-  public:
-    LookupResource(ResourceType level);
-
-    ~LookupResource();
-
-    ResourceType GetLevel() const
-    {
-      return level_;
-    }
-
-    void SetModalitiesInStudy(const std::string& modalities); 
-
-    void Add(const DicomTag& tag,
-             IFindConstraint* constraint);   // Takes ownership
-
-    void AddDicomConstraint(const DicomTag& tag,
-                            const std::string& dicomQuery,
-                            bool caseSensitive);
-
-    void FindCandidates(std::list<int64_t>& result,
-                        IDatabaseWrapper& database) const;
-
-    bool HasOnlyMainDicomTags() const
-    {
-      return unoptimizedConstraints_.empty();
-    }
-
-    bool IsMatch(const DicomMap& dicom) const;
-  };
-}
--- a/OrthancServer/Search/RangeConstraint.cpp	Wed Dec 19 14:35:55 2018 +0100
+++ b/OrthancServer/Search/RangeConstraint.cpp	Wed Dec 19 16:31:10 2018 +0100
@@ -56,29 +56,6 @@
   }
 
 
-  void RangeConstraint::Setup(LookupIdentifierQuery& lookup,
-                              const DicomTag& tag) const
-  {
-    if (!lower_.empty() &&
-        !upper_.empty())
-    {
-      lookup.AddRange(tag, lower_, upper_);
-    }
-    else
-    {
-      if (!lower_.empty())
-      {
-        lookup.AddConstraint(tag, IdentifierConstraintType_GreaterOrEqual, lower_);
-      }
-
-      if (!upper_.empty())
-      {
-        lookup.AddConstraint(tag, IdentifierConstraintType_SmallerOrEqual, upper_);
-      }
-    }
-  }
-
-
   bool RangeConstraint::Match(const std::string& value) const
   {
     std::string v;
--- a/OrthancServer/Search/RangeConstraint.h	Wed Dec 19 14:35:55 2018 +0100
+++ b/OrthancServer/Search/RangeConstraint.h	Wed Dec 19 16:31:10 2018 +0100
@@ -61,9 +61,6 @@
       return new RangeConstraint(*this);
     }
 
-    virtual void Setup(LookupIdentifierQuery& lookup,
-                       const DicomTag& tag) const;
-
     virtual bool Match(const std::string& value) const;
 
     virtual std::string Format() const
--- a/OrthancServer/Search/ValueConstraint.cpp	Wed Dec 19 14:35:55 2018 +0100
+++ b/OrthancServer/Search/ValueConstraint.cpp	Wed Dec 19 16:31:10 2018 +0100
@@ -55,12 +55,6 @@
   }
 
 
-  void ValueConstraint::Setup(LookupIdentifierQuery& lookup,
-                              const DicomTag& tag) const
-  {
-    lookup.AddConstraint(tag, IdentifierConstraintType_Equal, value_);
-  }
-
   bool ValueConstraint::Match(const std::string& value) const
   {
     if (isCaseSensitive_)
--- a/OrthancServer/Search/ValueConstraint.h	Wed Dec 19 14:35:55 2018 +0100
+++ b/OrthancServer/Search/ValueConstraint.h	Wed Dec 19 16:31:10 2018 +0100
@@ -58,9 +58,6 @@
       return new ValueConstraint(*this);
     }
 
-    virtual void Setup(LookupIdentifierQuery& lookup,
-                       const DicomTag& tag) const;
-
     virtual bool Match(const std::string& value) const;
 
     virtual std::string Format() const
--- a/OrthancServer/Search/WildcardConstraint.cpp	Wed Dec 19 14:35:55 2018 +0100
+++ b/OrthancServer/Search/WildcardConstraint.cpp	Wed Dec 19 16:31:10 2018 +0100
@@ -34,6 +34,8 @@
 #include "../PrecompiledHeadersServer.h"
 #include "WildcardConstraint.h"
 
+#include "../../Core/Toolbox.h"
+
 #include <boost/regex.hpp>
 
 namespace Orthanc
@@ -87,12 +89,6 @@
     }
   }
 
-  void WildcardConstraint::Setup(LookupIdentifierQuery& lookup,
-                                 const DicomTag& tag) const
-  {
-    lookup.AddConstraint(tag, IdentifierConstraintType_Wildcard, pimpl_->wildcard_);
-  }
-
   std::string WildcardConstraint::Format() const
   {
     return pimpl_->wildcard_;
--- a/OrthancServer/Search/WildcardConstraint.h	Wed Dec 19 14:35:55 2018 +0100
+++ b/OrthancServer/Search/WildcardConstraint.h	Wed Dec 19 16:31:10 2018 +0100
@@ -56,9 +56,6 @@
       return new WildcardConstraint(*this);
     }
 
-    virtual void Setup(LookupIdentifierQuery& lookup,
-                       const DicomTag& tag) const;
-
     virtual bool Match(const std::string& value) const;
 
     virtual std::string Format() const;
--- a/OrthancServer/ServerContext.cpp	Wed Dec 19 14:35:55 2018 +0100
+++ b/OrthancServer/ServerContext.cpp	Wed Dec 19 16:31:10 2018 +0100
@@ -42,7 +42,6 @@
 #include "../Plugins/Engine/OrthancPlugins.h"
 #include "OrthancConfiguration.h"
 #include "OrthancRestApi/OrthancRestApi.h"
-#include "Search/LookupResource.h"
 #include "ServerJobs/OrthancJobUnserializer.h"
 #include "ServerToolbox.h"
 
@@ -774,11 +773,13 @@
 
 
   void ServerContext::Apply(ILookupVisitor& visitor,
-                            const ::Orthanc::LookupResource& lookup,
+                            const DatabaseLookup& lookup,
+                            ResourceType queryLevel,
                             size_t since,
                             size_t limit)
   {
     LookupMode mode;
+    unsigned int databaseLimit;
       
     {
       // New configuration option in 1.5.1
@@ -804,11 +805,24 @@
                                "Configuration option \"StorageAccessOnFind\" "
                                "should be \"Always\", \"Never\" or \"Answers\": " + value);
       }
+
+      if (queryLevel == ResourceType_Instance)
+      {
+        databaseLimit = lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindInstances", 0);
+      }
+      else
+      {
+        databaseLimit = lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindResults", 0);
+      }
     }      
 
+    std::vector<std::string> resources, instances;
 
-    std::vector<std::string> resources, instances;
-    GetIndex().FindCandidates(resources, instances, lookup);
+    const size_t lookupLimit = (databaseLimit == 0 ? 0 : databaseLimit + 1);      
+    GetIndex().ApplyLookupResources(resources, &instances, lookup, queryLevel, lookupLimit);
+
+    bool complete = (databaseLimit == 0 ||
+                     resources.size() > databaseLimit);
 
     LOG(INFO) << "Number of candidate resources after fast DB filtering on main DICOM tags: " << resources.size();
 
@@ -816,7 +830,6 @@
 
     size_t countResults = 0;
     size_t skipped = 0;
-    bool complete = true;
 
     const bool isDicomAsJsonNeeded = visitor.IsDicomAsJsonNeeded();
     
--- a/OrthancServer/ServerContext.h	Wed Dec 19 14:35:55 2018 +0100
+++ b/OrthancServer/ServerContext.h	Wed Dec 19 16:31:10 2018 +0100
@@ -38,7 +38,6 @@
 #include "LuaScripting.h"
 #include "OrthancHttpHandler.h"
 #include "ServerIndex.h"
-#include "Search/LookupResource.h"
 
 #include "../Core/Cache/MemoryCache.h"
 #include "../Core/Cache/SharedArchive.h"
@@ -363,7 +362,8 @@
     void Stop();
 
     void Apply(ILookupVisitor& visitor,
-               const ::Orthanc::LookupResource& lookup,
+               const DatabaseLookup& lookup,
+               ResourceType queryLevel,
                size_t since,
                size_t limit);
 
--- a/OrthancServer/ServerEnumerations.h	Wed Dec 19 14:35:55 2018 +0100
+++ b/OrthancServer/ServerEnumerations.h	Wed Dec 19 16:31:10 2018 +0100
@@ -93,8 +93,7 @@
     GlobalProperty_FlushSleep = 2,
     GlobalProperty_AnonymizationSequence = 3,
     GlobalProperty_JobsRegistry = 5,
-    GlobalProperty_TotalCompressedSize = 6,     // Reserved for Orthanc > 1.5.0
-    GlobalProperty_TotalUncompressedSize = 7,   // Reserved for Orthanc > 1.5.0
+    GlobalProperty_GetTotalSizeIsFast = 6,      // New in Orthanc 1.5.2
     GlobalProperty_Modalities = 20,             // New in Orthanc 1.5.0
     GlobalProperty_Peers = 21,                  // New in Orthanc 1.5.0
 
--- a/OrthancServer/ServerIndex.cpp	Wed Dec 19 14:35:55 2018 +0100
+++ b/OrthancServer/ServerIndex.cpp	Wed Dec 19 16:31:10 2018 +0100
@@ -50,7 +50,6 @@
 #include "../Core/DicomParsing/FromDcmtkBridge.h"
 #include "ServerContext.h"
 #include "DicomInstanceToStore.h"
-#include "Search/LookupResource.h"
 
 #include <boost/lexical_cast.hpp>
 #include <stdio.h>
@@ -215,7 +214,7 @@
   {
   private:
     ServerIndex& index_;
-    std::auto_ptr<SQLite::ITransaction> transaction_;
+    std::auto_ptr<IDatabaseWrapper::ITransaction> transaction_;
     bool isCommitted_;
 
   public:
@@ -226,8 +225,6 @@
       transaction_.reset(index_.db_.StartTransaction());
       transaction_->Begin();
 
-      assert(index_.currentStorageSize_ == index_.db_.GetTotalCompressedSize());
-
       index_.listener_->StartTransaction();
     }
 
@@ -245,18 +242,16 @@
     {
       if (!isCommitted_)
       {
-        transaction_->Commit();
+        int64_t delta = (static_cast<int64_t>(sizeOfAddedFiles) -
+                         static_cast<int64_t>(index_.listener_->GetSizeOfFilesToRemove()));
+
+        transaction_->Commit(delta);
 
         // We can remove the files once the SQLite transaction has
         // been successfully committed. Some files might have to be
         // deleted because of recycling.
         index_.listener_->CommitFilesToRemove();
 
-        index_.currentStorageSize_ += sizeOfAddedFiles;
-
-        assert(index_.currentStorageSize_ >= index_.listener_->GetSizeOfFilesToRemove());
-        index_.currentStorageSize_ -= index_.listener_->GetSizeOfFilesToRemove();
-
         // Send all the pending changes to the Orthanc plugins
         index_.listener_->CommitChanges();
 
@@ -303,6 +298,107 @@
   };
 
 
+  class ServerIndex::MainDicomTagsRegistry : public boost::noncopyable
+  {
+  private:
+    class TagInfo
+    {
+    private:
+      ResourceType  level_;
+      DicomTagType  type_;
+
+    public:
+      TagInfo()
+      {
+      }
+
+      TagInfo(ResourceType level,
+              DicomTagType type) :
+        level_(level),
+        type_(type)
+      {
+      }
+
+      ResourceType GetLevel() const
+      {
+        return level_;
+      }
+
+      DicomTagType GetType() const
+      {
+        return type_;
+      }
+    };
+      
+    typedef std::map<DicomTag, TagInfo>   Registry;
+
+
+    Registry  registry_;
+      
+    void LoadTags(ResourceType level)
+    {
+      const DicomTag* tags = NULL;
+      size_t size;
+  
+      ServerToolbox::LoadIdentifiers(tags, size, level);
+  
+      for (size_t i = 0; i < size; i++)
+      {
+        if (registry_.find(tags[i]) == registry_.end())
+        {
+          registry_[tags[i]] = TagInfo(level, DicomTagType_Identifier);
+        }
+        else
+        {
+          // These patient-level tags are copied in the study level
+          assert(level == ResourceType_Study &&
+                 (tags[i] == DICOM_TAG_PATIENT_ID ||
+                  tags[i] == DICOM_TAG_PATIENT_NAME ||
+                  tags[i] == DICOM_TAG_PATIENT_BIRTH_DATE));
+        }
+      }
+  
+      DicomMap::LoadMainDicomTags(tags, size, level);
+  
+      for (size_t i = 0; i < size; i++)
+      {
+        if (registry_.find(tags[i]) == registry_.end())
+        {
+          registry_[tags[i]] = TagInfo(level, DicomTagType_Main);
+        }
+      }
+    }
+
+  public:
+    MainDicomTagsRegistry()
+    {
+      LoadTags(ResourceType_Patient);
+      LoadTags(ResourceType_Study);
+      LoadTags(ResourceType_Series);
+      LoadTags(ResourceType_Instance); 
+    }
+
+    void LookupTag(ResourceType& level,
+                   DicomTagType& type,
+                   const DicomTag& tag) const
+    {
+      Registry::const_iterator it = registry_.find(tag);
+
+      if (it == registry_.end())
+      {
+        // Default values
+        level = ResourceType_Instance;
+        type = DicomTagType_Generic;
+      }
+      else
+      {
+        level = it->second.GetLevel();
+        type = it->second.GetType();
+      }
+    }
+  };
+
+
   bool ServerIndex::DeleteResource(Json::Value& target,
                                    const std::string& uuid,
                                    ResourceType expectedType)
@@ -545,13 +641,12 @@
     db_(db),
     maximumStorageSize_(0),
     maximumPatients_(0),
-    overwrite_(false)
+    overwrite_(false),
+    mainDicomTagsRegistry_(new MainDicomTagsRegistry)
   {
     listener_.reset(new Listener(context));
     db_.SetListener(*listener_);
 
-    currentStorageSize_ = db_.GetTotalCompressedSize();
-
     // Initial recycling if the parameters have changed since the last
     // execution of Orthanc
     StandaloneRecycling();
@@ -877,8 +972,7 @@
     boost::mutex::scoped_lock lock(mutex_);
     target = Json::objectValue;
 
-    uint64_t cs = currentStorageSize_;
-    assert(cs == db_.GetTotalCompressedSize());
+    uint64_t cs = db_.GetTotalCompressedSize();
     uint64_t us = db_.GetTotalUncompressedSize();
     target["TotalDiskSize"] = boost::lexical_cast<std::string>(cs);
     target["TotalUncompressedSize"] = boost::lexical_cast<std::string>(us);
@@ -1369,10 +1463,9 @@
   {
     if (maximumStorageSize_ != 0)
     {
-      uint64_t currentSize = currentStorageSize_ - listener_->GetSizeOfFilesToRemove();
-      assert(db_.GetTotalCompressedSize() == currentSize);
-
-      if (currentSize + instanceSize > maximumStorageSize_)
+      assert(maximumStorageSize_ >= instanceSize);
+      
+      if (db_.IsDiskSizeAbove(maximumStorageSize_ - instanceSize))
       {
         return true;
       }
@@ -2030,7 +2123,7 @@
 
 
 
-  void ServerIndex::LookupIdentifierExact(std::list<std::string>& result,
+  void ServerIndex::LookupIdentifierExact(std::vector<std::string>& result,
                                           ResourceType level,
                                           const DicomTag& tag,
                                           const std::string& value)
@@ -2043,11 +2136,15 @@
     
     result.clear();
 
-    boost::mutex::scoped_lock lock(mutex_);
-
-    LookupIdentifierQuery query(level);
-    query.AddConstraint(tag, IdentifierConstraintType_Equal, value);
-    query.Apply(result, db_);
+    DicomTagConstraint c(tag, ConstraintType_Equal, value, true, true);
+
+    std::vector<DatabaseConstraint> query;
+    query.push_back(DatabaseConstraint(c, level, DicomTagType_Identifier));
+
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+      db_.ApplyLookupResources(result, NULL, query, level, 0);
+    }
   }
 
 
@@ -2337,36 +2434,6 @@
   }
 
 
-  void ServerIndex::FindCandidates(std::vector<std::string>& resources,
-                                   std::vector<std::string>& instances,
-                                   const ::Orthanc::LookupResource& lookup)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-   
-    std::list<int64_t> tmp;
-    lookup.FindCandidates(tmp, db_);
-
-    resources.resize(tmp.size());
-    instances.resize(tmp.size());
-
-    size_t pos = 0;
-    for (std::list<int64_t>::const_iterator
-           it = tmp.begin(); it != tmp.end(); ++it, pos++)
-    {
-      assert(db_.GetResourceType(*it) == lookup.GetLevel());
-      
-      int64_t instance;
-      if (!ServerToolbox::FindOneChildInstance(instance, db_, *it, lookup.GetLevel()))
-      {
-        throw OrthancException(ErrorCode_InternalError);
-      }
-
-      resources[pos] = db_.GetPublicId(*it);
-      instances[pos] = db_.GetPublicId(instance);
-    }
-  }
-
-
   bool ServerIndex::LookupParent(std::string& target,
                                  const std::string& publicId,
                                  ResourceType parentType)
@@ -2460,4 +2527,53 @@
       LOG(ERROR) << "EXCEPTION [" << e.What() << "]";
     }
   }
+
+
+  void ServerIndex::NormalizeLookup(std::vector<DatabaseConstraint>& target,
+                                    const DatabaseLookup& source,
+                                    ResourceType queryLevel) const
+  {
+    assert(mainDicomTagsRegistry_.get() != NULL);
+
+    target.clear();
+    target.reserve(source.GetConstraintsCount());
+
+    for (size_t i = 0; i < source.GetConstraintsCount(); i++)
+    {
+      ResourceType level;
+      DicomTagType type;
+      
+      mainDicomTagsRegistry_->LookupTag(level, type, source.GetConstraint(i).GetTag());
+
+      if (type == DicomTagType_Identifier ||
+          type == DicomTagType_Main)
+      {
+        // Use the fact that patient-level tags are copied at the study level
+        if (level == ResourceType_Patient &&
+            queryLevel != ResourceType_Patient)
+        {
+          level = ResourceType_Study;
+        }
+        
+        DatabaseConstraint c(source.GetConstraint(i), level, type);
+        target.push_back(c);
+      }
+    }
+  }
+
+
+  void ServerIndex::ApplyLookupResources(std::vector<std::string>& resourcesId,
+                                         std::vector<std::string>* instancesId,
+                                         const DatabaseLookup& lookup,
+                                         ResourceType queryLevel,
+                                         size_t limit)
+  {
+    std::vector<DatabaseConstraint> normalized;
+    NormalizeLookup(normalized, lookup, queryLevel);
+
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+      db_.ApplyLookupResources(resourcesId, instancesId, normalized, queryLevel, limit);
+    }
+  }
 }
--- a/OrthancServer/ServerIndex.h	Wed Dec 19 14:35:55 2018 +0100
+++ b/OrthancServer/ServerIndex.h	Wed Dec 19 16:31:10 2018 +0100
@@ -44,7 +44,6 @@
 
 namespace Orthanc
 {
-  class LookupResource;
   class ServerContext;
   class DicomInstanceToStore;
   class ParsedDicomFile;
@@ -59,6 +58,7 @@
     class Listener;
     class Transaction;
     class UnstableResourcePayload;
+    class MainDicomTagsRegistry;
 
     bool done_;
     boost::mutex mutex_;
@@ -69,10 +69,10 @@
     IDatabaseWrapper& db_;
     LeastRecentlyUsedIndex<int64_t, UnstableResourcePayload>  unstableResources_;
 
-    uint64_t     currentStorageSize_;
     uint64_t     maximumStorageSize_;
     unsigned int maximumPatients_;
     bool         overwrite_;
+    std::auto_ptr<MainDicomTagsRegistry>  mainDicomTagsRegistry_;
 
     static void FlushThread(ServerIndex* that,
                             unsigned int threadSleep);
@@ -126,6 +126,10 @@
                              MetadataType metadata,
                              const std::string& value);
 
+    void NormalizeLookup(std::vector<DatabaseConstraint>& target,
+                         const DatabaseLookup& source,
+                         ResourceType level) const;
+
   public:
     ServerIndex(ServerContext& context,
                 IDatabaseWrapper& database,
@@ -250,7 +254,7 @@
                        /* out */ uint64_t& dicomUncompressedSize, 
                        const std::string& publicId);
 
-    void LookupIdentifierExact(std::list<std::string>& result,
+    void LookupIdentifierExact(std::vector<std::string>& result,
                                ResourceType level,
                                const DicomTag& tag,
                                const std::string& value);
@@ -284,14 +288,16 @@
 
     unsigned int GetDatabaseVersion();
 
-    void FindCandidates(std::vector<std::string>& resources,
-                        std::vector<std::string>& instances,
-                        const ::Orthanc::LookupResource& lookup);
-
     bool LookupParent(std::string& target,
                       const std::string& publicId,
                       ResourceType parentType);
 
     void ReconstructInstance(ParsedDicomFile& dicom);
+
+    void ApplyLookupResources(std::vector<std::string>& resourcesId,
+                              std::vector<std::string>* instancesId,  // Can be NULL if not needed
+                              const DatabaseLookup& lookup,
+                              ResourceType queryLevel,
+                              size_t limit);
   };
 }
--- a/OrthancServer/ServerToolbox.cpp	Wed Dec 19 14:35:55 2018 +0100
+++ b/OrthancServer/ServerToolbox.cpp	Wed Dec 19 16:31:10 2018 +0100
@@ -170,6 +170,9 @@
 
       for (size_t i = 0; i < size; i++)
       {
+        // The identifiers tags are a subset of the main DICOM tags
+        assert(DicomMap::IsMainDicomTag(tags[i]));
+        
         const DicomValue* value = map.TestAndGetValue(tags[i]);
         if (value != NULL &&
             !value->IsNull() &&
--- a/OrthancServer/main.cpp	Wed Dec 19 14:35:55 2018 +0100
+++ b/OrthancServer/main.cpp	Wed Dec 19 16:31:10 2018 +0100
@@ -1174,7 +1174,7 @@
   else if (currentVersion != ORTHANC_DATABASE_VERSION)
   {
     throw OrthancException(ErrorCode_IncompatibleDatabaseVersion,
-                           "The database schema must be changed from version " +
+                           "The database schema must be upgraded from version " +
                            boost::lexical_cast<std::string>(currentVersion) + " to " +
                            boost::lexical_cast<std::string>(ORTHANC_DATABASE_VERSION) +
                            ": Please run Orthanc with the \"--upgrade\" argument");
--- a/Plugins/Engine/OrthancPluginDatabase.cpp	Wed Dec 19 14:35:55 2018 +0100
+++ b/Plugins/Engine/OrthancPluginDatabase.cpp	Wed Dec 19 16:31:10 2018 +0100
@@ -47,6 +47,62 @@
 
 namespace Orthanc
 {
+  class OrthancPluginDatabase::Transaction : public IDatabaseWrapper::ITransaction
+  {
+  private:
+    OrthancPluginDatabase&  that_;
+
+    void CheckSuccess(OrthancPluginErrorCode code) const
+    {
+      if (code != OrthancPluginErrorCode_Success)
+      {
+        that_.errorDictionary_.LogError(code, true);
+        throw OrthancException(static_cast<ErrorCode>(code));
+      }
+    }
+
+  public:
+    Transaction(OrthancPluginDatabase& that) :
+      that_(that)
+    {
+    }
+
+    virtual void Begin()
+    {
+      CheckSuccess(that_.backend_.startTransaction(that_.payload_));
+    }
+
+    virtual void Rollback()
+    {
+      CheckSuccess(that_.backend_.rollbackTransaction(that_.payload_));
+    }
+
+    virtual void Commit(int64_t diskSizeDelta)
+    {
+      if (that_.fastGetTotalSize_)
+      {
+        CheckSuccess(that_.backend_.commitTransaction(that_.payload_));
+      }
+      else
+      {
+        if (static_cast<int64_t>(that_.currentDiskSize_) + diskSizeDelta < 0)
+        {
+          throw OrthancException(ErrorCode_DatabasePlugin);
+        }
+
+        uint64_t newDiskSize = (that_.currentDiskSize_ + diskSizeDelta);
+
+        assert(newDiskSize == that_.GetTotalCompressedSize());
+
+        CheckSuccess(that_.backend_.commitTransaction(that_.payload_));
+
+        // The transaction has succeeded, we can commit the new disk size
+        that_.currentDiskSize_ = newDiskSize;
+      }
+    }
+  };
+
+
   static FileInfo Convert(const OrthancPluginAttachment& attachment)
   {
     return FileInfo(attachment.uuid,
@@ -189,6 +245,35 @@
   }
 
 
+  void OrthancPluginDatabase::Open()
+  {
+    CheckSuccess(backend_.open(payload_));
+
+    {
+      Transaction transaction(*this);
+      transaction.Begin();
+
+      std::string tmp;
+      fastGetTotalSize_ =
+        (LookupGlobalProperty(tmp, GlobalProperty_GetTotalSizeIsFast) &&
+         tmp == "1");
+      
+      if (fastGetTotalSize_)
+      {
+        currentDiskSize_ = 0;   // Unused
+      }
+      else
+      {
+        // This is the case of database plugins using Orthanc SDK <= 1.5.2
+        LOG(WARNING) << "Your database index plugin is not compatible with multiple Orthanc writers";
+        currentDiskSize_ = GetTotalCompressedSize();
+      }
+
+      transaction.Commit(0);
+    }
+  }
+
+
   void OrthancPluginDatabase::AddAttachment(int64_t id,
                                             const FileInfo& attachment)
   {
@@ -786,52 +871,9 @@
   }
 
 
-  class OrthancPluginDatabase::Transaction : public SQLite::ITransaction
+  IDatabaseWrapper::ITransaction* OrthancPluginDatabase::StartTransaction()
   {
-  private:
-    const OrthancPluginDatabaseBackend& backend_;
-    void* payload_;
-    PluginsErrorDictionary&  errorDictionary_;
-
-    void CheckSuccess(OrthancPluginErrorCode code)
-    {
-      if (code != OrthancPluginErrorCode_Success)
-      {
-        errorDictionary_.LogError(code, true);
-        throw OrthancException(static_cast<ErrorCode>(code));
-      }
-    }
-
-  public:
-    Transaction(const OrthancPluginDatabaseBackend& backend,
-                void* payload,
-                PluginsErrorDictionary&  errorDictionary) :
-      backend_(backend),
-      payload_(payload),
-      errorDictionary_(errorDictionary)
-    {
-    }
-
-    virtual void Begin()
-    {
-      CheckSuccess(backend_.startTransaction(payload_));
-    }
-
-    virtual void Rollback()
-    {
-      CheckSuccess(backend_.rollbackTransaction(payload_));
-    }
-
-    virtual void Commit()
-    {
-      CheckSuccess(backend_.commitTransaction(payload_));
-    }
-  };
-
-
-  SQLite::ITransaction* OrthancPluginDatabase::StartTransaction()
-  {
-    return new Transaction(backend_, payload_, errorDictionary_);
+    return new Transaction(*this);
   }
 
 
@@ -892,7 +934,7 @@
   {
     if (extensions_.upgradeDatabase != NULL)
     {
-      Transaction transaction(backend_, payload_, errorDictionary_);
+      Transaction transaction(*this);
       transaction.Begin();
 
       OrthancPluginErrorCode code = extensions_.upgradeDatabase(
@@ -901,7 +943,7 @@
 
       if (code == OrthancPluginErrorCode_Success)
       {
-        transaction.Commit();
+        transaction.Commit(0);
       }
       else
       {
@@ -1104,4 +1146,28 @@
                                boost::lexical_cast<std::string>(answer.type));
     }
   }
+
+    
+  bool OrthancPluginDatabase::IsDiskSizeAbove(uint64_t threshold)
+  {
+    if (fastGetTotalSize_)
+    {
+      return GetTotalCompressedSize() > threshold;
+    }
+    else
+    {
+      assert(GetTotalCompressedSize() == currentDiskSize_);
+      return currentDiskSize_ > threshold;
+    }      
+  }
+
+
+  void OrthancPluginDatabase::ApplyLookupResources(std::vector<std::string>& patientsId,
+                                                   std::vector<std::string>* instancesId,
+                                                   const std::vector<DatabaseConstraint>& lookup,
+                                                   ResourceType queryLevel,
+                                                   size_t limit)
+  {
+    throw OrthancException(ErrorCode_NotImplemented);
+  }
 }
--- a/Plugins/Engine/OrthancPluginDatabase.h	Wed Dec 19 14:35:55 2018 +0100
+++ b/Plugins/Engine/OrthancPluginDatabase.h	Wed Dec 19 16:31:10 2018 +0100
@@ -57,6 +57,9 @@
     void* payload_;
     IDatabaseListener* listener_;
 
+    bool      fastGetTotalSize_;
+    uint64_t  currentDiskSize_;
+
     std::list<std::string>         answerStrings_;
     std::list<int32_t>             answerInt32_;
     std::list<int64_t>             answerInt64_;
@@ -93,10 +96,7 @@
                           size_t extensionsSize,
                           void *payload);
 
-    virtual void Open()
-    {
-      CheckSuccess(backend_.open(payload_));
-    }
+    virtual void Open();
 
     virtual void Close()
     {
@@ -255,7 +255,7 @@
     virtual void SetProtectedPatient(int64_t internalId, 
                                      bool isProtected);
 
-    virtual SQLite::ITransaction* StartTransaction();
+    virtual IDatabaseWrapper::ITransaction* StartTransaction();
 
     virtual void SetListener(IDatabaseListener& listener)
     {
@@ -268,6 +268,14 @@
                          IStorageArea& storageArea);
 
     void AnswerReceived(const _OrthancPluginDatabaseAnswer& answer);
+
+    virtual bool IsDiskSizeAbove(uint64_t threshold);
+
+    virtual void ApplyLookupResources(std::vector<std::string>& patientsId,
+                                      std::vector<std::string>* instancesId,
+                                      const std::vector<DatabaseConstraint>& lookup,
+                                      ResourceType queryLevel,
+                                      size_t limit);
   };
 }
 
--- a/Plugins/Engine/OrthancPlugins.cpp	Wed Dec 19 14:35:55 2018 +0100
+++ b/Plugins/Engine/OrthancPlugins.cpp	Wed Dec 19 16:31:10 2018 +0100
@@ -1644,7 +1644,7 @@
         throw OrthancException(ErrorCode_InternalError);
     }
 
-    std::list<std::string> result;
+    std::vector<std::string> result;
 
     {
       PImpl::ServerContextLock lock(*pimpl_);
@@ -1653,7 +1653,7 @@
 
     if (result.size() == 1)
     {
-      *p.result = CopyString(result.front());
+      *p.result = CopyString(result[0]);
     }
     else
     {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/DatabaseOptimizations/LookupIdentifierQuery.cpp	Wed Dec 19 16:31:10 2018 +0100
@@ -0,0 +1,215 @@
+/**
+ * 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 General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeadersServer.h"
+#include "LookupIdentifierQuery.h"
+
+#include "../../Core/DicomParsing/FromDcmtkBridge.h"
+#include "../../Core/OrthancException.h"
+#include "../ServerToolbox.h"
+#include "SetOfResources.h"
+
+#include <cassert>
+
+
+
+namespace Orthanc
+{
+  LookupIdentifierQuery::SingleConstraint::
+  SingleConstraint(const DicomTag& tag,
+                   IdentifierConstraintType type,
+                   const std::string& value) : 
+    tag_(tag),
+    type_(type),
+    value_(ServerToolbox::NormalizeIdentifier(value))
+  {
+  }
+
+
+  LookupIdentifierQuery::RangeConstraint::
+  RangeConstraint(const DicomTag& tag,
+                  const std::string& start,
+                  const std::string& end) : 
+    tag_(tag),
+    start_(ServerToolbox::NormalizeIdentifier(start)),
+    end_(ServerToolbox::NormalizeIdentifier(end))
+  {
+  }
+
+
+  LookupIdentifierQuery::Disjunction::~Disjunction()
+  {
+    for (size_t i = 0; i < singleConstraints_.size(); i++)
+    {
+      delete singleConstraints_[i];
+    }
+
+    for (size_t i = 0; i < rangeConstraints_.size(); i++)
+    {
+      delete rangeConstraints_[i];
+    }
+  }
+
+
+  void LookupIdentifierQuery::Disjunction::Add(const DicomTag& tag,
+                                               IdentifierConstraintType type,
+                                               const std::string& value)
+  {
+    singleConstraints_.push_back(new SingleConstraint(tag, type, value));
+  }
+
+
+  void LookupIdentifierQuery::Disjunction::AddRange(const DicomTag& tag,
+                                                    const std::string& start,
+                                                    const std::string& end)
+  {
+    rangeConstraints_.push_back(new RangeConstraint(tag, start, end));
+  }
+
+
+  LookupIdentifierQuery::~LookupIdentifierQuery()
+  {
+    for (Disjunctions::iterator it = disjunctions_.begin();
+         it != disjunctions_.end(); ++it)
+    {
+      delete *it;
+    }
+  }
+
+
+  bool LookupIdentifierQuery::IsIdentifier(const DicomTag& tag)
+  {
+    return ServerToolbox::IsIdentifier(tag, level_);
+  }
+
+
+  void LookupIdentifierQuery::AddConstraint(DicomTag tag,
+                                            IdentifierConstraintType type,
+                                            const std::string& value)
+  {
+    assert(IsIdentifier(tag));
+    disjunctions_.push_back(new Disjunction);
+    disjunctions_.back()->Add(tag, type, value);
+  }
+
+
+  void LookupIdentifierQuery::AddRange(DicomTag tag,
+                                       const std::string& start,
+                                       const std::string& end)
+  {
+    assert(IsIdentifier(tag));
+    disjunctions_.push_back(new Disjunction);
+    disjunctions_.back()->AddRange(tag, start, end);
+  }
+
+
+  LookupIdentifierQuery::Disjunction& LookupIdentifierQuery::AddDisjunction()
+  {
+    disjunctions_.push_back(new Disjunction);
+    return *disjunctions_.back();
+  }
+
+
+  void LookupIdentifierQuery::Apply(std::list<std::string>& result,
+                                    IDatabaseWrapper& database)
+  {
+    SetOfResources resources(database, level_);
+    Apply(resources, database);
+
+    resources.Flatten(result);
+  }
+
+
+  void LookupIdentifierQuery::Apply(SetOfResources& result,
+                                    IDatabaseWrapper& database)
+  {
+    for (size_t i = 0; i < disjunctions_.size(); i++)
+    {
+      std::list<int64_t> a;
+
+      for (size_t j = 0; j < disjunctions_[i]->GetSingleConstraintsCount(); j++)
+      {
+        const SingleConstraint& constraint = disjunctions_[i]->GetSingleConstraint(j);
+        std::list<int64_t> b;
+        database.LookupIdentifier(b, level_, constraint.GetTag(), 
+                                  constraint.GetType(), constraint.GetValue());
+
+        a.splice(a.end(), b);
+      }
+
+      for (size_t j = 0; j < disjunctions_[i]->GetRangeConstraintsCount(); j++)
+      {
+        const RangeConstraint& constraint = disjunctions_[i]->GetRangeConstraint(j);
+        std::list<int64_t> b;
+        database.LookupIdentifierRange(b, level_, constraint.GetTag(), 
+                                       constraint.GetStart(), constraint.GetEnd());
+
+        a.splice(a.end(), b);
+      }
+
+      result.Intersect(a);
+    }
+  }
+
+
+  void LookupIdentifierQuery::Print(std::ostream& s) const
+  {
+    s << "Constraint: " << std::endl;
+    for (Disjunctions::const_iterator
+           it = disjunctions_.begin(); it != disjunctions_.end(); ++it)
+    {
+      if (it == disjunctions_.begin())
+        s << "   ";
+      else
+        s << "OR ";
+
+      for (size_t j = 0; j < (*it)->GetSingleConstraintsCount(); j++)
+      {
+        const SingleConstraint& c = (*it)->GetSingleConstraint(j);
+        s << FromDcmtkBridge::GetTagName(c.GetTag(), "");
+
+        switch (c.GetType())
+        {
+          case IdentifierConstraintType_Equal: s << " == "; break;
+          case IdentifierConstraintType_SmallerOrEqual: s << " <= "; break;
+          case IdentifierConstraintType_GreaterOrEqual: s << " >= "; break;
+          case IdentifierConstraintType_Wildcard: s << " ~= "; break;
+          default:
+            s << " ? ";
+        }
+
+        s << c.GetValue() << std::endl;
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/DatabaseOptimizations/LookupIdentifierQuery.h	Wed Dec 19 16:31:10 2018 +0100
@@ -0,0 +1,207 @@
+/**
+ * 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 General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../IDatabaseWrapper.h"
+
+#include "SetOfResources.h"
+
+#include <vector>
+#include <boost/noncopyable.hpp>
+
+namespace Orthanc
+{
+  /**
+   * Primitive for wildcard matching, as defined in DICOM:
+   * http://dicom.nema.org/dicom/2013/output/chtml/part04/sect_C.2.html#sect_C.2.2.2.4
+   * 
+   * "Any occurrence of an "*" or a "?", then "*" shall match any
+   * sequence of characters (including a zero length value) and "?"
+   * shall match any single character. This matching is case
+   * sensitive, except for Attributes with an PN Value
+   * Representation (e.g., Patient Name (0010,0010))."
+   * 
+   * Pay attention to the fact that "*" (resp. "?") generally
+   * corresponds to "%" (resp. "_") in primitive LIKE of SQL. The
+   * values "%", "_", "\" should in the user request should
+   * respectively be escaped as "\%", "\_" and "\\".
+   *
+   * This matching must be case sensitive: The special case of PN VR
+   * is taken into consideration by normalizing the query string in
+   * method "NormalizeIdentifier()".
+   **/
+
+  class LookupIdentifierQuery : public boost::noncopyable
+  {
+    // This class encodes a conjunction ("AND") of disjunctions. Each
+    // disjunction represents an "OR" of several constraints.
+
+  public:
+    class SingleConstraint
+    {
+    private:
+      DicomTag                  tag_;
+      IdentifierConstraintType  type_;
+      std::string               value_;
+
+    public:
+      SingleConstraint(const DicomTag& tag,
+                       IdentifierConstraintType type,
+                       const std::string& value);
+
+      const DicomTag& GetTag() const
+      {
+        return tag_;
+      }
+
+      IdentifierConstraintType GetType() const
+      {
+        return type_;
+      }
+      
+      const std::string& GetValue() const
+      {
+        return value_;
+      }
+    };
+
+
+    class RangeConstraint
+    {
+    private:
+      DicomTag     tag_;
+      std::string  start_;
+      std::string  end_;
+
+    public:
+      RangeConstraint(const DicomTag& tag,
+                      const std::string& start,
+                      const std::string& end);
+
+      const DicomTag& GetTag() const
+      {
+        return tag_;
+      }
+
+      const std::string& GetStart() const
+      {
+        return start_;
+      }
+
+      const std::string& GetEnd() const
+      {
+        return end_;
+      }
+    };
+
+
+    class Disjunction : public boost::noncopyable
+    {
+    private:
+      std::vector<SingleConstraint*>  singleConstraints_;
+      std::vector<RangeConstraint*>   rangeConstraints_;
+
+    public:
+      ~Disjunction();
+
+      void Add(const DicomTag& tag,
+               IdentifierConstraintType type,
+               const std::string& value);
+
+      void AddRange(const DicomTag& tag,
+                    const std::string& start,
+                    const std::string& end);
+
+      size_t GetSingleConstraintsCount() const
+      {
+        return singleConstraints_.size();
+      }
+
+      const SingleConstraint&  GetSingleConstraint(size_t i) const
+      {
+        return *singleConstraints_[i];
+      }
+
+      size_t GetRangeConstraintsCount() const
+      {
+        return rangeConstraints_.size();
+      }
+
+      const RangeConstraint&  GetRangeConstraint(size_t i) const
+      {
+        return *rangeConstraints_[i];
+      }
+    };
+
+
+  private:
+    typedef std::vector<Disjunction*>  Disjunctions;
+
+    ResourceType  level_;
+    Disjunctions  disjunctions_;
+
+  public:
+    LookupIdentifierQuery(ResourceType level) : level_(level)
+    {
+    }
+
+    ~LookupIdentifierQuery();
+
+    bool IsIdentifier(const DicomTag& tag);
+
+    void AddConstraint(DicomTag tag,
+                       IdentifierConstraintType type,
+                       const std::string& value);
+
+    void AddRange(DicomTag tag,
+                  const std::string& start,
+                  const std::string& end);
+
+    Disjunction& AddDisjunction();
+
+    ResourceType GetLevel() const
+    {
+      return level_;
+    }
+
+    // The database must be locked
+    void Apply(std::list<std::string>& result,
+               IDatabaseWrapper& database);
+
+    void Apply(SetOfResources& result,
+               IDatabaseWrapper& database);
+
+    void Print(std::ostream& s) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/DatabaseOptimizations/LookupResource.cpp	Wed Dec 19 16:31:10 2018 +0100
@@ -0,0 +1,479 @@
+/**
+ * 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 General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeadersServer.h"
+#include "LookupResource.h"
+
+#include "../../Core/OrthancException.h"
+#include "../../Core/FileStorage/StorageAccessor.h"
+#include "../ServerToolbox.h"
+#include "../../Core/DicomParsing/FromDcmtkBridge.h"
+
+
+namespace Orthanc
+{
+  static bool DoesDicomMapMatch(const DicomMap& dicom,
+                                const DicomTag& tag,
+                                const IFindConstraint& constraint)
+  {
+    const DicomValue* value = dicom.TestAndGetValue(tag);
+
+    return (value != NULL &&
+            !value->IsNull() &&
+            !value->IsBinary() &&
+            constraint.Match(value->GetContent()));
+  }
+
+  
+  LookupResource::Level::Level(ResourceType level) : level_(level)
+  {
+    const DicomTag* tags = NULL;
+    size_t size;
+    
+    ServerToolbox::LoadIdentifiers(tags, size, level);
+    
+    for (size_t i = 0; i < size; i++)
+    {
+      identifiers_.insert(tags[i]);
+    }
+    
+    DicomMap::LoadMainDicomTags(tags, size, level);
+    
+    for (size_t i = 0; i < size; i++)
+    {
+      if (identifiers_.find(tags[i]) == identifiers_.end())
+      {
+        mainTags_.insert(tags[i]);
+      }
+    }    
+  }
+
+  LookupResource::Level::~Level()
+  {
+    for (Constraints::iterator it = mainTagsConstraints_.begin();
+         it != mainTagsConstraints_.end(); ++it)
+    {
+      delete it->second;
+    }
+
+    for (Constraints::iterator it = identifiersConstraints_.begin();
+         it != identifiersConstraints_.end(); ++it)
+    {
+      delete it->second;
+    }
+  }
+
+  bool LookupResource::Level::Add(const DicomTag& tag,
+                                  std::auto_ptr<IFindConstraint>& constraint)
+  {
+    if (identifiers_.find(tag) != identifiers_.end())
+    {
+      if (level_ == ResourceType_Patient)
+      {
+        // The filters on the patient level must be cloned to the study level
+        identifiersConstraints_[tag] = constraint->Clone();
+      }
+      else
+      {
+        identifiersConstraints_[tag] = constraint.release();
+      }
+
+      return true;
+    }
+    else if (mainTags_.find(tag) != mainTags_.end())
+    {
+      if (level_ == ResourceType_Patient)
+      {
+        // The filters on the patient level must be cloned to the study level
+        mainTagsConstraints_[tag] = constraint->Clone();
+      }
+      else
+      {
+        mainTagsConstraints_[tag] = constraint.release();
+      }
+
+      return true;
+    }
+    else
+    {
+      // This is not a main DICOM tag
+      return false;
+    }
+  }
+
+
+  bool LookupResource::Level::IsMatch(const DicomMap& dicom) const
+  {
+    for (Constraints::const_iterator it = identifiersConstraints_.begin();
+         it != identifiersConstraints_.end(); ++it)
+    {
+      assert(it->second != NULL);
+
+      if (!DoesDicomMapMatch(dicom, it->first, *it->second))
+      {
+        return false;
+      }
+    }
+
+    for (Constraints::const_iterator it = mainTagsConstraints_.begin();
+         it != mainTagsConstraints_.end(); ++it)
+    {
+      assert(it->second != NULL);
+
+      if (!DoesDicomMapMatch(dicom, it->first, *it->second))
+      {
+        return false;
+      }
+    }
+
+    return true;
+  }
+  
+
+  LookupResource::LookupResource(ResourceType level) : level_(level)
+  {
+    switch (level)
+    {
+      case ResourceType_Patient:
+        levels_[ResourceType_Patient] = new Level(ResourceType_Patient);
+        break;
+
+      case ResourceType_Instance:
+        levels_[ResourceType_Instance] = new Level(ResourceType_Instance);
+        // Do not add "break" here
+
+      case ResourceType_Series:
+        levels_[ResourceType_Series] = new Level(ResourceType_Series);
+        // Do not add "break" here
+
+      case ResourceType_Study:
+        levels_[ResourceType_Study] = new Level(ResourceType_Study);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+  }
+
+
+  LookupResource::~LookupResource()
+  {
+    for (Levels::iterator it = levels_.begin();
+         it != levels_.end(); ++it)
+    {
+      delete it->second;
+    }
+
+    for (Constraints::iterator it = unoptimizedConstraints_.begin();
+         it != unoptimizedConstraints_.end(); ++it)
+    {
+      delete it->second;
+    }    
+  }
+
+
+
+  bool LookupResource::AddInternal(ResourceType level,
+                                   const DicomTag& tag,
+                                   std::auto_ptr<IFindConstraint>& constraint)
+  {
+    Levels::iterator it = levels_.find(level);
+    if (it != levels_.end())
+    {
+      if (it->second->Add(tag, constraint))
+      {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+
+  void LookupResource::Add(const DicomTag& tag,
+                           IFindConstraint* constraint)
+  {
+    std::auto_ptr<IFindConstraint> c(constraint);
+
+    if (!AddInternal(ResourceType_Patient, tag, c) &&
+        !AddInternal(ResourceType_Study, tag, c) &&
+        !AddInternal(ResourceType_Series, tag, c) &&
+        !AddInternal(ResourceType_Instance, tag, c))
+    {
+      unoptimizedConstraints_[tag] = c.release();
+    }
+  }
+
+
+  static bool Match(const DicomMap& tags,
+                    const DicomTag& tag,
+                    const IFindConstraint& constraint)
+  {
+    const DicomValue* value = tags.TestAndGetValue(tag);
+
+    if (value == NULL ||
+        value->IsNull() ||
+        value->IsBinary())
+    {
+      return false;
+    }
+    else
+    {
+      return constraint.Match(value->GetContent());
+    }
+  }
+
+
+  void LookupResource::Level::Apply(SetOfResources& candidates,
+                                    IDatabaseWrapper& database) const
+  {
+    // First, use the indexed identifiers
+    LookupIdentifierQuery query(level_);
+
+    for (Constraints::const_iterator it = identifiersConstraints_.begin(); 
+         it != identifiersConstraints_.end(); ++it)
+    {
+      it->second->Setup(query, it->first);
+    }
+
+    query.Apply(candidates, database);
+
+    /*{
+      query.Print(std::cout);
+      std::list<int64_t>  source;
+      candidates.Flatten(source);
+      printf("=> %d\n", source.size());
+      }*/
+
+    // Secondly, filter using the main DICOM tags
+    if (!identifiersConstraints_.empty() ||
+        !mainTagsConstraints_.empty())
+    {
+      std::list<int64_t>  source;
+      candidates.Flatten(source);
+      candidates.Clear();
+
+      std::list<int64_t>  filtered;
+      for (std::list<int64_t>::const_iterator candidate = source.begin(); 
+           candidate != source.end(); ++candidate)
+      {
+        DicomMap tags;
+        database.GetMainDicomTags(tags, *candidate);
+
+        bool match = true;
+
+        // Re-apply the identifier constraints, as their "Setup"
+        // method is less restrictive than their "Match" method
+        for (Constraints::const_iterator it = identifiersConstraints_.begin(); 
+             match && it != identifiersConstraints_.end(); ++it)
+        {
+          if (!Match(tags, it->first, *it->second))
+          {
+            match = false;
+          }
+        }
+
+        for (Constraints::const_iterator it = mainTagsConstraints_.begin(); 
+             match && it != mainTagsConstraints_.end(); ++it)
+        {
+          if (!Match(tags, it->first, *it->second))
+          {
+            match = false;
+          }
+        }
+
+        if (match)
+        {
+          filtered.push_back(*candidate);
+        }
+      }
+      
+      candidates.Intersect(filtered);
+    }
+  }
+
+
+
+  bool LookupResource::IsMatch(const DicomMap& dicom) const
+  {
+    for (Levels::const_iterator it = levels_.begin(); it != levels_.end(); ++it)
+    {
+      if (!it->second->IsMatch(dicom))
+      {
+        return false;
+      }
+    }
+
+    for (Constraints::const_iterator it = unoptimizedConstraints_.begin(); 
+         it != unoptimizedConstraints_.end(); ++it)
+    {
+      assert(it->second != NULL);
+
+      if (!DoesDicomMapMatch(dicom, it->first, *it->second))
+      {
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+
+  void LookupResource::ApplyLevel(SetOfResources& candidates,
+                                  ResourceType level,
+                                  IDatabaseWrapper& database) const
+  {
+    Levels::const_iterator it = levels_.find(level);
+    if (it != levels_.end())
+    {
+      it->second->Apply(candidates, database);
+    }
+
+    if (level == ResourceType_Study &&
+        modalitiesInStudy_.get() != NULL)
+    {
+      // There is a constraint on the "ModalitiesInStudy" DICOM
+      // extension. Check out whether one child series has one of the
+      // allowed modalities
+      std::list<int64_t> allStudies, matchingStudies;
+      candidates.Flatten(allStudies);
+ 
+      for (std::list<int64_t>::const_iterator
+             study = allStudies.begin(); study != allStudies.end(); ++study)
+      {
+        std::list<int64_t> childrenSeries;
+        database.GetChildrenInternalId(childrenSeries, *study);
+
+        for (std::list<int64_t>::const_iterator
+               series = childrenSeries.begin(); series != childrenSeries.end(); ++series)
+        {
+          DicomMap tags;
+          database.GetMainDicomTags(tags, *series);
+
+          const DicomValue* value = tags.TestAndGetValue(DICOM_TAG_MODALITY);
+          if (value != NULL &&
+              !value->IsNull() &&
+              !value->IsBinary())
+          {
+            if (modalitiesInStudy_->Match(value->GetContent()))
+            {
+              matchingStudies.push_back(*study);
+              break;
+            }
+          }
+        }
+      }
+
+      candidates.Intersect(matchingStudies);
+    }
+  }
+
+
+  void LookupResource::FindCandidates(std::list<int64_t>& result,
+                                      IDatabaseWrapper& database) const
+  {
+    ResourceType startingLevel;
+    if (level_ == ResourceType_Patient)
+    {
+      startingLevel = ResourceType_Patient;
+    }
+    else
+    {
+      startingLevel = ResourceType_Study;
+    }
+
+    SetOfResources candidates(database, startingLevel);
+
+    switch (level_)
+    {
+      case ResourceType_Patient:
+        ApplyLevel(candidates, ResourceType_Patient, database);
+        break;
+
+      case ResourceType_Study:
+        ApplyLevel(candidates, ResourceType_Study, database);
+        break;
+
+      case ResourceType_Series:
+        ApplyLevel(candidates, ResourceType_Study, database);
+        candidates.GoDown();
+        ApplyLevel(candidates, ResourceType_Series, database);
+        break;
+
+      case ResourceType_Instance:
+        ApplyLevel(candidates, ResourceType_Study, database);
+        candidates.GoDown();
+        ApplyLevel(candidates, ResourceType_Series, database);
+        candidates.GoDown();
+        ApplyLevel(candidates, ResourceType_Instance, database);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+    candidates.Flatten(result);
+  }
+
+
+  void LookupResource::SetModalitiesInStudy(const std::string& modalities)
+  {
+    modalitiesInStudy_.reset(new ListConstraint(true /* case sensitive */));
+    
+    std::vector<std::string> items;
+    Toolbox::TokenizeString(items, modalities, '\\');
+    
+    for (size_t i = 0; i < items.size(); i++)
+    {
+      modalitiesInStudy_->AddAllowedValue(items[i]);
+    }
+  }
+
+
+  void LookupResource::AddDicomConstraint(const DicomTag& tag,
+                                          const std::string& dicomQuery,
+                                          bool caseSensitive)
+  {
+    // http://www.itk.org/Wiki/DICOM_QueryRetrieve_Explained
+    // http://dicomiseasy.blogspot.be/2012/01/dicom-queryretrieve-part-i.html  
+    if (tag == DICOM_TAG_MODALITIES_IN_STUDY)
+    {
+      SetModalitiesInStudy(dicomQuery);
+    }
+    else 
+    {
+      Add(tag, IFindConstraint::ParseDicomConstraint(tag, dicomQuery, caseSensitive));
+    }
+  }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/DatabaseOptimizations/LookupResource.h	Wed Dec 19 16:31:10 2018 +0100
@@ -0,0 +1,115 @@
+/**
+ * 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 General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "ListConstraint.h"
+#include "SetOfResources.h"
+
+#include <memory>
+
+namespace Orthanc
+{
+  class LookupResource : public boost::noncopyable
+  {
+  private:
+    typedef std::map<DicomTag, IFindConstraint*>  Constraints;
+    
+    class Level
+    {
+    private:
+      ResourceType        level_;
+      std::set<DicomTag>  identifiers_;
+      std::set<DicomTag>  mainTags_;
+      Constraints         identifiersConstraints_;
+      Constraints         mainTagsConstraints_;
+
+    public:
+      Level(ResourceType level);
+
+      ~Level();
+
+      bool Add(const DicomTag& tag,
+               std::auto_ptr<IFindConstraint>& constraint);
+
+      void Apply(SetOfResources& candidates,
+                 IDatabaseWrapper& database) const;
+
+      bool IsMatch(const DicomMap& dicom) const;
+    };
+
+    typedef std::map<ResourceType, Level*>  Levels;
+
+    ResourceType                    level_;
+    Levels                          levels_;
+    Constraints                     unoptimizedConstraints_;   // Constraints on non-main DICOM tags
+    std::auto_ptr<ListConstraint>   modalitiesInStudy_;
+
+    bool AddInternal(ResourceType level,
+                     const DicomTag& tag,
+                     std::auto_ptr<IFindConstraint>& constraint);
+
+    void ApplyLevel(SetOfResources& candidates,
+                    ResourceType level,
+                    IDatabaseWrapper& database) const;
+
+  public:
+    LookupResource(ResourceType level);
+
+    ~LookupResource();
+
+    ResourceType GetLevel() const
+    {
+      return level_;
+    }
+
+    void SetModalitiesInStudy(const std::string& modalities); 
+
+    void Add(const DicomTag& tag,
+             IFindConstraint* constraint);   // Takes ownership
+
+    void AddDicomConstraint(const DicomTag& tag,
+                            const std::string& dicomQuery,
+                            bool caseSensitive);
+
+    void FindCandidates(std::list<int64_t>& result,
+                        IDatabaseWrapper& database) const;
+
+    bool HasOnlyMainDicomTags() const
+    {
+      return unoptimizedConstraints_.empty();
+    }
+
+    bool IsMatch(const DicomMap& dicom) const;
+  };
+}
--- a/UnitTestsSources/DatabaseLookupTests.cpp	Wed Dec 19 14:35:55 2018 +0100
+++ b/UnitTestsSources/DatabaseLookupTests.cpp	Wed Dec 19 16:31:10 2018 +0100
@@ -44,28 +44,19 @@
 {
   {
     ASSERT_THROW(DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Equal, 
-                                        "HEL*LO", true), OrthancException);
+                                        "HEL*LO", true, true), OrthancException);
     ASSERT_THROW(DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Equal,
-                                        "HEL?LO", true), OrthancException);
+                                        "HEL?LO", true, true), OrthancException);
     ASSERT_THROW(DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Equal,
-                                        true), OrthancException);
+                                        true, true), OrthancException);
 
-    DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Equal, "HELLO", true);
+    DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Equal, "HELLO", true, true);
     ASSERT_TRUE(tag.IsMatch("HELLO"));
     ASSERT_FALSE(tag.IsMatch("hello"));
 
     ASSERT_TRUE(tag.IsCaseSensitive());
     ASSERT_EQ(ConstraintType_Equal, tag.GetConstraintType());
 
-    ASSERT_FALSE(tag.HasTagInfo());
-    ASSERT_THROW(tag.GetTagType(), OrthancException);
-    ASSERT_THROW(tag.GetLevel(), OrthancException);
-
-    tag.SetTagInfo(DicomTagType_Identifier, ResourceType_Series);
-    ASSERT_TRUE(tag.HasTagInfo());
-    ASSERT_EQ(DicomTagType_Identifier, tag.GetTagType());
-    ASSERT_EQ(ResourceType_Series, tag.GetLevel());
-
     DicomMap m;
     ASSERT_FALSE(tag.IsMatch(m));
     m.SetNullValue(DICOM_TAG_PATIENT_NAME);
@@ -77,7 +68,7 @@
   }
 
   {
-    DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Equal, "HELlo", false);
+    DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Equal, "HELlo", false, true);
     ASSERT_TRUE(tag.IsMatch("HELLO"));
     ASSERT_TRUE(tag.IsMatch("hello"));
 
@@ -85,7 +76,7 @@
   }
 
   {
-    DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Wildcard, "HE*L?O", true);
+    DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Wildcard, "HE*L?O", true, true);
     ASSERT_TRUE(tag.IsMatch("HELLO"));
     ASSERT_TRUE(tag.IsMatch("HELLLLLO"));
     ASSERT_TRUE(tag.IsMatch("HELxO"));
@@ -93,7 +84,7 @@
   }
 
   {
-    DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Wildcard, "HE*l?o", false);
+    DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Wildcard, "HE*l?o", false, true);
     ASSERT_TRUE(tag.IsMatch("HELLO"));
     ASSERT_TRUE(tag.IsMatch("HELLLLLO"));
     ASSERT_TRUE(tag.IsMatch("HELxO"));
@@ -104,21 +95,23 @@
   }
 
   {
-    DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_SmallerOrEqual, "123", true);
+    DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_SmallerOrEqual, "123", true, true);
     ASSERT_TRUE(tag.IsMatch("120"));
     ASSERT_TRUE(tag.IsMatch("123"));
     ASSERT_FALSE(tag.IsMatch("124"));
+    ASSERT_TRUE(tag.IsMandatory());
   }
 
   {
-    DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_GreaterOrEqual, "123", true);
+    DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_GreaterOrEqual, "123", true, false);
     ASSERT_FALSE(tag.IsMatch("122"));
     ASSERT_TRUE(tag.IsMatch("123"));
     ASSERT_TRUE(tag.IsMatch("124"));
+    ASSERT_FALSE(tag.IsMandatory());
   }
 
   {
-    DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_List, true);
+    DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_List, true, true);
     ASSERT_FALSE(tag.IsMatch("CT"));
     ASSERT_FALSE(tag.IsMatch("MR"));
 
@@ -137,7 +130,7 @@
   }
 
   {
-    DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_List, false);
+    DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_List, false, true);
 
     tag.AddValue("ct");
     tag.AddValue("mr");
@@ -155,20 +148,16 @@
 {
   {
     DatabaseLookup lookup;
-    lookup.AddDicomConstraint(DICOM_TAG_PATIENT_ID, "HELLO", true);
+    lookup.AddDicomConstraint(DICOM_TAG_PATIENT_ID, "HELLO", true, true);
     ASSERT_EQ(1u, lookup.GetConstraintsCount());
     ASSERT_EQ(ConstraintType_Equal, lookup.GetConstraint(0).GetConstraintType());
     ASSERT_EQ("HELLO", lookup.GetConstraint(0).GetValue());
     ASSERT_TRUE(lookup.GetConstraint(0).IsCaseSensitive());
-
-    ASSERT_TRUE(lookup.GetConstraint(0).HasTagInfo());
-    ASSERT_EQ(DicomTagType_Identifier, lookup.GetConstraint(0).GetTagType());
-    ASSERT_EQ(ResourceType_Patient, lookup.GetConstraint(0).GetLevel());
   }
 
   {
     DatabaseLookup lookup;
-    lookup.AddDicomConstraint(DICOM_TAG_PATIENT_ID, "HELLO", false);
+    lookup.AddDicomConstraint(DICOM_TAG_PATIENT_ID, "HELLO", false, true);
     ASSERT_EQ(1u, lookup.GetConstraintsCount());
 
     // This is *not* a PN VR => "false" above is *not* used
@@ -177,14 +166,14 @@
 
   {
     DatabaseLookup lookup;
-    lookup.AddDicomConstraint(DICOM_TAG_PATIENT_NAME, "HELLO", true);
+    lookup.AddDicomConstraint(DICOM_TAG_PATIENT_NAME, "HELLO", true, true);
     ASSERT_EQ(1u, lookup.GetConstraintsCount());
     ASSERT_TRUE(lookup.GetConstraint(0).IsCaseSensitive());
   }
 
   {
     DatabaseLookup lookup;
-    lookup.AddDicomConstraint(DICOM_TAG_PATIENT_NAME, "HELLO", false);
+    lookup.AddDicomConstraint(DICOM_TAG_PATIENT_NAME, "HELLO", false, true);
     ASSERT_EQ(1u, lookup.GetConstraintsCount());
 
     // This is a PN VR => "false" above is used
@@ -193,11 +182,7 @@
 
   {
     DatabaseLookup lookup;
-    lookup.AddDicomConstraint(DICOM_TAG_SERIES_DESCRIPTION, "2012-2016", false);
-
-    ASSERT_TRUE(lookup.GetConstraint(0).HasTagInfo());
-    ASSERT_EQ(DicomTagType_Main, lookup.GetConstraint(0).GetTagType());
-    ASSERT_EQ(ResourceType_Series, lookup.GetConstraint(0).GetLevel());
+    lookup.AddDicomConstraint(DICOM_TAG_SERIES_DESCRIPTION, "2012-2016", false, true);
 
     // This is not a data VR
     ASSERT_EQ(ConstraintType_Equal, lookup.GetConstraint(0).GetConstraintType());
@@ -205,7 +190,7 @@
 
   {
     DatabaseLookup lookup;
-    lookup.AddDicomConstraint(DICOM_TAG_PATIENT_BIRTH_DATE, "2012-2016", false);
+    lookup.AddDicomConstraint(DICOM_TAG_PATIENT_BIRTH_DATE, "2012-2016", false, true);
 
     // This is a data VR => range is effective
     ASSERT_EQ(2u, lookup.GetConstraintsCount());
@@ -221,7 +206,7 @@
 
   {
     DatabaseLookup lookup;
-    lookup.AddDicomConstraint(DICOM_TAG_PATIENT_BIRTH_DATE, "2012-", false);
+    lookup.AddDicomConstraint(DICOM_TAG_PATIENT_BIRTH_DATE, "2012-", false, true);
 
     ASSERT_EQ(1u, lookup.GetConstraintsCount());
     ASSERT_EQ(ConstraintType_GreaterOrEqual, lookup.GetConstraint(0).GetConstraintType());
@@ -230,7 +215,7 @@
 
   {
     DatabaseLookup lookup;
-    lookup.AddDicomConstraint(DICOM_TAG_PATIENT_BIRTH_DATE, "-2016", false);
+    lookup.AddDicomConstraint(DICOM_TAG_PATIENT_BIRTH_DATE, "-2016", false, true);
 
     ASSERT_EQ(1u, lookup.GetConstraintsCount());
     ASSERT_EQ(DICOM_TAG_PATIENT_BIRTH_DATE,  lookup.GetConstraint(0).GetTag());
@@ -240,11 +225,10 @@
 
   {
     DatabaseLookup lookup;
-    lookup.AddDicomConstraint(DICOM_TAG_MODALITIES_IN_STUDY, "CT\\MR", false);
+    lookup.AddDicomConstraint(DICOM_TAG_MODALITIES_IN_STUDY, "CT\\MR", false, true);
 
     ASSERT_EQ(1u, lookup.GetConstraintsCount());
     ASSERT_EQ(DICOM_TAG_MODALITY,  lookup.GetConstraint(0).GetTag());
-    ASSERT_EQ(ResourceType_Series, lookup.GetConstraint(0).GetLevel());
     ASSERT_EQ(ConstraintType_List, lookup.GetConstraint(0).GetConstraintType());
 
     const std::set<std::string>& values = lookup.GetConstraint(0).GetValues();
@@ -256,11 +240,10 @@
 
   {
     DatabaseLookup lookup;
-    lookup.AddDicomConstraint(DICOM_TAG_STUDY_DESCRIPTION, "CT\\MR", false);
+    lookup.AddDicomConstraint(DICOM_TAG_STUDY_DESCRIPTION, "CT\\MR", false, true);
 
     ASSERT_EQ(1u, lookup.GetConstraintsCount());
     ASSERT_EQ(DICOM_TAG_STUDY_DESCRIPTION, lookup.GetConstraint(0).GetTag());
-    ASSERT_EQ(ResourceType_Study, lookup.GetConstraint(0).GetLevel());
     ASSERT_EQ(ConstraintType_List, lookup.GetConstraint(0).GetConstraintType());
 
     const std::set<std::string>& values = lookup.GetConstraint(0).GetValues();
@@ -272,7 +255,7 @@
 
   {
     DatabaseLookup lookup;
-    lookup.AddDicomConstraint(DICOM_TAG_STUDY_DESCRIPTION, "HE*O", false);
+    lookup.AddDicomConstraint(DICOM_TAG_STUDY_DESCRIPTION, "HE*O", false, true);
 
     ASSERT_EQ(1u, lookup.GetConstraintsCount());
     ASSERT_EQ(ConstraintType_Wildcard, lookup.GetConstraint(0).GetConstraintType());
@@ -280,7 +263,7 @@
 
   {
     DatabaseLookup lookup;
-    lookup.AddDicomConstraint(DICOM_TAG_STUDY_DESCRIPTION, "HE?O", false);
+    lookup.AddDicomConstraint(DICOM_TAG_STUDY_DESCRIPTION, "HE?O", false, true);
 
     ASSERT_EQ(1u, lookup.GetConstraintsCount());
     ASSERT_EQ(ConstraintType_Wildcard, lookup.GetConstraint(0).GetConstraintType());
@@ -288,10 +271,9 @@
 
   {
     DatabaseLookup lookup;
-    lookup.AddDicomConstraint(DICOM_TAG_RELATED_FRAME_OF_REFERENCE_UID, "TEST", false);
-
-    ASSERT_TRUE(lookup.GetConstraint(0).HasTagInfo());
-    ASSERT_EQ(DicomTagType_Generic, lookup.GetConstraint(0).GetTagType());
-    ASSERT_EQ(ResourceType_Instance, lookup.GetConstraint(0).GetLevel());
+    lookup.AddDicomConstraint(DICOM_TAG_RELATED_FRAME_OF_REFERENCE_UID, "TEST", false, true);
+    lookup.AddDicomConstraint(DICOM_TAG_PATIENT_NAME, "TEST2", false, false);
+    ASSERT_TRUE(lookup.GetConstraint(0).IsMandatory());
+    ASSERT_FALSE(lookup.GetConstraint(1).IsMandatory());
   }
 }
--- a/UnitTestsSources/MultiThreadingTests.cpp	Wed Dec 19 14:35:55 2018 +0100
+++ b/UnitTestsSources/MultiThreadingTests.cpp	Wed Dec 19 16:31:10 2018 +0100
@@ -42,7 +42,7 @@
 #include "../Core/SerializationToolbox.h"
 #include "../Core/SystemToolbox.h"
 #include "../Core/Toolbox.h"
-#include "../OrthancServer/DatabaseWrapper.h"
+#include "../OrthancServer/SQLiteDatabaseWrapper.h"
 #include "../OrthancServer/ServerContext.h"
 #include "../OrthancServer/ServerJobs/LuaJobManager.h"
 #include "../OrthancServer/ServerJobs/OrthancJobUnserializer.h"
@@ -1281,7 +1281,7 @@
   {
   private:
     MemoryStorageArea              storage_;
-    DatabaseWrapper                db_;   // The SQLite DB is in memory
+    SQLiteDatabaseWrapper          db_;   // The SQLite DB is in memory
     std::auto_ptr<ServerContext>   context_;
     TimeoutDicomConnectionManager  manager_;
 
--- a/UnitTestsSources/ServerIndexTests.cpp	Wed Dec 19 14:35:55 2018 +0100
+++ b/UnitTestsSources/ServerIndexTests.cpp	Wed Dec 19 16:31:10 2018 +0100
@@ -37,10 +37,8 @@
 #include "../Core/FileStorage/FilesystemStorage.h"
 #include "../Core/FileStorage/MemoryStorageArea.h"
 #include "../Core/Logging.h"
-#include "../OrthancServer/DatabaseWrapper.h"
-#include "../OrthancServer/Search/LookupIdentifierQuery.h"
+#include "../OrthancServer/SQLiteDatabaseWrapper.h"
 #include "../OrthancServer/ServerContext.h"
-#include "../OrthancServer/ServerIndex.h"
 #include "../OrthancServer/ServerToolbox.h"
 
 #include <ctype.h>
@@ -117,7 +115,7 @@
       switch (GetParam())
       {
         case DatabaseWrapperClass_SQLite:
-          index_.reset(new DatabaseWrapper());
+          index_.reset(new SQLiteDatabaseWrapper());
           break;
 
         default:
@@ -141,7 +139,7 @@
       {
         case DatabaseWrapperClass_SQLite:
         {
-          DatabaseWrapper* sqlite = dynamic_cast<DatabaseWrapper*>(index_.get());
+          SQLiteDatabaseWrapper* sqlite = dynamic_cast<SQLiteDatabaseWrapper*>(index_.get());
           ASSERT_EQ(expected, sqlite->GetTableRecordCount(table));
           break;
         }
@@ -159,7 +157,7 @@
       {
         case DatabaseWrapperClass_SQLite:
         {
-          DatabaseWrapper* sqlite = dynamic_cast<DatabaseWrapper*>(index_.get());
+          SQLiteDatabaseWrapper* sqlite = dynamic_cast<SQLiteDatabaseWrapper*>(index_.get());
           ASSERT_FALSE(sqlite->GetParentPublicId(s, id));
           break;
         }
@@ -177,7 +175,7 @@
       {
         case DatabaseWrapperClass_SQLite:
         {
-          DatabaseWrapper* sqlite = dynamic_cast<DatabaseWrapper*>(index_.get());
+          SQLiteDatabaseWrapper* sqlite = dynamic_cast<SQLiteDatabaseWrapper*>(index_.get());
           ASSERT_TRUE(sqlite->GetParentPublicId(s, id));
           ASSERT_EQ(expected, s);
           break;
@@ -196,7 +194,7 @@
       {
         case DatabaseWrapperClass_SQLite:
         {
-          DatabaseWrapper* sqlite = dynamic_cast<DatabaseWrapper*>(index_.get());
+          SQLiteDatabaseWrapper* sqlite = dynamic_cast<SQLiteDatabaseWrapper*>(index_.get());
           sqlite->GetChildren(j, id);
           ASSERT_EQ(0u, j.size());
           break;
@@ -215,7 +213,7 @@
       {
         case DatabaseWrapperClass_SQLite:
         {
-          DatabaseWrapper* sqlite = dynamic_cast<DatabaseWrapper*>(index_.get());
+          SQLiteDatabaseWrapper* sqlite = dynamic_cast<SQLiteDatabaseWrapper*>(index_.get());
           sqlite->GetChildren(j, id);
           ASSERT_EQ(1u, j.size());
           ASSERT_EQ(expected, j.front());
@@ -237,7 +235,7 @@
       {
         case DatabaseWrapperClass_SQLite:
         {
-          DatabaseWrapper* sqlite = dynamic_cast<DatabaseWrapper*>(index_.get());
+          SQLiteDatabaseWrapper* sqlite = dynamic_cast<SQLiteDatabaseWrapper*>(index_.get());
           sqlite->GetChildren(j, id);
           ASSERT_EQ(2u, j.size());
           ASSERT_TRUE((expected1 == j.front() && expected2 == j.back()) ||
@@ -251,16 +249,42 @@
     }
 
 
-    void DoLookup(std::list<std::string>& result,
-                  ResourceType level,
-                  const DicomTag& tag,
-                  const std::string& value)
+    void DoLookupIdentifier(std::vector<std::string>& result,
+                            ResourceType level,
+                            const DicomTag& tag,
+                            ConstraintType type,
+                            const std::string& value)
     {
-      LookupIdentifierQuery query(level);
-      query.AddConstraint(tag, IdentifierConstraintType_Equal, value);
-      query.Apply(result, *index_);
+      assert(ServerToolbox::IsIdentifier(tag, level));
+      
+      DicomTagConstraint c(tag, type, value, true, true);
+      
+      std::vector<DatabaseConstraint> lookup;
+      lookup.push_back(DatabaseConstraint(c, level, DicomTagType_Identifier));
+      
+      index_->ApplyLookupResources(result, NULL, lookup, level, 0 /* no limit */);
     }
+    
 
+    void DoLookupIdentifier2(std::vector<std::string>& result,
+                             ResourceType level,
+                             const DicomTag& tag,
+                             ConstraintType type1,
+                             const std::string& value1,
+                             ConstraintType type2,
+                             const std::string& value2)
+    {
+      assert(ServerToolbox::IsIdentifier(tag, level));
+      
+      DicomTagConstraint c1(tag, type1, value1, true, true);
+      DicomTagConstraint c2(tag, type2, value2, true, true);
+      
+      std::vector<DatabaseConstraint> lookup;
+      lookup.push_back(DatabaseConstraint(c1, level, DicomTagType_Identifier));
+      lookup.push_back(DatabaseConstraint(c2, level, DicomTagType_Identifier));
+      
+      index_->ApplyLookupResources(result, NULL, lookup, level, 0 /* no limit */);
+    }
   };
 }
 
@@ -464,7 +488,15 @@
 
   CheckTableRecordCount(0, "Resources");
   CheckTableRecordCount(0, "AttachedFiles");
-  CheckTableRecordCount(2, "GlobalProperties");
+  CheckTableRecordCount(3, "GlobalProperties");
+
+  std::string tmp;
+  ASSERT_TRUE(index_->LookupGlobalProperty(tmp, GlobalProperty_DatabaseSchemaVersion));
+  ASSERT_EQ("6", tmp);
+  ASSERT_TRUE(index_->LookupGlobalProperty(tmp, GlobalProperty_FlushSleep));
+  ASSERT_EQ("World", tmp);
+  ASSERT_TRUE(index_->LookupGlobalProperty(tmp, GlobalProperty_GetTotalSizeIsFast));
+  ASSERT_EQ("1", tmp);
 
   ASSERT_EQ(3u, listener_->deletedFiles_.size());
   ASSERT_FALSE(std::find(listener_->deletedFiles_.begin(), 
@@ -676,7 +708,7 @@
 
   SystemToolbox::RemoveFile(path + "/index");
   FilesystemStorage storage(path);
-  DatabaseWrapper db;   // The SQLite DB is in memory
+  SQLiteDatabaseWrapper db;   // The SQLite DB is in memory
   db.Open();
   ServerContext context(db, storage, true /* running unit tests */, 10);
   context.SetupJobsEngine(true, false);
@@ -708,64 +740,48 @@
   index_->SetIdentifierTag(a[2], DICOM_TAG_STUDY_INSTANCE_UID, "0");
   index_->SetIdentifierTag(a[3], DICOM_TAG_SERIES_INSTANCE_UID, "0");
 
-  std::list<std::string> s;
+  std::vector<std::string> s;
 
-  DoLookup(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID, "0");
+  DoLookupIdentifier(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID, ConstraintType_Equal, "0");
   ASSERT_EQ(2u, s.size());
   ASSERT_TRUE(std::find(s.begin(), s.end(), "a") != s.end());
   ASSERT_TRUE(std::find(s.begin(), s.end(), "c") != s.end());
 
-  DoLookup(s, ResourceType_Series, DICOM_TAG_SERIES_INSTANCE_UID, "0");
+  DoLookupIdentifier(s, ResourceType_Series, DICOM_TAG_SERIES_INSTANCE_UID, ConstraintType_Equal, "0");
   ASSERT_EQ(1u, s.size());
   ASSERT_TRUE(std::find(s.begin(), s.end(), "d") != s.end());
 
-  DoLookup(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID, "1");
+  DoLookupIdentifier(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID, ConstraintType_Equal, "1");
   ASSERT_EQ(1u, s.size());
   ASSERT_TRUE(std::find(s.begin(), s.end(), "b") != s.end());
 
-  DoLookup(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID, "1");
+  DoLookupIdentifier(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID, ConstraintType_Equal, "1");
   ASSERT_EQ(1u, s.size());
   ASSERT_TRUE(std::find(s.begin(), s.end(), "b") != s.end());
 
-  DoLookup(s, ResourceType_Series, DICOM_TAG_SERIES_INSTANCE_UID, "1");
+  DoLookupIdentifier(s, ResourceType_Series, DICOM_TAG_SERIES_INSTANCE_UID, ConstraintType_Equal, "1");
+  ASSERT_EQ(0u, s.size());
+
+  DoLookupIdentifier(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID, ConstraintType_GreaterOrEqual, "0");
+  ASSERT_EQ(3u, s.size());
+
+  DoLookupIdentifier(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID, ConstraintType_GreaterOrEqual, "1");
+  ASSERT_EQ(1u, s.size());
+
+  DoLookupIdentifier(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID, ConstraintType_GreaterOrEqual, "2");
   ASSERT_EQ(0u, s.size());
 
-  {
-    LookupIdentifierQuery query(ResourceType_Study);
-    query.AddConstraint(DICOM_TAG_STUDY_INSTANCE_UID, IdentifierConstraintType_GreaterOrEqual, "0");
-    query.Apply(s, *index_);
-    ASSERT_EQ(3u, s.size());
-  }
-
-  {
-    LookupIdentifierQuery query(ResourceType_Study);
-    query.AddConstraint(DICOM_TAG_STUDY_INSTANCE_UID, IdentifierConstraintType_GreaterOrEqual, "0");
-    query.AddConstraint(DICOM_TAG_STUDY_INSTANCE_UID, IdentifierConstraintType_SmallerOrEqual, "0");
-    query.Apply(s, *index_);
-    ASSERT_EQ(2u, s.size());
-  }
+  DoLookupIdentifier2(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID,
+                      ConstraintType_GreaterOrEqual, "0", ConstraintType_SmallerOrEqual, "0");
+  ASSERT_EQ(2u, s.size());
 
-  {
-    LookupIdentifierQuery query(ResourceType_Study);
-    query.AddConstraint(DICOM_TAG_STUDY_INSTANCE_UID, IdentifierConstraintType_GreaterOrEqual, "1");
-    query.AddConstraint(DICOM_TAG_STUDY_INSTANCE_UID, IdentifierConstraintType_SmallerOrEqual, "1");
-    query.Apply(s, *index_);
-    ASSERT_EQ(1u, s.size());
-  }
+  DoLookupIdentifier2(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID,
+                      ConstraintType_GreaterOrEqual, "1", ConstraintType_SmallerOrEqual, "1");
+  ASSERT_EQ(1u, s.size());
 
-  {
-    LookupIdentifierQuery query(ResourceType_Study);
-    query.AddConstraint(DICOM_TAG_STUDY_INSTANCE_UID, IdentifierConstraintType_GreaterOrEqual, "1");
-    query.Apply(s, *index_);
-    ASSERT_EQ(1u, s.size());
-  }
-
-  {
-    LookupIdentifierQuery query(ResourceType_Study);
-    query.AddConstraint(DICOM_TAG_STUDY_INSTANCE_UID, IdentifierConstraintType_GreaterOrEqual, "2");
-    query.Apply(s, *index_);
-    ASSERT_EQ(0u, s.size());
-  }
+  DoLookupIdentifier2(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID,
+                      ConstraintType_GreaterOrEqual, "0", ConstraintType_SmallerOrEqual, "1");
+  ASSERT_EQ(3u, s.size());
 }
 
 
@@ -776,7 +792,7 @@
 
   SystemToolbox::RemoveFile(path + "/index");
   FilesystemStorage storage(path);
-  DatabaseWrapper db;   // The SQLite DB is in memory
+  SQLiteDatabaseWrapper db;   // The SQLite DB is in memory
   db.Open();
   ServerContext context(db, storage, true /* running unit tests */, 10);
   context.SetupJobsEngine(true, false);
@@ -850,7 +866,7 @@
 }
 
 
-TEST(LookupIdentifierQuery, NormalizeIdentifier)
+TEST(ServerIndex, NormalizeIdentifier)
 {
   ASSERT_EQ("H^L.LO", ServerToolbox::NormalizeIdentifier("   Hé^l.LO  %_  "));
   ASSERT_EQ("1.2.840.113619.2.176.2025", ServerToolbox::NormalizeIdentifier("   1.2.840.113619.2.176.2025  "));
@@ -864,7 +880,7 @@
     bool overwrite = (i == 0);
 
     MemoryStorageArea storage;
-    DatabaseWrapper db;   // The SQLite DB is in memory
+    SQLiteDatabaseWrapper db;   // The SQLite DB is in memory
     db.Open();
     ServerContext context(db, storage, true /* running unit tests */, 10);
     context.SetupJobsEngine(true, false);
--- a/UnitTestsSources/UnitTestsMain.cpp	Wed Dec 19 14:35:55 2018 +0100
+++ b/UnitTestsSources/UnitTestsMain.cpp	Wed Dec 19 16:31:10 2018 +0100
@@ -756,10 +756,29 @@
   ASSERT_EQ(MimeType_Xml, StringToMimeType("text/xml"));
   ASSERT_EQ(MimeType_Xml, StringToMimeType(EnumerationToString(MimeType_Xml)));
   ASSERT_THROW(StringToMimeType("nope"), OrthancException);
+
+  ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Patient, ResourceType_Patient));
+  ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Patient, ResourceType_Study));
+  ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Patient, ResourceType_Series));
+  ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Patient, ResourceType_Instance));
+
+  ASSERT_FALSE(IsResourceLevelAboveOrEqual(ResourceType_Study, ResourceType_Patient));
+  ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Study, ResourceType_Study));
+  ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Study, ResourceType_Series));
+  ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Study, ResourceType_Instance));
+
+  ASSERT_FALSE(IsResourceLevelAboveOrEqual(ResourceType_Series, ResourceType_Patient));
+  ASSERT_FALSE(IsResourceLevelAboveOrEqual(ResourceType_Series, ResourceType_Study));
+  ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Series, ResourceType_Series));
+  ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Series, ResourceType_Instance));
+
+  ASSERT_FALSE(IsResourceLevelAboveOrEqual(ResourceType_Instance, ResourceType_Patient));
+  ASSERT_FALSE(IsResourceLevelAboveOrEqual(ResourceType_Instance, ResourceType_Study));
+  ASSERT_FALSE(IsResourceLevelAboveOrEqual(ResourceType_Instance, ResourceType_Series));
+  ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Instance, ResourceType_Instance));
 }
 
 
-
 #if defined(__linux__) || defined(__OpenBSD__)
 #include <endian.h>
 #elif defined(__FreeBSD__)