changeset 3125:8296f0f75716 db-changes

integration mainline->db-changes
author Sebastien Jodogne <s.jodogne@gmail.com>
date Tue, 15 Jan 2019 21:11:44 +0100
parents c0d7aee8c3f8 (diff) 58ea4ef84c92 (current diff)
children e678a2b1a25b
files
diffstat 122 files changed, 7890 insertions(+), 5422 deletions(-) [+]
line wrap: on
line diff
--- a/CMakeLists.txt	Tue Jan 15 18:46:59 2019 +0100
+++ b/CMakeLists.txt	Tue Jan 15 21:11:44 2019 +0100
@@ -53,7 +53,13 @@
 #####################################################################
 
 set(ORTHANC_SERVER_SOURCES
-  OrthancServer/DatabaseWrapper.cpp
+  OrthancServer/Database/Compatibility/DatabaseLookup.cpp
+  OrthancServer/Database/Compatibility/ICreateInstance.cpp
+  OrthancServer/Database/Compatibility/IGetChildrenMetadata.cpp
+  OrthancServer/Database/Compatibility/ILookupResources.cpp
+  OrthancServer/Database/Compatibility/SetOfResources.cpp
+  OrthancServer/Database/ResourcesContent.cpp
+  OrthancServer/Database/SQLiteDatabaseWrapper.cpp
   OrthancServer/DicomInstanceOrigin.cpp
   OrthancServer/DicomInstanceToStore.cpp
   OrthancServer/ExportedResource.cpp
@@ -71,17 +77,11 @@
   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
-  OrthancServer/Search/WildcardConstraint.cpp
+  OrthancServer/Search/ISqlLookupFormatter.cpp
   OrthancServer/ServerContext.cpp
   OrthancServer/ServerEnumerations.cpp
   OrthancServer/ServerIndex.cpp
@@ -91,6 +91,7 @@
   OrthancServer/ServerJobs/LuaJobManager.cpp
   OrthancServer/ServerJobs/MergeStudyJob.cpp
   OrthancServer/ServerJobs/Operations/DeleteResourceOperation.cpp
+  OrthancServer/ServerJobs/Operations/DicomInstanceOperationValue.cpp
   OrthancServer/ServerJobs/Operations/ModifyInstanceOperation.cpp
   OrthancServer/ServerJobs/Operations/StorePeerOperation.cpp
   OrthancServer/ServerJobs/Operations/StoreScuOperation.cpp
@@ -127,6 +128,8 @@
 
 
 if (ENABLE_PLUGINS)
+  include_directories(${CMAKE_SOURCE_DIR}/Plugins/Include)
+
   list(APPEND ORTHANC_SERVER_SOURCES
     Plugins/Engine/OrthancPluginDatabase.cpp
     Plugins/Engine/OrthancPlugins.cpp
@@ -168,13 +171,16 @@
 #####################################################################
 
 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
+  CONFIGURATION_SAMPLE         ${CMAKE_CURRENT_SOURCE_DIR}/Resources/Configuration.json
+  DICOM_CONFORMANCE_STATEMENT  ${CMAKE_CURRENT_SOURCE_DIR}/Resources/DicomConformanceStatement.txt
+  FONT_UBUNTU_MONO_BOLD_16     ${CMAKE_CURRENT_SOURCE_DIR}/Resources/Fonts/UbuntuMonoBold-16.json
+  LUA_TOOLBOX                  ${CMAKE_CURRENT_SOURCE_DIR}/Resources/Toolbox.lua
+  PREPARE_DATABASE             ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/Database/PrepareDatabase.sql
+  UPGRADE_DATABASE_3_TO_4      ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/Database/Upgrade3To4.sql
+  UPGRADE_DATABASE_4_TO_5      ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/Database/Upgrade4To5.sql
+
+  INSTALL_TRACK_ATTACHMENTS_SIZE
+  ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/Database/InstallTrackAttachmentsSize.sql
   )
 
 if (STANDALONE_BUILD)
@@ -231,8 +237,6 @@
 endif()
 
 
-include_directories(${CMAKE_SOURCE_DIR}/Plugins/Include)
-
 add_definitions(
   -DORTHANC_BUILD_UNIT_TESTS=1
   -DORTHANC_ENABLE_LOGGING_PLUGIN=0
--- a/Core/Enumerations.cpp	Tue Jan 15 18:46:59 2019 +0100
+++ b/Core/Enumerations.cpp	Tue Jan 15 21:11:44 2019 +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	Tue Jan 15 18:46:59 2019 +0100
+++ b/Core/Enumerations.h	Tue Jan 15 21:11:44 2019 +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/NEWS	Tue Jan 15 18:46:59 2019 +0100
+++ b/NEWS	Tue Jan 15 21:11:44 2019 +0100
@@ -1,12 +1,23 @@
 Pending changes in the mainline
 ===============================
 
+REST API
+--------
+
+* More consistent handling of the "Last" field returned by the "/changes" URI
+
+Plugins
+-------
+
+* New primitives to speed up databases (custom index plugins)
+
 Maintenance
 -----------
 
 * Don't consider tags whose group is below 0x0008 in C-FIND SCP
 * Compatibility with DCMTK 3.6.4
 * Fix issue #21 (DICOM files missing after uploading with Firefox)
+* Fix issue #58 (Patient recycling order should be defined by their received last instance)
 * Fix issue #118 (Wording in Configuration.json regarding SynchronousCMove)
 * Fix issue #124 (GET /studies/ID/media fails for certain dicom file)
 * Fix issue #125 (Mongoose: /instances/{id} returns 500 on invalid HTTP Method)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Database/Compatibility/DatabaseLookup.cpp	Tue Jan 15 21:11:44 2019 +0100
@@ -0,0 +1,420 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 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 "DatabaseLookup.h"
+
+#include "../../../Core/OrthancException.h"
+#include "../../Search/DicomTagConstraint.h"
+#include "../../ServerToolbox.h"
+#include "SetOfResources.h"
+
+namespace Orthanc
+{
+  namespace Compatibility
+  {
+    namespace
+    {
+      // Anonymous namespace to avoid clashes between compiler modules
+      class MainTagsConstraints : boost::noncopyable
+      {
+      private:
+        std::vector<DicomTagConstraint*>  constraints_;
+
+      public:
+        ~MainTagsConstraints()
+        {
+          for (size_t i = 0; i < constraints_.size(); i++)
+          {
+            assert(constraints_[i] != NULL);
+            delete constraints_[i];
+          }
+        }
+
+        void Reserve(size_t n)
+        {
+          constraints_.reserve(n);
+        }
+
+        size_t GetSize() const
+        {
+          return constraints_.size();
+        }
+
+        DicomTagConstraint& GetConstraint(size_t i) const
+        {
+          if (i >= constraints_.size())
+          {
+            throw OrthancException(ErrorCode_ParameterOutOfRange);
+          }
+          else
+          {
+            assert(constraints_[i] != NULL);
+            return *constraints_[i];
+          }
+        }
+        
+        void Add(const DatabaseConstraint& constraint)
+        {
+          constraints_.push_back(new DicomTagConstraint(constraint));
+        }          
+      };
+    }
+    
+    
+    static void ApplyIdentifierConstraint(SetOfResources& candidates,
+                                          ILookupResources& compatibility,
+                                          const DatabaseConstraint& constraint,
+                                          ResourceType level)
+    {
+      std::list<int64_t> matches;
+
+      switch (constraint.GetConstraintType())
+      {
+        case ConstraintType_Equal:
+          compatibility.LookupIdentifier(matches, level, constraint.GetTag(),
+                                    IdentifierConstraintType_Equal, constraint.GetSingleValue());
+          break;
+          
+        case ConstraintType_SmallerOrEqual:
+          compatibility.LookupIdentifier(matches, level, constraint.GetTag(),
+                                    IdentifierConstraintType_SmallerOrEqual, constraint.GetSingleValue());
+          break;
+          
+        case ConstraintType_GreaterOrEqual:
+          compatibility.LookupIdentifier(matches, level, constraint.GetTag(),
+                                    IdentifierConstraintType_GreaterOrEqual, constraint.GetSingleValue());
+
+          break;
+          
+        case ConstraintType_Wildcard:
+          compatibility.LookupIdentifier(matches, level, constraint.GetTag(),
+                                    IdentifierConstraintType_Wildcard, constraint.GetSingleValue());
+
+          break;
+          
+        case ConstraintType_List:
+          for (size_t i = 0; i < constraint.GetValuesCount(); i++)
+          {
+            std::list<int64_t> tmp;
+            compatibility.LookupIdentifier(tmp, level, constraint.GetTag(),
+                                      IdentifierConstraintType_Wildcard, constraint.GetValue(i));
+            matches.splice(matches.end(), tmp);
+          }
+
+          break;
+          
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+
+      candidates.Intersect(matches);
+    }
+
+    
+    static void ApplyIdentifierRange(SetOfResources& candidates,
+                                     ILookupResources& compatibility,
+                                     const DatabaseConstraint& smaller,
+                                     const DatabaseConstraint& greater,
+                                     ResourceType level)
+    {
+      assert(smaller.GetConstraintType() == ConstraintType_SmallerOrEqual &&
+             greater.GetConstraintType() == ConstraintType_GreaterOrEqual &&
+             smaller.GetTag() == greater.GetTag() &&
+             ServerToolbox::IsIdentifier(smaller.GetTag(), level));
+
+      std::list<int64_t> matches;
+      compatibility.LookupIdentifierRange(matches, level, smaller.GetTag(),
+                                     greater.GetSingleValue(), smaller.GetSingleValue());
+      candidates.Intersect(matches);
+    }
+
+    
+    static void ApplyLevel(SetOfResources& candidates,
+                           IDatabaseWrapper& database,
+                           ILookupResources& compatibility,
+                           const std::vector<DatabaseConstraint>& lookup,
+                           ResourceType level)
+    {
+      typedef std::set<const DatabaseConstraint*>  SetOfConstraints;
+      typedef std::map<DicomTag, SetOfConstraints> Identifiers;
+
+      // (1) Select which constraints apply to this level, and split
+      // them between "identifier tags" constraints and "main DICOM
+      // tags" constraints
+
+      Identifiers       identifiers;
+      SetOfConstraints  mainTags;
+      
+      for (size_t i = 0; i < lookup.size(); i++)
+      {
+        if (lookup[i].GetLevel() == level)
+        {
+          if (lookup[i].IsIdentifier())
+          {
+            identifiers[lookup[i].GetTag()].insert(&lookup[i]);
+          }
+          else
+          {
+            mainTags.insert(&lookup[i]);
+          }
+        }
+      }
+
+      
+      // (2) Apply the constraints over the identifiers
+      
+      for (Identifiers::const_iterator it = identifiers.begin();
+           it != identifiers.end(); ++it)
+      {
+        // Check whether some range constraint over identifiers is
+        // present at this level
+        const DatabaseConstraint* smaller = NULL;
+        const DatabaseConstraint* greater = NULL;
+        
+        for (SetOfConstraints::const_iterator it2 = it->second.begin();
+             it2 != it->second.end(); ++it2)
+        {
+          assert(*it2 != NULL);
+        
+          if ((*it2)->GetConstraintType() == ConstraintType_SmallerOrEqual)
+          {
+            smaller = *it2;
+          }
+
+          if ((*it2)->GetConstraintType() == ConstraintType_GreaterOrEqual)
+          {
+            greater = *it2;
+          }
+        }
+
+        if (smaller != NULL &&
+            greater != NULL)
+        {
+          // There is a range constraint: Apply it, as it is more efficient
+          ApplyIdentifierRange(candidates, compatibility, *smaller, *greater, level);
+        }
+        else
+        {
+          smaller = NULL;
+          greater = NULL;
+        }
+
+        for (SetOfConstraints::const_iterator it2 = it->second.begin();
+             it2 != it->second.end(); ++it2)
+        {
+          // Check to avoid applying twice the range constraint
+          if (*it2 != smaller &&
+              *it2 != greater)
+          {
+            ApplyIdentifierConstraint(candidates, compatibility, **it2, level);
+          }
+        }
+      }
+
+
+      // (3) Apply the constraints over the main DICOM tags (no index
+      // here, so this is less efficient than filtering over the
+      // identifiers)
+      if (!mainTags.empty())
+      {
+        MainTagsConstraints c;
+        c.Reserve(mainTags.size());
+        
+        for (SetOfConstraints::const_iterator it = mainTags.begin();
+             it != mainTags.end(); ++it)
+        {
+          assert(*it != NULL);
+          c.Add(**it);
+        }
+
+        std::list<int64_t>  source;
+        candidates.Flatten(compatibility, 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;
+
+          for (size_t i = 0; i < c.GetSize(); i++)
+          {
+            if (!c.GetConstraint(i).IsMatch(tags))
+            {
+              match = false;
+              break;
+            }
+          }
+        
+          if (match)
+          {
+            filtered.push_back(*candidate);
+          }
+        }
+
+        candidates.Intersect(filtered);
+      }
+    }
+
+
+    static std::string GetOneInstance(IDatabaseWrapper& compatibility,
+                                      int64_t resource,
+                                      ResourceType level)
+    {
+      for (int i = level; i < ResourceType_Instance; i++)
+      {
+        assert(compatibility.GetResourceType(resource) == static_cast<ResourceType>(i));
+
+        std::list<int64_t> children;
+        compatibility.GetChildrenInternalId(children, resource);
+          
+        if (children.empty())
+        {
+          throw OrthancException(ErrorCode_Database);
+        }
+          
+        resource = children.front();
+      }
+
+      return compatibility.GetPublicId(resource);
+    }
+                           
+
+    void DatabaseLookup::ApplyLookupResources(std::list<std::string>& resourcesId,
+                                              std::list<std::string>* instancesId,
+                                              const std::vector<DatabaseConstraint>& lookup,
+                                              ResourceType queryLevel,
+                                              size_t limit)
+    {
+      // This is a re-implementation of
+      // "../../../Resources/Graveyard/DatabaseOptimizations/LookupResource.cpp"
+
+      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;
+        }
+      }
+
+      assert(upperLevel <= queryLevel &&
+             queryLevel <= lowerLevel);
+
+      SetOfResources candidates(database_, upperLevel);
+
+      for (int level = upperLevel; level <= lowerLevel; level++)
+      {
+        ApplyLevel(candidates, database_, compatibility_, lookup, static_cast<ResourceType>(level));
+
+        if (level != lowerLevel)
+        {
+          candidates.GoDown();
+        }
+      }
+
+      std::list<int64_t> resources;
+      candidates.Flatten(compatibility_, resources);
+
+      // Climb up, up to queryLevel
+
+      for (int level = lowerLevel; level > queryLevel; level--)
+      {
+        std::list<int64_t> parents;
+        for (std::list<int64_t>::const_iterator
+               it = resources.begin(); it != resources.end(); ++it)
+        {
+          int64_t parent;
+          if (database_.LookupParent(parent, *it))
+          {
+            parents.push_back(parent);
+          }
+        }
+
+        resources.swap(parents);
+      }
+
+      // Apply the limit, if given
+
+      if (limit != 0 &&
+          resources.size() > limit)
+      {
+        resources.resize(limit);
+      }
+
+      // Get the public ID of all the selected resources
+
+      size_t pos = 0;
+
+      for (std::list<int64_t>::const_iterator
+             it = resources.begin(); it != resources.end(); ++it, pos++)
+      {
+        assert(database_.GetResourceType(*it) == queryLevel);
+
+        const std::string resource = database_.GetPublicId(*it);
+        resourcesId.push_back(resource);
+
+        if (instancesId != NULL)
+        {
+          if (queryLevel == ResourceType_Instance)
+          {
+            // The resource is itself the instance
+            instancesId->push_back(resource);
+          }
+          else
+          {
+            // Collect one child instance for each of the selected resources
+            instancesId->push_back(GetOneInstance(database_, *it, queryLevel));
+          }
+        }
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Database/Compatibility/DatabaseLookup.h	Tue Jan 15 21:11:44 2019 +0100
@@ -0,0 +1,64 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 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 "ILookupResources.h"
+
+namespace Orthanc
+{
+  namespace Compatibility
+  {
+    class DatabaseLookup : public boost::noncopyable
+    {
+    private:
+      IDatabaseWrapper&  database_;
+      ILookupResources&  compatibility_;
+
+    public:
+      DatabaseLookup(IDatabaseWrapper& database,
+                     ILookupResources& compatibility) :
+        database_(database),
+        compatibility_(compatibility)
+      {
+      }
+
+      void ApplyLookupResources(std::list<std::string>& resourcesId,
+                                std::list<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/Database/Compatibility/ICreateInstance.cpp	Tue Jan 15 21:11:44 2019 +0100
@@ -0,0 +1,157 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 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 "ICreateInstance.h"
+
+#include "../../../Core/OrthancException.h"
+
+namespace Orthanc
+{
+  namespace Compatibility
+  {
+    bool ICreateInstance::Apply(ICreateInstance& database,
+                                IDatabaseWrapper::CreateInstanceResult& result,
+                                int64_t& instanceId,
+                                const std::string& hashPatient,
+                                const std::string& hashStudy,
+                                const std::string& hashSeries,
+                                const std::string& hashInstance)
+    {
+      {
+        ResourceType type;
+        int64_t tmp;
+        
+        if (database.LookupResource(tmp, type, hashInstance))
+        {
+          // The instance already exists
+          assert(type == ResourceType_Instance);
+          instanceId = tmp;
+          return false;
+        }
+      }
+
+      instanceId = database.CreateResource(hashInstance, ResourceType_Instance);
+
+      result.isNewPatient_ = false;
+      result.isNewStudy_ = false;
+      result.isNewSeries_ = false;
+      result.patientId_ = -1;
+      result.studyId_ = -1;
+      result.seriesId_ = -1;
+      
+      // Detect up to which level the patient/study/series/instance
+      // hierarchy must be created
+
+      {
+        ResourceType dummy;
+
+        if (database.LookupResource(result.seriesId_, dummy, hashSeries))
+        {
+          assert(dummy == ResourceType_Series);
+          // The patient, the study and the series already exist
+
+          bool ok = (database.LookupResource(result.patientId_, dummy, hashPatient) &&
+                     database.LookupResource(result.studyId_, dummy, hashStudy));
+          assert(ok);
+        }
+        else if (database.LookupResource(result.studyId_, dummy, hashStudy))
+        {
+          assert(dummy == ResourceType_Study);
+
+          // New series: The patient and the study already exist
+          result.isNewSeries_ = true;
+
+          bool ok = database.LookupResource(result.patientId_, dummy, hashPatient);
+          assert(ok);
+        }
+        else if (database.LookupResource(result.patientId_, dummy, hashPatient))
+        {
+          assert(dummy == ResourceType_Patient);
+
+          // New study and series: The patient already exist
+          result.isNewStudy_ = true;
+          result.isNewSeries_ = true;
+        }
+        else
+        {
+          // New patient, study and series: Nothing exists
+          result.isNewPatient_ = true;
+          result.isNewStudy_ = true;
+          result.isNewSeries_ = true;
+        }
+      }
+
+      // Create the series if needed
+      if (result.isNewSeries_)
+      {
+        result.seriesId_ = database.CreateResource(hashSeries, ResourceType_Series);
+      }
+
+      // Create the study if needed
+      if (result.isNewStudy_)
+      {
+        result.studyId_ = database.CreateResource(hashStudy, ResourceType_Study);
+      }
+
+      // Create the patient if needed
+      if (result.isNewPatient_)
+      {
+        result.patientId_ = database.CreateResource(hashPatient, ResourceType_Patient);
+      }
+
+      // Create the parent-to-child links
+      database.AttachChild(result.seriesId_, instanceId);
+
+      if (result.isNewSeries_)
+      {
+        database.AttachChild(result.studyId_, result.seriesId_);
+      }
+
+      if (result.isNewStudy_)
+      {
+        database.AttachChild(result.patientId_, result.studyId_);
+      }
+
+      database.TagMostRecentPatient(result.patientId_);
+      
+      // Sanity checks
+      assert(result.patientId_ != -1);
+      assert(result.studyId_ != -1);
+      assert(result.seriesId_ != -1);
+      assert(instanceId != -1);
+
+      return true;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Database/Compatibility/ICreateInstance.h	Tue Jan 15 21:11:44 2019 +0100
@@ -0,0 +1,66 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 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"
+
+namespace Orthanc
+{
+  namespace Compatibility
+  {
+    class ICreateInstance : public boost::noncopyable
+    {
+    public:
+      virtual bool LookupResource(int64_t& id,
+                                  ResourceType& type,
+                                  const std::string& publicId) = 0;
+
+      virtual int64_t CreateResource(const std::string& publicId,
+                                     ResourceType type) = 0;
+
+      virtual void AttachChild(int64_t parent,
+                               int64_t child) = 0;
+
+      virtual void TagMostRecentPatient(int64_t patientId) = 0;
+      
+      static bool Apply(ICreateInstance& database,
+                        IDatabaseWrapper::CreateInstanceResult& result,
+                        int64_t& instanceId,
+                        const std::string& patient,
+                        const std::string& study,
+                        const std::string& series,
+                        const std::string& instance);
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Database/Compatibility/IGetChildrenMetadata.cpp	Tue Jan 15 21:11:44 2019 +0100
@@ -0,0 +1,67 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 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 "IGetChildrenMetadata.h"
+
+namespace Orthanc
+{
+  namespace Compatibility
+  {
+    void IGetChildrenMetadata::Apply(IGetChildrenMetadata& database,
+                                     std::list<std::string>& target,
+                                     int64_t resourceId,
+                                     MetadataType metadata)
+    {
+      // This function comes from an optimization of
+      // "ServerIndex::GetSeriesStatus()" in Orthanc <= 1.5.1
+      // Loop over the instances of this series
+
+      target.clear();
+      
+      std::list<int64_t> children;
+      database.GetChildrenInternalId(children, resourceId);
+
+      std::set<int64_t> instances;
+      for (std::list<int64_t>::const_iterator 
+             it = children.begin(); it != children.end(); ++it)
+      {
+        std::string value;
+        if (database.LookupMetadata(value, *it, metadata))
+        {
+          target.push_back(value);
+        }
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Database/Compatibility/IGetChildrenMetadata.h	Tue Jan 15 21:11:44 2019 +0100
@@ -0,0 +1,61 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 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 "../../ServerEnumerations.h"
+
+#include <boost/noncopyable.hpp>
+#include <list>
+
+namespace Orthanc
+{
+  namespace Compatibility
+  {
+    class IGetChildrenMetadata : public boost::noncopyable
+    {
+    public:
+      virtual void GetChildrenInternalId(std::list<int64_t>& target,
+                                         int64_t id) = 0;
+
+      virtual bool LookupMetadata(std::string& target,
+                                  int64_t id,
+                                  MetadataType type) = 0;
+
+      static void Apply(IGetChildrenMetadata& database,
+                        std::list<std::string>& target,
+                        int64_t resourceId,
+                        MetadataType metadata);
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Database/Compatibility/ILookupResources.cpp	Tue Jan 15 21:11:44 2019 +0100
@@ -0,0 +1,56 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 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 "ILookupResources.h"
+
+#include "DatabaseLookup.h"
+
+namespace Orthanc
+{
+  namespace Compatibility
+  {
+    void ILookupResources::Apply(
+      IDatabaseWrapper& database,
+      ILookupResources& compatibility,
+      std::list<std::string>& resourcesId,
+      std::list<std::string>* instancesId,
+      const std::vector<DatabaseConstraint>& lookup,
+      ResourceType queryLevel,
+      size_t limit)
+    {
+      Compatibility::DatabaseLookup compat(database, compatibility);
+      compat.ApplyLookupResources(resourcesId, instancesId, lookup, queryLevel, limit);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Database/Compatibility/ILookupResources.h	Tue Jan 15 21:11:44 2019 +0100
@@ -0,0 +1,78 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 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"
+
+namespace Orthanc
+{
+  namespace Compatibility
+  {
+    /**
+     * This is a compatibility class that contains database primitives
+     * that were used in Orthanc <= 1.5.1, and that have been removed
+     * during the optimization of the database engine.
+     **/
+    class ILookupResources : public boost::noncopyable
+    {     
+    public:
+      virtual ~ILookupResources()
+      {
+      }
+      
+      virtual void GetAllInternalIds(std::list<int64_t>& target,
+                                     ResourceType resourceType) = 0;
+      
+      virtual void LookupIdentifier(std::list<int64_t>& result,
+                                    ResourceType level,
+                                    const DicomTag& tag,
+                                    IdentifierConstraintType type,
+                                    const std::string& value) = 0;
+ 
+      virtual void LookupIdentifierRange(std::list<int64_t>& result,
+                                         ResourceType level,
+                                         const DicomTag& tag,
+                                         const std::string& start,
+                                         const std::string& end) = 0;
+
+      static void Apply(IDatabaseWrapper& database,
+                        ILookupResources& compatibility,
+                        std::list<std::string>& resourcesId,
+                        std::list<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/Database/Compatibility/ISetResourcesContent.h	Tue Jan 15 21:11:44 2019 +0100
@@ -0,0 +1,68 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 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 "../ResourcesContent.h"
+
+namespace Orthanc
+{
+  namespace Compatibility
+  {
+    class ISetResourcesContent : public boost::noncopyable
+    {
+    public:
+      virtual ~ISetResourcesContent()
+      {
+      }
+      
+      virtual void SetMainDicomTag(int64_t id,
+                                   const DicomTag& tag,
+                                   const std::string& value) = 0;
+
+      virtual void SetIdentifierTag(int64_t id,
+                                    const DicomTag& tag,
+                                    const std::string& value) = 0;
+
+      virtual void SetMetadata(int64_t id,
+                               MetadataType type,
+                               const std::string& value) = 0;
+
+      static void Apply(ISetResourcesContent& that,
+                        const ResourcesContent& content)
+      {
+        content.Store(that);
+      }
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Database/Compatibility/SetOfResources.cpp	Tue Jan 15 21:11:44 2019 +0100
@@ -0,0 +1,161 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 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 "SetOfResources.h"
+
+#include "../../../Core/OrthancException.h"
+
+
+namespace Orthanc
+{
+  namespace Compatibility
+  {
+    void SetOfResources::Intersect(const std::list<int64_t>& resources)
+    {
+      if (resources_.get() == NULL)
+      {
+        resources_.reset(new Resources);
+
+        for (std::list<int64_t>::const_iterator
+               it = resources.begin(); it != resources.end(); ++it)
+        {
+          resources_->insert(*it);
+        }
+      }
+      else
+      {
+        std::auto_ptr<Resources> filtered(new Resources);
+
+        for (std::list<int64_t>::const_iterator
+               it = resources.begin(); it != resources.end(); ++it)
+        {
+          if (resources_->find(*it) != resources_->end())
+          {
+            filtered->insert(*it);
+          }
+        }
+
+        resources_ = filtered;
+      }
+    }
+
+
+    void SetOfResources::GoDown()
+    {
+      if (level_ == ResourceType_Instance)
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+
+      if (resources_.get() != NULL)
+      {
+        std::auto_ptr<Resources> children(new Resources);
+
+        for (Resources::const_iterator it = resources_->begin(); 
+             it != resources_->end(); ++it)
+        {
+          std::list<int64_t> tmp;
+          database_.GetChildrenInternalId(tmp, *it);
+
+          for (std::list<int64_t>::const_iterator
+                 child = tmp.begin(); child != tmp.end(); ++child)
+          {
+            children->insert(*child);
+          }
+        }
+
+        resources_ = children;
+      }
+
+      switch (level_)
+      {
+        case ResourceType_Patient:
+          level_ = ResourceType_Study;
+          break;
+
+        case ResourceType_Study:
+          level_ = ResourceType_Series;
+          break;
+
+        case ResourceType_Series:
+          level_ = ResourceType_Instance;
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+
+    void SetOfResources::Flatten(std::list<std::string>& result)
+    {
+      result.clear();
+      
+      if (resources_.get() == NULL)
+      {
+        // All the resources of this level are part of the filter
+        database_.GetAllPublicIds(result, level_);
+      }
+      else
+      {
+        for (Resources::const_iterator it = resources_->begin(); 
+             it != resources_->end(); ++it)
+        {
+          result.push_back(database_.GetPublicId(*it));
+        }
+      }
+    }
+
+
+    void SetOfResources::Flatten(ILookupResources& compatibility,
+                                 std::list<int64_t>& result)
+    {
+      result.clear();
+      
+      if (resources_.get() == NULL)
+      {
+        // All the resources of this level are part of the filter
+        compatibility.GetAllInternalIds(result, level_);
+      }
+      else
+      {
+        for (Resources::const_iterator it = resources_->begin(); 
+             it != resources_->end(); ++it)
+        {
+          result.push_back(*it);
+        }
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Database/Compatibility/SetOfResources.h	Tue Jan 15 21:11:44 2019 +0100
@@ -0,0 +1,83 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 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 "ILookupResources.h"
+
+#include <set>
+#include <memory>
+
+namespace Orthanc
+{
+  namespace Compatibility
+  {
+    class SetOfResources : public boost::noncopyable
+    {
+    private:
+      typedef std::set<int64_t>  Resources;
+
+      IDatabaseWrapper&         database_;
+      ResourceType              level_;
+      std::auto_ptr<Resources>  resources_;
+    
+    public:
+      SetOfResources(IDatabaseWrapper& database,
+                     ResourceType level) : 
+        database_(database),
+        level_(level)
+      {
+      }
+
+      ResourceType GetLevel() const
+      {
+        return level_;
+      }
+
+      void Intersect(const std::list<int64_t>& resources);
+
+      void GoDown();
+
+      void Flatten(ILookupResources& compatibility,
+                   std::list<int64_t>& result);
+
+      void Flatten(std::list<std::string>& result);
+
+      void Clear()
+      {
+        resources_.reset(NULL);
+      }
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Database/IDatabaseListener.h	Tue Jan 15 21:11:44 2019 +0100
@@ -0,0 +1,57 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 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 "../ServerEnumerations.h"
+#include "../ServerIndexChange.h"
+
+#include <string>
+
+namespace Orthanc
+{
+  class IDatabaseListener : public boost::noncopyable
+  {
+  public:
+    virtual ~IDatabaseListener()
+    {
+    }
+
+    virtual void SignalRemainingAncestor(ResourceType parentType,
+                                         const std::string& publicId) = 0;
+
+    virtual void SignalFileDeleted(const FileInfo& info) = 0;
+
+    virtual void SignalChange(const ServerIndexChange& change) = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Database/IDatabaseWrapper.h	Tue Jan 15 21:11:44 2019 +0100
@@ -0,0 +1,249 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 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 "../../Core/DicomFormat/DicomMap.h"
+#include "../../Core/FileStorage/FileInfo.h"
+#include "../../Core/FileStorage/IStorageArea.h"
+#include "../../Core/SQLite/ITransaction.h"
+
+#include "../ExportedResource.h"
+#include "IDatabaseListener.h"
+
+#include <list>
+#include <boost/noncopyable.hpp>
+
+namespace Orthanc
+{
+  class DatabaseConstraint;
+  class ResourcesContent;
+
+  
+  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;
+    };
+
+
+    struct CreateInstanceResult
+    {
+      bool     isNewPatient_;
+      bool     isNewStudy_;
+      bool     isNewSeries_;
+      int64_t  patientId_;
+      int64_t  studyId_;
+      int64_t  seriesId_;
+    };
+
+    virtual ~IDatabaseWrapper()
+    {
+    }
+
+    virtual void Open() = 0;
+
+    virtual void Close() = 0;
+
+    virtual void AddAttachment(int64_t id,
+                               const FileInfo& attachment) = 0;
+
+    virtual void ClearChanges() = 0;
+
+    virtual void ClearExportedResources() = 0;
+
+    virtual void DeleteAttachment(int64_t id,
+                                  FileContentType attachment) = 0;
+
+    virtual void DeleteMetadata(int64_t id,
+                                MetadataType type) = 0;
+
+    virtual void DeleteResource(int64_t id) = 0;
+
+    virtual void FlushToDisk() = 0;
+
+    virtual bool HasFlushToDisk() const = 0;
+
+    virtual void GetAllMetadata(std::map<MetadataType, std::string>& target,
+                                int64_t id) = 0;
+
+    virtual void GetAllPublicIds(std::list<std::string>& target,
+                                 ResourceType resourceType) = 0;
+
+    virtual void GetAllPublicIds(std::list<std::string>& target,
+                                 ResourceType resourceType,
+                                 size_t since,
+                                 size_t limit) = 0;
+
+    virtual void GetChanges(std::list<ServerIndexChange>& target /*out*/,
+                            bool& done /*out*/,
+                            int64_t since,
+                            uint32_t maxResults) = 0;
+
+    virtual void GetChildrenInternalId(std::list<int64_t>& target,
+                                       int64_t id) = 0;
+
+    virtual void GetChildrenPublicId(std::list<std::string>& target,
+                                     int64_t id) = 0;
+
+    virtual void GetExportedResources(std::list<ExportedResource>& target /*out*/,
+                                      bool& done /*out*/,
+                                      int64_t since,
+                                      uint32_t maxResults) = 0;
+
+    virtual void GetLastChange(std::list<ServerIndexChange>& target /*out*/) = 0;
+
+    virtual void GetLastExportedResource(std::list<ExportedResource>& target /*out*/) = 0;
+
+    virtual void GetMainDicomTags(DicomMap& map,
+                                  int64_t id) = 0;
+
+    virtual std::string GetPublicId(int64_t resourceId) = 0;
+
+    virtual uint64_t GetResourceCount(ResourceType resourceType) = 0;
+
+    virtual ResourceType GetResourceType(int64_t resourceId) = 0;
+
+    virtual uint64_t GetTotalCompressedSize() = 0;
+    
+    virtual uint64_t GetTotalUncompressedSize() = 0;
+
+    virtual bool IsExistingResource(int64_t internalId) = 0;
+
+    virtual bool IsProtectedPatient(int64_t internalId) = 0;
+
+    virtual void ListAvailableMetadata(std::list<MetadataType>& target,
+                                       int64_t id) = 0;
+
+    virtual void ListAvailableAttachments(std::list<FileContentType>& target,
+                                          int64_t id) = 0;
+
+    virtual void LogChange(int64_t internalId,
+                           const ServerIndexChange& change) = 0;
+
+    virtual void LogExportedResource(const ExportedResource& resource) = 0;
+    
+    virtual bool LookupAttachment(FileInfo& attachment,
+                                  int64_t id,
+                                  FileContentType contentType) = 0;
+
+    virtual bool LookupGlobalProperty(std::string& target,
+                                      GlobalProperty property) = 0;
+
+    virtual bool LookupMetadata(std::string& target,
+                                int64_t id,
+                                MetadataType type) = 0;
+
+    virtual bool LookupParent(int64_t& parentId,
+                              int64_t resourceId) = 0;
+
+    virtual bool LookupResource(int64_t& id,
+                                ResourceType& type,
+                                const std::string& publicId) = 0;
+
+    virtual bool SelectPatientToRecycle(int64_t& internalId) = 0;
+
+    virtual bool SelectPatientToRecycle(int64_t& internalId,
+                                        int64_t patientIdToAvoid) = 0;
+
+    virtual void SetGlobalProperty(GlobalProperty property,
+                                   const std::string& value) = 0;
+
+    virtual void ClearMainDicomTags(int64_t id) = 0;
+
+    virtual void SetMetadata(int64_t id,
+                             MetadataType type,
+                             const std::string& value) = 0;
+
+    virtual void SetProtectedPatient(int64_t internalId, 
+                                     bool isProtected) = 0;
+
+    virtual ITransaction* StartTransaction() = 0;
+
+    virtual void SetListener(IDatabaseListener& listener) = 0;
+
+    virtual unsigned int GetDatabaseVersion() = 0;
+
+    virtual void Upgrade(unsigned int targetVersion,
+                         IStorageArea& storageArea) = 0;
+
+
+    /**
+     * Primitives introduced in Orthanc 1.5.2
+     **/
+    
+    virtual bool IsDiskSizeAbove(uint64_t threshold) = 0;
+    
+    virtual void ApplyLookupResources(std::list<std::string>& resourcesId,
+                                      std::list<std::string>* instancesId, // Can be NULL if not needed
+                                      const std::vector<DatabaseConstraint>& lookup,
+                                      ResourceType queryLevel,
+                                      size_t limit) = 0;
+
+    // Returns "true" iff. the instance is new and has been inserted
+    // into the database. If "false" is returned, the content of
+    // "result" is undefined, but "instanceId" must be properly
+    // set. This method must also tag the parent patient as the most
+    // recent in the patient recycling order if it is not protected
+    // (so as to fix issue #58).
+    virtual bool CreateInstance(CreateInstanceResult& result, /* out */
+                                int64_t& instanceId,          /* out */
+                                const std::string& patient,
+                                const std::string& study,
+                                const std::string& series,
+                                const std::string& instance) = 0;
+
+    // It is guaranteed that the resources to be modified have no main
+    // DICOM tags, and no DICOM identifiers associated with
+    // them. However, some metadata might be already existing, and
+    // have to be overwritten.
+    virtual void SetResourcesContent(const ResourcesContent& content) = 0;
+
+    virtual void GetChildrenMetadata(std::list<std::string>& target,
+                                     int64_t resourceId,
+                                     MetadataType metadata) = 0;
+
+    virtual int64_t GetLastChangeIndex() = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Database/InstallTrackAttachmentsSize.sql	Tue Jan 15 21:11:44 2019 +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;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Database/PrepareDatabase.sql	Tue Jan 15 21:11:44 2019 +0100
@@ -0,0 +1,126 @@
+CREATE TABLE GlobalProperties(
+       property INTEGER PRIMARY KEY,
+       value TEXT
+       );
+
+CREATE TABLE Resources(
+       internalId INTEGER PRIMARY KEY AUTOINCREMENT,
+       resourceType INTEGER,
+       publicId TEXT,
+       parentId INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE
+       );
+
+CREATE TABLE MainDicomTags(
+       id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE,
+       tagGroup INTEGER,
+       tagElement INTEGER,
+       value TEXT,
+       PRIMARY KEY(id, tagGroup, tagElement)
+       );
+
+-- The following table was added in Orthanc 0.8.5 (database v5)
+CREATE TABLE DicomIdentifiers(
+       id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE,
+       tagGroup INTEGER,
+       tagElement INTEGER,
+       value TEXT,
+       PRIMARY KEY(id, tagGroup, tagElement)
+       );
+
+CREATE TABLE Metadata(
+       id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE,
+       type INTEGER,
+       value TEXT,
+       PRIMARY KEY(id, type)
+       );
+
+CREATE TABLE AttachedFiles(
+       id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE,
+       fileType INTEGER,
+       uuid TEXT,
+       compressedSize INTEGER,
+       uncompressedSize INTEGER,
+       compressionType INTEGER,
+       uncompressedMD5 TEXT,  -- New in Orthanc 0.7.3 (database v4)
+       compressedMD5 TEXT,    -- New in Orthanc 0.7.3 (database v4)
+       PRIMARY KEY(id, fileType)
+       );              
+
+CREATE TABLE Changes(
+       seq INTEGER PRIMARY KEY AUTOINCREMENT,
+       changeType INTEGER,
+       internalId INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE,
+       resourceType INTEGER,
+       date TEXT
+       );
+
+CREATE TABLE ExportedResources(
+       seq INTEGER PRIMARY KEY AUTOINCREMENT,
+       resourceType INTEGER,
+       publicId TEXT,
+       remoteModality TEXT,
+       patientId TEXT,
+       studyInstanceUid TEXT,
+       seriesInstanceUid TEXT,
+       sopInstanceUid TEXT,
+       date TEXT
+       ); 
+
+CREATE TABLE PatientRecyclingOrder(
+       seq INTEGER PRIMARY KEY AUTOINCREMENT,
+       patientId INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE
+       );
+
+CREATE INDEX ChildrenIndex ON Resources(parentId);
+CREATE INDEX PublicIndex ON Resources(publicId);
+CREATE INDEX ResourceTypeIndex ON Resources(resourceType);
+CREATE INDEX PatientRecyclingIndex ON PatientRecyclingOrder(patientId);
+
+CREATE INDEX MainDicomTagsIndex1 ON MainDicomTags(id);
+-- The 2 following indexes were removed in Orthanc 0.8.5 (database v5), to speed up
+-- CREATE INDEX MainDicomTagsIndex2 ON MainDicomTags(tagGroup, tagElement);
+-- CREATE INDEX MainDicomTagsIndexValues ON MainDicomTags(value COLLATE BINARY);
+
+-- The 3 following indexes were added in Orthanc 0.8.5 (database v5)
+CREATE INDEX DicomIdentifiersIndex1 ON DicomIdentifiers(id);
+CREATE INDEX DicomIdentifiersIndex2 ON DicomIdentifiers(tagGroup, tagElement);
+CREATE INDEX DicomIdentifiersIndexValues ON DicomIdentifiers(value COLLATE BINARY);
+
+CREATE INDEX ChangesIndex ON Changes(internalId);
+
+CREATE TRIGGER AttachedFileDeleted
+AFTER DELETE ON AttachedFiles
+BEGIN
+  SELECT SignalFileDeleted(old.uuid, old.fileType, old.uncompressedSize, 
+                           old.compressionType, old.compressedSize,
+                           -- These 2 arguments are new in Orthanc 0.7.3 (database v4)
+                           old.uncompressedMD5, old.compressedMD5);
+END;
+
+CREATE TRIGGER ResourceDeleted
+AFTER DELETE ON Resources
+BEGIN
+  SELECT SignalResourceDeleted(old.publicId, old.resourceType);  -- New in Orthanc 0.8.5 (db v5)
+  SELECT SignalRemainingAncestor(parent.publicId, parent.resourceType) 
+    FROM Resources AS parent WHERE internalId = old.parentId;
+END;
+
+-- Delete a parent resource when its unique child is deleted 
+CREATE TRIGGER ResourceDeletedParentCleaning
+AFTER DELETE ON Resources
+FOR EACH ROW WHEN (SELECT COUNT(*) FROM Resources WHERE parentId = old.parentId) = 0
+BEGIN
+  DELETE FROM Resources WHERE internalId = old.parentId;
+END;
+
+CREATE TRIGGER PatientAdded
+AFTER INSERT ON Resources
+FOR EACH ROW WHEN new.resourceType = 1  -- "1" corresponds to "ResourceType_Patient" in C++
+BEGIN
+  INSERT INTO PatientRecyclingOrder VALUES (NULL, new.internalId);
+END;
+
+
+-- Set the version of the database schema
+-- The "1" corresponds to the "GlobalProperty_DatabaseSchemaVersion" enumeration
+INSERT INTO GlobalProperties VALUES (1, "6");
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Database/ResourcesContent.cpp	Tue Jan 15 21:11:44 2019 +0100
@@ -0,0 +1,65 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 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 "ResourcesContent.h"
+
+#include "Compatibility/ISetResourcesContent.h"
+
+#include <cassert>
+
+
+namespace Orthanc
+{
+  void ResourcesContent::Store(Compatibility::ISetResourcesContent& compatibility) const
+  {
+    for (std::list<TagValue>::const_iterator
+           it = tags_.begin(); it != tags_.end(); ++it)
+    {
+      if (it->isIdentifier_)
+      {
+        compatibility.SetIdentifierTag(it->resourceId_, it->tag_,  it->value_);
+      }
+      else
+      {
+        compatibility.SetMainDicomTag(it->resourceId_, it->tag_,  it->value_);
+      }
+    }
+
+    for (std::list<Metadata>::const_iterator
+           it = metadata_.begin(); it != metadata_.end(); ++it)
+    {
+      compatibility.SetMetadata(it->resourceId_, it->metadata_,  it->value_);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Database/ResourcesContent.h	Tue Jan 15 21:11:44 2019 +0100
@@ -0,0 +1,134 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 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 "../../Core/DicomFormat/DicomMap.h"
+#include "../ServerEnumerations.h"
+
+#include <boost/noncopyable.hpp>
+#include <list>
+
+
+namespace Orthanc
+{
+  namespace Compatibility
+  {
+    class ISetResourcesContent;
+  }
+  
+  class ResourcesContent : public boost::noncopyable
+  {
+  public:
+    struct TagValue
+    {
+      int64_t      resourceId_;
+      bool         isIdentifier_;
+      DicomTag     tag_;
+      std::string  value_;
+
+      TagValue(int64_t resourceId,
+               bool isIdentifier,
+               const DicomTag& tag,
+               const std::string& value) :
+        resourceId_(resourceId),
+        isIdentifier_(isIdentifier),
+        tag_(tag),
+        value_(value)
+      {
+      }
+    };
+
+    struct Metadata
+    {
+      int64_t       resourceId_;
+      MetadataType  metadata_;
+      std::string   value_;
+
+      Metadata(int64_t  resourceId,
+               MetadataType metadata,
+               const std::string& value) :
+        resourceId_(resourceId),
+        metadata_(metadata),
+        value_(value)
+      {
+      }
+    };
+
+    typedef std::list<TagValue>  ListTags;
+    typedef std::list<Metadata>  ListMetadata;
+    
+  private:
+    ListTags       tags_;
+    ListMetadata   metadata_;
+
+  public:
+    void AddMainDicomTag(int64_t resourceId,
+                         const DicomTag& tag,
+                         const std::string& value)
+    {
+      tags_.push_back(TagValue(resourceId, false, tag, value));
+    }
+
+    void AddIdentifierTag(int64_t resourceId,
+                          const DicomTag& tag,
+                          const std::string& value)
+    {
+      tags_.push_back(TagValue(resourceId, true, tag, value));
+    }
+
+    void AddMetadata(int64_t resourceId,
+                     MetadataType metadata,
+                     const std::string& value)
+    {
+      metadata_.push_back(Metadata(resourceId, metadata, value));
+    }
+
+    void AddResource(int64_t resource,
+                     ResourceType level,
+                     const DicomMap& dicomSummary);
+
+    // WARNING: The database should be locked with a transaction!
+    void Store(Compatibility::ISetResourcesContent& target) const;
+
+    const ListTags& GetListTags() const
+    {
+      return tags_;
+    }
+
+    const ListMetadata& GetListMetadata() const
+    {
+      return metadata_;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Database/SQLiteDatabaseWrapper.cpp	Tue Jan 15 21:11:44 2019 +0100
@@ -0,0 +1,1351 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 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 "../Search/ISqlLookupFormatter.h"
+#include "../ServerToolbox.h"
+
+#include <EmbeddedResources.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();
+  }
+
+
+  int SQLiteDatabaseWrapper::GetGlobalIntegerProperty(GlobalProperty property,
+                                                      int defaultValue)
+  {
+    std::string tmp;
+
+    if (!LookupGlobalProperty(tmp, GlobalProperty_DatabasePatchLevel))
+    {
+      return defaultValue;
+    }
+    else
+    {
+      try
+      {
+        return boost::lexical_cast<int>(tmp);
+      }
+      catch (boost::bad_lexical_cast&)
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange,
+                               "Global property " + boost::lexical_cast<std::string>(property) +
+                               " should be an integer, but found: " + tmp);
+      }
+    }
+  }
+
+
+  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;");
+    
+    {
+      SQLite::Transaction t(db_);
+      t.Begin();
+
+      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);
+        }
+      }
+
+      t.Commit();
+    }
+
+    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::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();
+  }
+
+
+  bool SQLiteDatabaseWrapper::IsDiskSizeAbove(uint64_t threshold)
+  {
+    return GetTotalCompressedSize() > threshold;
+  }
+
+
+
+  class SQLiteDatabaseWrapper::LookupFormatter : public ISqlLookupFormatter
+  {
+  private:
+    std::list<std::string>  values_;
+
+  public:
+    virtual std::string GenerateParameter(const std::string& value)
+    {
+      values_.push_back(value);
+      return "?";
+    }
+    
+    virtual std::string FormatResourceType(ResourceType level)
+    {
+      return boost::lexical_cast<std::string>(level);
+    }
+
+    virtual std::string FormatWildcardEscape()
+    {
+      return "ESCAPE '\\'";
+    }
+
+    void Bind(SQLite::Statement& statement) const
+    {
+      size_t pos = 0;
+      
+      for (std::list<std::string>::const_iterator
+             it = values_.begin(); it != values_.end(); ++it, pos++)
+      {
+        statement.BindString(pos, *it);
+      }
+    }
+  };
+
+  
+  static void AnswerLookup(std::list<std::string>& resourcesId,
+                           std::list<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::list<std::string>& resourcesId,
+                                                   std::list<std::string>* instancesId,
+                                                   const std::vector<DatabaseConstraint>& lookup,
+                                                   ResourceType queryLevel,
+                                                   size_t limit)
+  {
+    LookupFormatter formatter;
+
+    std::string sql;
+    LookupFormatter::Apply(sql, formatter, lookup, queryLevel, limit);
+
+    sql = "CREATE TEMPORARY TABLE Lookup AS " + sql;
+    
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "DROP TABLE IF EXISTS Lookup");
+      s.Run();
+    }
+
+    {
+      SQLite::Statement statement(db_, sql);
+      formatter.Bind(statement);
+      statement.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));
+      }
+    }
+  }
+
+
+  int64_t SQLiteDatabaseWrapper::GetLastChangeIndex()
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                        "SELECT seq FROM sqlite_sequence WHERE name='Changes'");
+
+    if (s.Step())
+    {
+      int64_t c = s.ColumnInt(0);
+      assert(!s.Step());
+      return c;
+    }
+    else
+    {
+      // No change has been recorded so far in the database
+      return 0;
+    }
+  }
+
+
+  void SQLiteDatabaseWrapper::TagMostRecentPatient(int64_t patient)
+  {
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE,
+                          "DELETE FROM PatientRecyclingOrder WHERE patientId=?");
+      s.BindInt64(0, patient);
+      s.Run();
+
+      assert(db_.GetLastChangeCount() == 0 ||
+             db_.GetLastChangeCount() == 1);
+      
+      if (db_.GetLastChangeCount() == 0)
+      {
+        // The patient was protected, there was nothing to delete from the recycling order
+        return;
+      }
+    }
+
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE,
+                          "INSERT INTO PatientRecyclingOrder VALUES(NULL, ?)");
+      s.BindInt64(0, patient);
+      s.Run();
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Database/SQLiteDatabaseWrapper.h	Tue Jan 15 21:11:44 2019 +0100
@@ -0,0 +1,365 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 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 "Compatibility/ICreateInstance.h"
+#include "Compatibility/IGetChildrenMetadata.h"
+#include "Compatibility/ISetResourcesContent.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,
+    public Compatibility::ICreateInstance,
+    public Compatibility::IGetChildrenMetadata,
+    public Compatibility::ISetResourcesContent
+  {
+  private:
+    class Transaction;
+    class LookupFormatter;
+
+    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);
+
+    // Unused => could be removed
+    int GetGlobalIntegerProperty(GlobalProperty property,
+                                 int defaultValue);
+
+  public:
+    SQLiteDatabaseWrapper(const std::string& path);
+
+    SQLiteDatabaseWrapper();
+
+    virtual void Open()
+      ORTHANC_OVERRIDE;
+
+    virtual void Close()
+      ORTHANC_OVERRIDE
+    {
+      db_.Close();
+    }
+
+    virtual void SetListener(IDatabaseListener& listener)
+      ORTHANC_OVERRIDE;
+
+    virtual bool LookupParent(int64_t& parentId,
+                              int64_t resourceId)
+      ORTHANC_OVERRIDE;
+
+    virtual std::string GetPublicId(int64_t resourceId)
+      ORTHANC_OVERRIDE;
+
+    virtual ResourceType GetResourceType(int64_t resourceId)
+      ORTHANC_OVERRIDE;
+
+    virtual void DeleteResource(int64_t id)
+      ORTHANC_OVERRIDE;
+
+    virtual void GetChanges(std::list<ServerIndexChange>& target /*out*/,
+                            bool& done /*out*/,
+                            int64_t since,
+                            uint32_t maxResults)
+      ORTHANC_OVERRIDE;
+
+    virtual void GetLastChange(std::list<ServerIndexChange>& target /*out*/)
+      ORTHANC_OVERRIDE;
+
+    virtual IDatabaseWrapper::ITransaction* StartTransaction()
+      ORTHANC_OVERRIDE;
+
+    virtual void FlushToDisk()
+      ORTHANC_OVERRIDE
+    {
+      db_.FlushToDisk();
+    }
+
+    virtual bool HasFlushToDisk() const
+      ORTHANC_OVERRIDE
+    {
+      return true;
+    }
+
+    virtual void ClearChanges()
+      ORTHANC_OVERRIDE
+    {
+      ClearTable("Changes");
+    }
+
+    virtual void ClearExportedResources()
+      ORTHANC_OVERRIDE
+    {
+      ClearTable("ExportedResources");
+    }
+
+    virtual void GetAllMetadata(std::map<MetadataType, std::string>& target,
+                                int64_t id)
+      ORTHANC_OVERRIDE;
+
+    virtual unsigned int GetDatabaseVersion()
+      ORTHANC_OVERRIDE
+    {
+      return version_;
+    }
+
+    virtual void Upgrade(unsigned int targetVersion,
+                         IStorageArea& storageArea)
+      ORTHANC_OVERRIDE;
+
+
+    /**
+     * 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)
+      ORTHANC_OVERRIDE;
+
+    virtual bool LookupGlobalProperty(std::string& target,
+                                      GlobalProperty property)
+      ORTHANC_OVERRIDE;
+
+    virtual int64_t CreateResource(const std::string& publicId,
+                                   ResourceType type)
+      ORTHANC_OVERRIDE;
+
+    virtual bool LookupResource(int64_t& id,
+                                ResourceType& type,
+                                const std::string& publicId)
+      ORTHANC_OVERRIDE;
+
+    virtual void AttachChild(int64_t parent,
+                             int64_t child)
+      ORTHANC_OVERRIDE;
+
+    virtual void SetMetadata(int64_t id,
+                             MetadataType type,
+                             const std::string& value)
+      ORTHANC_OVERRIDE;
+
+    virtual void DeleteMetadata(int64_t id,
+                                MetadataType type)
+      ORTHANC_OVERRIDE;
+
+    virtual bool LookupMetadata(std::string& target,
+                                int64_t id,
+                                MetadataType type)
+      ORTHANC_OVERRIDE;
+
+    virtual void ListAvailableMetadata(std::list<MetadataType>& target,
+                                       int64_t id)
+      ORTHANC_OVERRIDE;
+
+    virtual void AddAttachment(int64_t id,
+                               const FileInfo& attachment)
+      ORTHANC_OVERRIDE;
+
+    virtual void DeleteAttachment(int64_t id,
+                                  FileContentType attachment)
+      ORTHANC_OVERRIDE;
+
+    virtual void ListAvailableAttachments(std::list<FileContentType>& target,
+                                          int64_t id)
+      ORTHANC_OVERRIDE;
+
+    virtual bool LookupAttachment(FileInfo& attachment,
+                                  int64_t id,
+                                  FileContentType contentType)
+      ORTHANC_OVERRIDE;
+
+    virtual void ClearMainDicomTags(int64_t id)
+      ORTHANC_OVERRIDE;
+
+    virtual void SetMainDicomTag(int64_t id,
+                                 const DicomTag& tag,
+                                 const std::string& value)
+      ORTHANC_OVERRIDE;
+
+    virtual void SetIdentifierTag(int64_t id,
+                                  const DicomTag& tag,
+                                  const std::string& value)
+      ORTHANC_OVERRIDE;
+
+    virtual void GetMainDicomTags(DicomMap& map,
+                                  int64_t id)
+      ORTHANC_OVERRIDE;
+
+    virtual void GetChildrenPublicId(std::list<std::string>& target,
+                                     int64_t id)
+      ORTHANC_OVERRIDE;
+
+    virtual void GetChildrenInternalId(std::list<int64_t>& target,
+                                       int64_t id)
+      ORTHANC_OVERRIDE;
+
+    virtual void LogChange(int64_t internalId,
+                           const ServerIndexChange& change)
+      ORTHANC_OVERRIDE;
+
+    virtual void LogExportedResource(const ExportedResource& resource)
+      ORTHANC_OVERRIDE;
+    
+    virtual void GetExportedResources(std::list<ExportedResource>& target /*out*/,
+                                      bool& done /*out*/,
+                                      int64_t since,
+                                      uint32_t maxResults)
+      ORTHANC_OVERRIDE;
+
+    virtual void GetLastExportedResource(std::list<ExportedResource>& target /*out*/)
+      ORTHANC_OVERRIDE;
+
+    virtual uint64_t GetTotalCompressedSize()
+      ORTHANC_OVERRIDE;
+    
+    virtual uint64_t GetTotalUncompressedSize()
+      ORTHANC_OVERRIDE;
+
+    virtual uint64_t GetResourceCount(ResourceType resourceType)
+      ORTHANC_OVERRIDE;
+
+    virtual void GetAllPublicIds(std::list<std::string>& target,
+                                 ResourceType resourceType)
+      ORTHANC_OVERRIDE;
+
+    virtual void GetAllPublicIds(std::list<std::string>& target,
+                                 ResourceType resourceType,
+                                 size_t since,
+                                 size_t limit)
+      ORTHANC_OVERRIDE;
+
+    virtual bool SelectPatientToRecycle(int64_t& internalId)
+      ORTHANC_OVERRIDE;
+
+    virtual bool SelectPatientToRecycle(int64_t& internalId,
+                                        int64_t patientIdToAvoid)
+      ORTHANC_OVERRIDE;
+
+    virtual bool IsProtectedPatient(int64_t internalId)
+      ORTHANC_OVERRIDE;
+
+    virtual void SetProtectedPatient(int64_t internalId, 
+                                     bool isProtected)
+      ORTHANC_OVERRIDE;
+
+    virtual bool IsExistingResource(int64_t internalId)
+      ORTHANC_OVERRIDE;
+
+    virtual bool IsDiskSizeAbove(uint64_t threshold)
+      ORTHANC_OVERRIDE;
+
+    virtual void ApplyLookupResources(std::list<std::string>& resourcesId,
+                                      std::list<std::string>* instancesId,
+                                      const std::vector<DatabaseConstraint>& lookup,
+                                      ResourceType queryLevel,
+                                      size_t limit)
+      ORTHANC_OVERRIDE;
+
+    virtual bool CreateInstance(CreateInstanceResult& result,
+                                int64_t& instanceId,
+                                const std::string& patient,
+                                const std::string& study,
+                                const std::string& series,
+                                const std::string& instance)
+      ORTHANC_OVERRIDE
+    {
+      return ICreateInstance::Apply
+        (*this, result, instanceId, patient, study, series, instance);
+    }
+
+    virtual void SetResourcesContent(const Orthanc::ResourcesContent& content)
+      ORTHANC_OVERRIDE
+    {
+      ISetResourcesContent::Apply(*this, content);
+    }
+
+    virtual void GetChildrenMetadata(std::list<std::string>& target,
+                                     int64_t resourceId,
+                                     MetadataType metadata)
+      ORTHANC_OVERRIDE
+    {
+      IGetChildrenMetadata::Apply(*this, target, resourceId, metadata);
+    }
+
+    virtual int64_t GetLastChangeIndex() ORTHANC_OVERRIDE;
+
+    virtual void TagMostRecentPatient(int64_t patient) ORTHANC_OVERRIDE;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Database/Upgrade3To4.sql	Tue Jan 15 21:11:44 2019 +0100
@@ -0,0 +1,24 @@
+-- This SQLite script updates the version of the Orthanc database from 3 to 4.
+
+-- Add 2 new columns at "AttachedFiles"
+
+ALTER TABLE AttachedFiles ADD COLUMN uncompressedMD5 TEXT;
+ALTER TABLE AttachedFiles ADD COLUMN compressedMD5 TEXT;
+
+-- Update the "AttachedFileDeleted" trigger
+
+DROP TRIGGER AttachedFileDeleted;
+
+CREATE TRIGGER AttachedFileDeleted
+AFTER DELETE ON AttachedFiles
+BEGIN
+  SELECT SignalFileDeleted(old.uuid, old.fileType, old.uncompressedSize, 
+                           old.compressionType, old.compressedSize,
+                           -- These 2 arguments are new in Orthanc 0.7.3 (database v4)
+                           old.uncompressedMD5, old.compressedMD5);
+END;
+
+-- Change the database version
+-- The "1" corresponds to the "GlobalProperty_DatabaseSchemaVersion" enumeration
+
+UPDATE GlobalProperties SET value="4" WHERE property=1;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Database/Upgrade4To5.sql	Tue Jan 15 21:11:44 2019 +0100
@@ -0,0 +1,66 @@
+-- This SQLite script updates the version of the Orthanc database from 4 to 5.
+
+
+-- Remove 2 indexes to speed up
+
+DROP INDEX MainDicomTagsIndex2;
+DROP INDEX MainDicomTagsIndexValues;
+
+
+-- Add a new table to index the DICOM identifiers
+
+CREATE TABLE DicomIdentifiers(
+       id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE,
+       tagGroup INTEGER,
+       tagElement INTEGER,
+       value TEXT,
+       PRIMARY KEY(id, tagGroup, tagElement)
+       );
+
+CREATE INDEX DicomIdentifiersIndex1 ON DicomIdentifiers(id);
+CREATE INDEX DicomIdentifiersIndex2 ON DicomIdentifiers(tagGroup, tagElement);
+CREATE INDEX DicomIdentifiersIndexValues ON DicomIdentifiers(value COLLATE BINARY);
+
+
+-- Migrate data from MainDicomTags to MainResourcesTags and MainInstancesTags
+
+INSERT INTO DicomIdentifiers SELECT * FROM MainDicomTags
+       WHERE ((tagGroup = 16 AND tagElement = 32) OR  -- PatientID (0x0010, 0x0020)
+              (tagGroup = 32 AND tagElement = 13) OR  -- StudyInstanceUID (0x0020, 0x000d)
+              (tagGroup = 8  AND tagElement = 80) OR  -- AccessionNumber (0x0008, 0x0050)
+              (tagGroup = 32 AND tagElement = 14) OR  -- SeriesInstanceUID (0x0020, 0x000e)
+              (tagGroup = 8  AND tagElement = 24));   -- SOPInstanceUID (0x0008, 0x0018)
+
+DELETE FROM MainDicomTags
+       WHERE ((tagGroup = 16 AND tagElement = 32) OR  -- PatientID (0x0010, 0x0020)
+              (tagGroup = 32 AND tagElement = 13) OR  -- StudyInstanceUID (0x0020, 0x000d)
+              (tagGroup = 8  AND tagElement = 80) OR  -- AccessionNumber (0x0008, 0x0050)
+              (tagGroup = 32 AND tagElement = 14) OR  -- SeriesInstanceUID (0x0020, 0x000e)
+              (tagGroup = 8  AND tagElement = 24));   -- SOPInstanceUID (0x0008, 0x0018)
+
+
+-- Upgrade the "ResourceDeleted" trigger
+
+DROP TRIGGER ResourceDeleted;
+DROP TRIGGER ResourceDeletedParentCleaning;
+
+CREATE TRIGGER ResourceDeleted
+AFTER DELETE ON Resources
+BEGIN
+  SELECT SignalResourceDeleted(old.publicId, old.resourceType);
+  SELECT SignalRemainingAncestor(parent.publicId, parent.resourceType) 
+    FROM Resources AS parent WHERE internalId = old.parentId;
+END;
+
+CREATE TRIGGER ResourceDeletedParentCleaning
+AFTER DELETE ON Resources
+FOR EACH ROW WHEN (SELECT COUNT(*) FROM Resources WHERE parentId = old.parentId) = 0
+BEGIN
+  DELETE FROM Resources WHERE internalId = old.parentId;
+END;
+
+
+-- Change the database version
+-- The "1" corresponds to the "GlobalProperty_DatabaseSchemaVersion" enumeration
+
+UPDATE GlobalProperties SET value="5" WHERE property=1;
--- a/OrthancServer/DatabaseWrapper.cpp	Tue Jan 15 18:46:59 2019 +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-2019 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	Tue Jan 15 18:46:59 2019 +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-2019 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/DicomInstanceOrigin.cpp	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/DicomInstanceOrigin.cpp	Tue Jan 15 21:11:44 2019 +0100
@@ -37,7 +37,6 @@
 #include "../Core/OrthancException.h"
 #include "../Core/SerializationToolbox.h"
 
-
 namespace Orthanc
 {
   void DicomInstanceOrigin::Format(Json::Value& result) const
--- a/OrthancServer/IDatabaseListener.h	Tue Jan 15 18:46:59 2019 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,56 +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-2019 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 <string>
-#include "ServerEnumerations.h"
-#include "ServerIndexChange.h"
-
-namespace Orthanc
-{
-  class IDatabaseListener
-  {
-  public:
-    virtual ~IDatabaseListener()
-    {
-    }
-
-    virtual void SignalRemainingAncestor(ResourceType parentType,
-                                         const std::string& publicId) = 0;
-
-    virtual void SignalFileDeleted(const FileInfo& info) = 0;
-
-    virtual void SignalChange(const ServerIndexChange& change) = 0;
-  };
-}
--- a/OrthancServer/IDatabaseWrapper.h	Tue Jan 15 18:46:59 2019 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,210 +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-2019 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 "../Core/DicomFormat/DicomMap.h"
-#include "../Core/SQLite/ITransaction.h"
-#include "../Core/FileStorage/IStorageArea.h"
-#include "../Core/FileStorage/FileInfo.h"
-#include "IDatabaseListener.h"
-#include "ExportedResource.h"
-
-#include <list>
-#include <boost/noncopyable.hpp>
-
-namespace Orthanc
-{
-  class IDatabaseWrapper : public boost::noncopyable
-  {
-  public:
-    virtual ~IDatabaseWrapper()
-    {
-    }
-
-    virtual void Open() = 0;
-
-    virtual void Close() = 0;
-
-    virtual void AddAttachment(int64_t id,
-                               const FileInfo& attachment) = 0;
-
-    virtual void AttachChild(int64_t parent,
-                             int64_t child) = 0;
-
-    virtual void ClearChanges() = 0;
-
-    virtual void ClearExportedResources() = 0;
-
-    virtual int64_t CreateResource(const std::string& publicId,
-                                   ResourceType type) = 0;
-
-    virtual void DeleteAttachment(int64_t id,
-                                  FileContentType attachment) = 0;
-
-    virtual void DeleteMetadata(int64_t id,
-                                MetadataType type) = 0;
-
-    virtual void DeleteResource(int64_t id) = 0;
-
-    virtual void FlushToDisk() = 0;
-
-    virtual bool HasFlushToDisk() const = 0;
-
-    virtual void GetAllMetadata(std::map<MetadataType, std::string>& target,
-                                int64_t id) = 0;
-
-    virtual void GetAllInternalIds(std::list<int64_t>& target,
-                                   ResourceType resourceType) = 0;
-
-    virtual void GetAllPublicIds(std::list<std::string>& target,
-                                 ResourceType resourceType) = 0;
-
-    virtual void GetAllPublicIds(std::list<std::string>& target,
-                                 ResourceType resourceType,
-                                 size_t since,
-                                 size_t limit) = 0;
-
-    virtual void GetChanges(std::list<ServerIndexChange>& target /*out*/,
-                            bool& done /*out*/,
-                            int64_t since,
-                            uint32_t maxResults) = 0;
-
-    virtual void GetChildrenInternalId(std::list<int64_t>& target,
-                                       int64_t id) = 0;
-
-    virtual void GetChildrenPublicId(std::list<std::string>& target,
-                                     int64_t id) = 0;
-
-    virtual void GetExportedResources(std::list<ExportedResource>& target /*out*/,
-                                      bool& done /*out*/,
-                                      int64_t since,
-                                      uint32_t maxResults) = 0;
-
-    virtual void GetLastChange(std::list<ServerIndexChange>& target /*out*/) = 0;
-
-    virtual void GetLastExportedResource(std::list<ExportedResource>& target /*out*/) = 0;
-
-    virtual void GetMainDicomTags(DicomMap& map,
-                                  int64_t id) = 0;
-
-    virtual std::string GetPublicId(int64_t resourceId) = 0;
-
-    virtual uint64_t GetResourceCount(ResourceType resourceType) = 0;
-
-    virtual ResourceType GetResourceType(int64_t resourceId) = 0;
-
-    virtual uint64_t GetTotalCompressedSize() = 0;
-    
-    virtual uint64_t GetTotalUncompressedSize() = 0;
-
-    virtual bool IsExistingResource(int64_t internalId) = 0;
-
-    virtual bool IsProtectedPatient(int64_t internalId) = 0;
-
-    virtual void ListAvailableMetadata(std::list<MetadataType>& target,
-                                       int64_t id) = 0;
-
-    virtual void ListAvailableAttachments(std::list<FileContentType>& target,
-                                          int64_t id) = 0;
-
-    virtual void LogChange(int64_t internalId,
-                           const ServerIndexChange& change) = 0;
-
-    virtual void LogExportedResource(const ExportedResource& resource) = 0;
-    
-    virtual bool LookupAttachment(FileInfo& attachment,
-                                  int64_t id,
-                                  FileContentType contentType) = 0;
-
-    virtual bool LookupGlobalProperty(std::string& target,
-                                      GlobalProperty property) = 0;
-
-    virtual void LookupIdentifier(std::list<int64_t>& result,
-                                  ResourceType level,
-                                  const DicomTag& tag,
-                                  IdentifierConstraintType type,
-                                  const std::string& value) = 0;
-
-    virtual void LookupIdentifierRange(std::list<int64_t>& result,
-                                       ResourceType level,
-                                       const DicomTag& tag,
-                                       const std::string& start,
-                                       const std::string& end) = 0;
-
-    virtual bool LookupMetadata(std::string& target,
-                                int64_t id,
-                                MetadataType type) = 0;
-
-    virtual bool LookupParent(int64_t& parentId,
-                              int64_t resourceId) = 0;
-
-    virtual bool LookupResource(int64_t& id,
-                                ResourceType& type,
-                                const std::string& publicId) = 0;
-
-    virtual bool SelectPatientToRecycle(int64_t& internalId) = 0;
-
-    virtual bool SelectPatientToRecycle(int64_t& internalId,
-                                        int64_t patientIdToAvoid) = 0;
-
-    virtual void SetGlobalProperty(GlobalProperty property,
-                                   const std::string& value) = 0;
-
-    virtual void ClearMainDicomTags(int64_t id) = 0;
-
-    virtual void SetMainDicomTag(int64_t id,
-                                 const DicomTag& tag,
-                                 const std::string& value) = 0;
-
-    virtual void SetIdentifierTag(int64_t id,
-                                  const DicomTag& tag,
-                                  const std::string& value) = 0;
-
-    virtual void SetMetadata(int64_t id,
-                             MetadataType type,
-                             const std::string& value) = 0;
-
-    virtual void SetProtectedPatient(int64_t internalId, 
-                                     bool isProtected) = 0;
-
-    virtual SQLite::ITransaction* StartTransaction() = 0;
-
-    virtual void SetListener(IDatabaseListener& listener) = 0;
-
-    virtual unsigned int GetDatabaseVersion() = 0;
-
-    virtual void Upgrade(unsigned int targetVersion,
-                         IStorageArea& storageArea) = 0;
-  };
-}
--- a/OrthancServer/OrthancFindRequestHandler.cpp	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/OrthancFindRequestHandler.cpp	Tue Jan 15 21:11:44 2019 +0100
@@ -35,11 +35,12 @@
 #include "OrthancFindRequestHandler.h"
 
 #include "../Core/DicomFormat/DicomArray.h"
-#include "../Core/Lua/LuaFunctionCall.h"
+#include "../Core/DicomParsing/FromDcmtkBridge.h"
 #include "../Core/Logging.h"
-#include "../Core/DicomParsing/FromDcmtkBridge.h"
+#include "../Core/Lua/LuaFunctionCall.h"
 #include "OrthancConfiguration.h"
-#include "Search/LookupResource.h"
+#include "Search/DatabaseLookup.h"
+#include "ServerContext.h"
 #include "ServerToolbox.h"
 
 #include <boost/regex.hpp> 
@@ -614,7 +615,7 @@
      * Build up the query object.
      **/
 
-    LookupResource lookup(level);
+    DatabaseLookup lookup;
 
     bool caseSensitivePN;
 
@@ -654,7 +655,7 @@
           sensitive = caseSensitivePN;
         }
 
-        lookup.AddDicomConstraint(tag, value, sensitive);
+        lookup.AddDicomConstraint(tag, value, sensitive, true /* mandatory */);
       }
       else
       {
@@ -672,7 +673,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/OrthancFindRequestHandler.h	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/OrthancFindRequestHandler.h	Tue Jan 15 21:11:44 2019 +0100
@@ -34,10 +34,10 @@
 
 #include "../Core/DicomNetworking/IFindRequestHandler.h"
 
-#include "ServerContext.h"
-
 namespace Orthanc
 {
+  class ServerContext;
+  
   class OrthancFindRequestHandler : public IFindRequestHandler
   {
   private:
--- a/OrthancServer/OrthancInitialization.cpp	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/OrthancInitialization.cpp	Tue Jan 15 21:11:44 2019 +0100
@@ -45,7 +45,7 @@
 #include "../Core/Logging.h"
 #include "../Core/OrthancException.h"
 
-#include "DatabaseWrapper.h"
+#include "Database/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/OrthancInitialization.h	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/OrthancInitialization.h	Tue Jan 15 21:11:44 2019 +0100
@@ -34,7 +34,7 @@
 #pragma once
 
 #include "../Core/FileStorage/IStorageArea.h"
-#include "IDatabaseWrapper.h"
+#include "Database/IDatabaseWrapper.h"
 
 namespace Orthanc
 {
--- a/OrthancServer/OrthancMoveRequestHandler.cpp	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/OrthancMoveRequestHandler.cpp	Tue Jan 15 21:11:44 2019 +0100
@@ -34,12 +34,14 @@
 #include "PrecompiledHeadersServer.h"
 #include "OrthancMoveRequestHandler.h"
 
-#include "OrthancConfiguration.h"
 #include "../../Core/DicomParsing/FromDcmtkBridge.h"
 #include "../Core/DicomFormat/DicomArray.h"
 #include "../Core/Logging.h"
+#include "OrthancConfiguration.h"
+#include "ServerContext.h"
 #include "ServerJobs/DicomModalityStoreJob.h"
 
+
 namespace Orthanc
 {
   namespace
@@ -226,7 +228,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 +237,7 @@
     }
     else
     {
-      publicId = ids.front();
+      publicId = ids[0];
       return true;
     }
   }
--- a/OrthancServer/OrthancMoveRequestHandler.h	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/OrthancMoveRequestHandler.h	Tue Jan 15 21:11:44 2019 +0100
@@ -33,10 +33,11 @@
 #pragma once
 
 #include "../Core/DicomNetworking/IMoveRequestHandler.h"
-#include "ServerContext.h"
 
 namespace Orthanc
 {
+  class ServerContext;
+  
   class OrthancMoveRequestHandler : public IMoveRequestHandler
   {
   private:
--- a/OrthancServer/OrthancRestApi/OrthancRestArchive.cpp	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/OrthancRestApi/OrthancRestArchive.cpp	Tue Jan 15 21:11:44 2019 +0100
@@ -35,9 +35,12 @@
 #include "OrthancRestApi.h"
 
 #include "../../Core/HttpServer/FilesystemHttpSender.h"
+#include "../../Core/OrthancException.h"
 #include "../../Core/SerializationToolbox.h"
+#include "../ServerContext.h"
 #include "../ServerJobs/ArchiveJob.h"
 
+
 namespace Orthanc
 {
   static const char* const KEY_RESOURCES = "Resources";
--- a/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp	Tue Jan 15 21:11:44 2019 +0100
@@ -34,11 +34,14 @@
 #include "../PrecompiledHeadersServer.h"
 #include "OrthancRestApi.h"
 
+#include "../../Core/Cache/SharedArchive.h"
 #include "../../Core/DicomParsing/FromDcmtkBridge.h"
 #include "../../Core/Logging.h"
 #include "../../Core/SerializationToolbox.h"
+
 #include "../OrthancConfiguration.h"
 #include "../QueryRetrieveHandler.h"
+#include "../ServerContext.h"
 #include "../ServerJobs/DicomModalityStoreJob.h"
 #include "../ServerJobs/DicomMoveScuJob.h"
 #include "../ServerJobs/OrthancPeerStoreJob.h"
--- a/OrthancServer/OrthancRestApi/OrthancRestResources.cpp	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/OrthancRestApi/OrthancRestResources.cpp	Tue Jan 15 21:11:44 2019 +0100
@@ -41,11 +41,13 @@
 #include "../../Core/Logging.h"
 #include "../DefaultDicomImageDecoder.h"
 #include "../OrthancConfiguration.h"
-#include "../Search/LookupResource.h"
+#include "../Search/DatabaseLookup.h"
 #include "../ServerContext.h"
 #include "../ServerToolbox.h"
 #include "../SliceOrdering.h"
 
+#include "../../Plugins/Engine/OrthancPlugins.h"
+
 
 namespace Orthanc
 {
@@ -1228,13 +1230,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]));
     }
   }
 
@@ -1401,9 +1402,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++)
@@ -1414,14 +1415,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);
     }
   }
 
--- a/OrthancServer/OrthancRestApi/OrthancRestSystem.cpp	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/OrthancRestApi/OrthancRestSystem.cpp	Tue Jan 15 21:11:44 2019 +0100
@@ -34,10 +34,10 @@
 #include "../PrecompiledHeadersServer.h"
 #include "OrthancRestApi.h"
 
+#include "../../Core/DicomParsing/FromDcmtkBridge.h"
+#include "../../Plugins/Engine/OrthancPlugins.h"
+#include "../../Plugins/Engine/PluginsManager.h"
 #include "../OrthancConfiguration.h"
-#include "../../Core/DicomParsing/FromDcmtkBridge.h"
-#include "../../Plugins/Engine/PluginsManager.h"
-#include "../../Plugins/Engine/OrthancPlugins.h"
 #include "../ServerContext.h"
 
 
--- a/OrthancServer/PrecompiledHeadersServer.h	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/PrecompiledHeadersServer.h	Tue Jan 15 21:11:44 2019 +0100
@@ -37,6 +37,6 @@
 
 #if ORTHANC_USE_PRECOMPILED_HEADERS == 1
 
-#include "ServerContext.h"
+#include <boost/thread.hpp>
 
 #endif
--- a/OrthancServer/PrepareDatabase.sql	Tue Jan 15 18:46:59 2019 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,126 +0,0 @@
-CREATE TABLE GlobalProperties(
-       property INTEGER PRIMARY KEY,
-       value TEXT
-       );
-
-CREATE TABLE Resources(
-       internalId INTEGER PRIMARY KEY AUTOINCREMENT,
-       resourceType INTEGER,
-       publicId TEXT,
-       parentId INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE
-       );
-
-CREATE TABLE MainDicomTags(
-       id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE,
-       tagGroup INTEGER,
-       tagElement INTEGER,
-       value TEXT,
-       PRIMARY KEY(id, tagGroup, tagElement)
-       );
-
--- The following table was added in Orthanc 0.8.5 (database v5)
-CREATE TABLE DicomIdentifiers(
-       id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE,
-       tagGroup INTEGER,
-       tagElement INTEGER,
-       value TEXT,
-       PRIMARY KEY(id, tagGroup, tagElement)
-       );
-
-CREATE TABLE Metadata(
-       id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE,
-       type INTEGER,
-       value TEXT,
-       PRIMARY KEY(id, type)
-       );
-
-CREATE TABLE AttachedFiles(
-       id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE,
-       fileType INTEGER,
-       uuid TEXT,
-       compressedSize INTEGER,
-       uncompressedSize INTEGER,
-       compressionType INTEGER,
-       uncompressedMD5 TEXT,  -- New in Orthanc 0.7.3 (database v4)
-       compressedMD5 TEXT,    -- New in Orthanc 0.7.3 (database v4)
-       PRIMARY KEY(id, fileType)
-       );              
-
-CREATE TABLE Changes(
-       seq INTEGER PRIMARY KEY AUTOINCREMENT,
-       changeType INTEGER,
-       internalId INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE,
-       resourceType INTEGER,
-       date TEXT
-       );
-
-CREATE TABLE ExportedResources(
-       seq INTEGER PRIMARY KEY AUTOINCREMENT,
-       resourceType INTEGER,
-       publicId TEXT,
-       remoteModality TEXT,
-       patientId TEXT,
-       studyInstanceUid TEXT,
-       seriesInstanceUid TEXT,
-       sopInstanceUid TEXT,
-       date TEXT
-       ); 
-
-CREATE TABLE PatientRecyclingOrder(
-       seq INTEGER PRIMARY KEY AUTOINCREMENT,
-       patientId INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE
-       );
-
-CREATE INDEX ChildrenIndex ON Resources(parentId);
-CREATE INDEX PublicIndex ON Resources(publicId);
-CREATE INDEX ResourceTypeIndex ON Resources(resourceType);
-CREATE INDEX PatientRecyclingIndex ON PatientRecyclingOrder(patientId);
-
-CREATE INDEX MainDicomTagsIndex1 ON MainDicomTags(id);
--- The 2 following indexes were removed in Orthanc 0.8.5 (database v5), to speed up
--- CREATE INDEX MainDicomTagsIndex2 ON MainDicomTags(tagGroup, tagElement);
--- CREATE INDEX MainDicomTagsIndexValues ON MainDicomTags(value COLLATE BINARY);
-
--- The 3 following indexes were added in Orthanc 0.8.5 (database v5)
-CREATE INDEX DicomIdentifiersIndex1 ON DicomIdentifiers(id);
-CREATE INDEX DicomIdentifiersIndex2 ON DicomIdentifiers(tagGroup, tagElement);
-CREATE INDEX DicomIdentifiersIndexValues ON DicomIdentifiers(value COLLATE BINARY);
-
-CREATE INDEX ChangesIndex ON Changes(internalId);
-
-CREATE TRIGGER AttachedFileDeleted
-AFTER DELETE ON AttachedFiles
-BEGIN
-  SELECT SignalFileDeleted(old.uuid, old.fileType, old.uncompressedSize, 
-                           old.compressionType, old.compressedSize,
-                           -- These 2 arguments are new in Orthanc 0.7.3 (database v4)
-                           old.uncompressedMD5, old.compressedMD5);
-END;
-
-CREATE TRIGGER ResourceDeleted
-AFTER DELETE ON Resources
-BEGIN
-  SELECT SignalResourceDeleted(old.publicId, old.resourceType);  -- New in Orthanc 0.8.5 (db v5)
-  SELECT SignalRemainingAncestor(parent.publicId, parent.resourceType) 
-    FROM Resources AS parent WHERE internalId = old.parentId;
-END;
-
--- Delete a parent resource when its unique child is deleted 
-CREATE TRIGGER ResourceDeletedParentCleaning
-AFTER DELETE ON Resources
-FOR EACH ROW WHEN (SELECT COUNT(*) FROM Resources WHERE parentId = old.parentId) = 0
-BEGIN
-  DELETE FROM Resources WHERE internalId = old.parentId;
-END;
-
-CREATE TRIGGER PatientAdded
-AFTER INSERT ON Resources
-FOR EACH ROW WHEN new.resourceType = 1  -- "1" corresponds to "ResourceType_Patient" in C++
-BEGIN
-  INSERT INTO PatientRecyclingOrder VALUES (NULL, new.internalId);
-END;
-
-
--- Set the version of the database schema
--- The "1" corresponds to the "GlobalProperty_DatabaseSchemaVersion" enumeration
-INSERT INTO GlobalProperties VALUES (1, "6");
--- a/OrthancServer/QueryRetrieveHandler.cpp	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/QueryRetrieveHandler.cpp	Tue Jan 15 21:11:44 2019 +0100
@@ -38,6 +38,8 @@
 
 #include "../Core/DicomParsing/FromDcmtkBridge.h"
 #include "../Core/Logging.h"
+#include "LuaScripting.h"
+#include "ServerContext.h"
 
 
 namespace Orthanc
--- a/OrthancServer/QueryRetrieveHandler.h	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/QueryRetrieveHandler.h	Tue Jan 15 21:11:44 2019 +0100
@@ -33,10 +33,13 @@
 
 #pragma once
 
-#include "ServerContext.h"
+#include "../Core/DicomNetworking/DicomFindAnswers.h"
+#include "../Core/DicomNetworking/RemoteModalityParameters.h"
 
 namespace Orthanc
 {
+  class ServerContext;
+  
   class QueryRetrieveHandler : public IDynamicObject
   {
   private:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Search/DatabaseConstraint.cpp	Tue Jan 15 21:11:44 2019 +0100
@@ -0,0 +1,245 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 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"
+
+
+namespace Orthanc
+{
+  namespace Plugins
+  {
+#if ORTHANC_ENABLE_PLUGINS == 1
+    OrthancPluginResourceType Convert(ResourceType type)
+    {
+      switch (type)
+      {
+        case ResourceType_Patient:
+          return OrthancPluginResourceType_Patient;
+
+        case ResourceType_Study:
+          return OrthancPluginResourceType_Study;
+
+        case ResourceType_Series:
+          return OrthancPluginResourceType_Series;
+
+        case ResourceType_Instance:
+          return OrthancPluginResourceType_Instance;
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+#endif
+
+
+#if ORTHANC_ENABLE_PLUGINS == 1
+    ResourceType Convert(OrthancPluginResourceType type)
+    {
+      switch (type)
+      {
+        case OrthancPluginResourceType_Patient:
+          return ResourceType_Patient;
+
+        case OrthancPluginResourceType_Study:
+          return ResourceType_Study;
+
+        case OrthancPluginResourceType_Series:
+          return ResourceType_Series;
+
+        case OrthancPluginResourceType_Instance:
+          return ResourceType_Instance;
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+#endif
+
+
+#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
+    OrthancPluginConstraintType Convert(ConstraintType constraint)
+    {
+      switch (constraint)
+      {
+        case ConstraintType_Equal:
+          return OrthancPluginConstraintType_Equal;
+
+        case ConstraintType_GreaterOrEqual:
+          return OrthancPluginConstraintType_GreaterOrEqual;
+
+        case ConstraintType_SmallerOrEqual:
+          return OrthancPluginConstraintType_SmallerOrEqual;
+
+        case ConstraintType_Wildcard:
+          return OrthancPluginConstraintType_Wildcard;
+
+        case ConstraintType_List:
+          return OrthancPluginConstraintType_List;
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+#endif    
+
+    
+#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
+    ConstraintType Convert(OrthancPluginConstraintType constraint)
+    {
+      switch (constraint)
+      {
+        case OrthancPluginConstraintType_Equal:
+          return ConstraintType_Equal;
+
+        case OrthancPluginConstraintType_GreaterOrEqual:
+          return ConstraintType_GreaterOrEqual;
+
+        case OrthancPluginConstraintType_SmallerOrEqual:
+          return ConstraintType_SmallerOrEqual;
+
+        case OrthancPluginConstraintType_Wildcard:
+          return ConstraintType_Wildcard;
+
+        case OrthancPluginConstraintType_List:
+          return ConstraintType_List;
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+#endif
+  }
+
+  DatabaseConstraint::DatabaseConstraint(ResourceType level,
+                                         const DicomTag& tag,
+                                         bool isIdentifier,
+                                         ConstraintType type,
+                                         const std::vector<std::string>& values,
+                                         bool caseSensitive,
+                                         bool mandatory) :
+    level_(level),
+    tag_(tag),
+    isIdentifier_(isIdentifier),
+    constraintType_(type),
+    values_(values),
+    caseSensitive_(caseSensitive),
+    mandatory_(mandatory)
+  {
+    if (type != ConstraintType_List &&
+        values_.size() != 1)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }      
+
+    
+#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
+  DatabaseConstraint::DatabaseConstraint(const OrthancPluginDatabaseConstraint& constraint) :
+    level_(Plugins::Convert(constraint.level)),
+    tag_(constraint.tagGroup, constraint.tagElement),
+    isIdentifier_(constraint.isIdentifierTag),
+    constraintType_(Plugins::Convert(constraint.type)),
+    caseSensitive_(constraint.isCaseSensitive),
+    mandatory_(constraint.isMandatory)
+  {
+    if (constraintType_ != ConstraintType_List &&
+        constraint.valuesCount != 1)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    values_.resize(constraint.valuesCount);
+
+    for (uint32_t i = 0; i < constraint.valuesCount; i++)
+    {
+      assert(constraint.values[i] != NULL);
+      values_[i].assign(constraint.values[i]);
+    }
+  }
+#endif
+    
+
+  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];
+    }
+  }
+
+
+#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
+  void DatabaseConstraint::EncodeForPlugins(OrthancPluginDatabaseConstraint& constraint,
+                                            std::vector<const char*>& tmpValues) const
+  {
+    memset(&constraint, 0, sizeof(constraint));
+    
+    tmpValues.resize(values_.size());
+
+    for (size_t i = 0; i < values_.size(); i++)
+    {
+      tmpValues[i] = values_[i].c_str();
+    }
+
+    constraint.level = Plugins::Convert(level_);
+    constraint.tagGroup = tag_.GetGroup();
+    constraint.tagElement = tag_.GetElement();
+    constraint.isIdentifierTag = isIdentifier_;
+    constraint.isCaseSensitive = caseSensitive_;
+    constraint.isMandatory = mandatory_;
+    constraint.type = Plugins::Convert(constraintType_);
+    constraint.valuesCount = values_.size();
+    constraint.values = (tmpValues.empty() ? NULL : &tmpValues[0]);
+  }
+#endif    
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Search/DatabaseConstraint.h	Tue Jan 15 21:11:44 2019 +0100
@@ -0,0 +1,144 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 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 "../../Core/DicomFormat/DicomMap.h"
+#include "../ServerEnumerations.h"
+
+#define ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT 0
+
+#if ORTHANC_ENABLE_PLUGINS == 1
+#  include <orthanc/OrthancCDatabasePlugin.h>
+#  if defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE)      // Macro introduced in 1.3.1
+#    if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 5, 2)
+#      undef  ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT
+#      define ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT 1
+#    endif
+#  endif
+#endif
+
+namespace Orthanc
+{
+  namespace Plugins
+  {
+#if ORTHANC_ENABLE_PLUGINS == 1
+    OrthancPluginResourceType Convert(ResourceType type);
+#endif
+
+#if ORTHANC_ENABLE_PLUGINS == 1
+    ResourceType Convert(OrthancPluginResourceType type);
+#endif
+
+#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
+    OrthancPluginConstraintType Convert(ConstraintType constraint);
+#endif
+
+#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
+    ConstraintType Convert(OrthancPluginConstraintType constraint);
+#endif
+  }
+
+
+  // This class is also used by the "orthanc-databases" project
+  class DatabaseConstraint
+  {
+  private:
+    ResourceType              level_;
+    DicomTag                  tag_;
+    bool                      isIdentifier_;
+    ConstraintType            constraintType_;
+    std::vector<std::string>  values_;
+    bool                      caseSensitive_;
+    bool                      mandatory_;
+
+  public:
+    DatabaseConstraint(ResourceType level,
+                       const DicomTag& tag,
+                       bool isIdentifier,
+                       ConstraintType type,
+                       const std::vector<std::string>& values,
+                       bool caseSensitive,
+                       bool mandatory);
+
+#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
+    DatabaseConstraint(const OrthancPluginDatabaseConstraint& constraint);
+#endif
+    
+    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_;
+    }
+
+    bool IsMatch(const DicomMap& dicom) const;
+
+#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
+    void EncodeForPlugins(OrthancPluginDatabaseConstraint& constraint,
+                          std::vector<const char*>& tmpValues) const;
+#endif    
+  };
+}
--- a/OrthancServer/Search/DatabaseLookup.cpp	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/Search/DatabaseLookup.cpp	Tue Jan 15 21:11:44 2019 +0100
@@ -36,53 +36,12 @@
 
 #include "../ServerToolbox.h"
 #include "../../Core/DicomParsing/FromDcmtkBridge.h"
+#include "../../Core/DicomParsing/ToDcmtkBridge.h"
+#include "../../Core/OrthancException.h"
+#include "../../Core/Toolbox.h"
 
 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 +75,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 +94,126 @@
   }
 
 
+  bool DatabaseLookup::IsMatch(DcmItem& item,
+                               Encoding encoding) const
+  {
+    for (size_t i = 0; i < constraints_.size(); i++)
+    {
+      assert(constraints_[i] != NULL);
+
+      const bool isOptionalConstraint = !constraints_[i]->IsMandatory();
+      const DcmTagKey tag = ToDcmtkBridge::Convert(constraints_[i]->GetTag());
+
+      DcmElement* element = NULL;
+      if (!item.findAndGetElement(tag, element).good())
+      {
+        return isOptionalConstraint;
+      }
+
+      if (element == NULL)
+      {
+        return false;
+      }
+
+      std::set<DicomTag> ignoreTagLength;
+      std::auto_ptr<DicomValue> value(FromDcmtkBridge::ConvertLeafElement
+                                      (*element, DicomToJsonFlags_None, 
+                                       0, encoding, ignoreTagLength));
+
+      // WARNING: Also modify "HierarchicalMatcher::Setup()" if modifying this code
+      if (value.get() == NULL ||
+          value->IsNull())
+      {
+        return isOptionalConstraint;        
+      }
+      else if (value->IsBinary() ||
+               !constraints_[i]->IsMatch(value->GetContent()))
+      {
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+
+  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 +253,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	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/Search/DatabaseLookup.h	Tue Jan 15 21:11:44 2019 +0100
@@ -35,49 +35,24 @@
 
 #include "DicomTagConstraint.h"
 
+class DcmItem;
+
 namespace Orthanc
 {
   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 +70,21 @@
 
     void AddConstraint(DicomTagConstraint* constraint);  // Takes ownership
 
-    bool IsMatch(const DicomMap& value);
+    bool IsMatch(const DicomMap& value) const;
+
+    bool IsMatch(DcmItem& item,
+                 Encoding encoding) 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	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/Search/DicomTagConstraint.cpp	Tue Jan 15 21:11:44 2019 +0100
@@ -34,8 +34,13 @@
 #include "../PrecompiledHeadersServer.h"
 #include "DicomTagConstraint.h"
 
+#if defined(ORTHANC_ENABLE_LUA) && ORTHANC_ENABLE_LUA != 0
+#  include "../ServerToolbox.h"
+#endif
+
 #include "../../Core/OrthancException.h"
 #include "../../Core/Toolbox.h"
+#include "DatabaseConstraint.h"
 
 #include <boost/regex.hpp>
 
@@ -94,32 +99,24 @@
   };
 
 
-  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
-    tag_(tag),
-    constraintType_(type),
-    caseSensitive_(caseSensitive)
+  void DicomTagConstraint::AssignSingleValue(const std::string& value)
   {
-    if (type == ConstraintType_Equal ||
-        type == ConstraintType_SmallerOrEqual ||
-        type == ConstraintType_GreaterOrEqual ||
-        type == ConstraintType_Wildcard)
-    {
-      values_.insert(value);
-    }
-    else
+    if (constraintType_ != ConstraintType_Wildcard &&
+        (value.find('*') != std::string::npos ||
+         value.find('?') != std::string::npos))
     {
       throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
 
-    if (type != ConstraintType_Wildcard &&
-        (value.find('*') != std::string::npos ||
-         value.find('?') != std::string::npos))
+    if (constraintType_ == ConstraintType_Equal ||
+        constraintType_ == ConstraintType_SmallerOrEqual ||
+        constraintType_ == ConstraintType_GreaterOrEqual ||
+        constraintType_ == ConstraintType_Wildcard)
+    {
+      values_.clear();
+      values_.insert(value);
+    }
+    else
     {
       throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
@@ -128,13 +125,26 @@
 
   DicomTagConstraint::DicomTagConstraint(const DicomTag& tag,
                                          ConstraintType type,
-                                         bool caseSensitive) :
-    hasTagInfo_(false),
-    tagType_(DicomTagType_Generic),  // Dummy initialization
-    level_(ResourceType_Patient),    // Dummy initialization
+                                         const std::string& value,
+                                         bool caseSensitive,
+                                         bool mandatory) :
     tag_(tag),
     constraintType_(type),
-    caseSensitive_(caseSensitive)
+    caseSensitive_(caseSensitive),
+    mandatory_(mandatory)
+  {
+    AssignSingleValue(value);
+  }
+
+
+  DicomTagConstraint::DicomTagConstraint(const DicomTag& tag,
+                                         ConstraintType type,
+                                         bool caseSensitive,
+                                         bool mandatory) :
+    tag_(tag),
+    constraintType_(type),
+    caseSensitive_(caseSensitive),
+    mandatory_(mandatory)
   {
     if (type != ConstraintType_List)
     {
@@ -143,37 +153,33 @@
   }
 
 
-  void DicomTagConstraint::SetTagInfo(DicomTagType tagType,
-                                      ResourceType level)
+  DicomTagConstraint::DicomTagConstraint(const DatabaseConstraint& constraint) :
+    tag_(constraint.GetTag()),
+    constraintType_(constraint.GetConstraintType()),
+    caseSensitive_(constraint.IsCaseSensitive()),
+    mandatory_(constraint.IsMandatory())
   {
-    hasTagInfo_ = true;
-    tagType_ = tagType;
-    level_ = level;
-  }
-
-
-  DicomTagType DicomTagConstraint::GetTagType() const
-  {
-    if (!hasTagInfo_)
+#if defined(ORTHANC_ENABLE_LUA) && ORTHANC_ENABLE_LUA != 0
+    assert(constraint.IsIdentifier() ==
+           ServerToolbox::IsIdentifier(constraint.GetTag(), constraint.GetLevel()));
+#endif
+    
+    if (constraint.IsIdentifier())
     {
+      // This conversion is only available for main DICOM tags, not for identifers
       throw OrthancException(ErrorCode_BadSequenceOfCalls);
     }
+    
+    if (constraintType_ == ConstraintType_List)
+    {
+      for (size_t i = 0; i < constraint.GetValuesCount(); i++)
+      {
+        AddValue(constraint.GetValue(i));
+      }
+    }
     else
     {
-      return tagType_;
-    }
-  }
-
-
-  const ResourceType DicomTagConstraint::GetLevel() const
-  {
-    if (!hasTagInfo_)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      return level_;
+      AssignSingleValue(constraint.GetSingleValue());
     }
   }
 
@@ -268,8 +274,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;
     }
@@ -278,4 +294,91 @@
       return IsMatch(tmp->GetContent());
     }
   }
+
+
+  std::string DicomTagConstraint::Format() const
+  {
+    switch (constraintType_)
+    {
+      case ConstraintType_Equal:
+        return tag_.Format() + " == " + GetValue();
+
+      case ConstraintType_SmallerOrEqual:
+        return tag_.Format() + " <= " + GetValue();
+
+      case ConstraintType_GreaterOrEqual:
+        return tag_.Format() + " >= " + GetValue();
+
+      case ConstraintType_Wildcard:
+        return tag_.Format() + " ~~ " + GetValue();
+
+      case ConstraintType_List:
+      {
+        std::string s = tag_.Format() + " IN [ ";
+
+        bool first = true;
+        for (std::set<std::string>::const_iterator
+               it = values_.begin(); it != values_.end(); ++it)
+        {
+          if (first)
+          {
+            first = false;
+          }
+          else
+          {
+            s += ", ";
+          }
+
+          s += *it;
+        }
+
+        return s + "]";
+      }
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+  }
+
+
+  DatabaseConstraint DicomTagConstraint::ConvertToDatabaseConstraint(ResourceType level,
+                                                                     DicomTagType tagType) const
+  {
+    bool isIdentifier, caseSensitive;
+    
+    switch (tagType)
+    {
+      case DicomTagType_Identifier:
+        isIdentifier = true;
+        caseSensitive = true;
+        break;
+
+      case DicomTagType_Main:
+        isIdentifier = false;
+        caseSensitive = IsCaseSensitive();
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+    std::vector<std::string> values;
+    values.reserve(values_.size());
+      
+    for (std::set<std::string>::const_iterator
+           it = values_.begin(); it != values_.end(); ++it)
+    {
+      if (isIdentifier)
+      {
+        values.push_back(ServerToolbox::NormalizeIdentifier(*it));
+      }
+      else
+      {
+        values.push_back(*it);
+      }
+    }
+
+    return DatabaseConstraint(level, tag_, isIdentifier, constraintType_,
+                              values, caseSensitive, mandatory_);
+  }  
 }
--- a/OrthancServer/Search/DicomTagConstraint.h	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/Search/DicomTagConstraint.h	Tue Jan 15 21:11:44 2019 +0100
@@ -35,6 +35,7 @@
 
 #include "../ServerEnumerations.h"
 #include "../../Core/DicomFormat/DicomMap.h"
+#include "DatabaseConstraint.h"
 
 #include <boost/shared_ptr.hpp>
 
@@ -46,37 +47,30 @@
     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_;
 
+    void AssignSingleValue(const std::string& value);
+
   public:
     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_;
-    }
+                       bool caseSensitive,
+                       bool mandatory);
 
-    void SetTagInfo(DicomTagType tagType,
-                    ResourceType level);
-
-    DicomTagType GetTagType() const;
-
-    const ResourceType GetLevel() const;
+    DicomTagConstraint(const DatabaseConstraint& constraint);
 
     const DicomTag& GetTag() const
     {
@@ -93,6 +87,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;
@@ -105,5 +109,10 @@
     bool IsMatch(const std::string& value);
 
     bool IsMatch(const DicomMap& value);
+
+    std::string Format() const;
+
+    DatabaseConstraint ConvertToDatabaseConstraint(ResourceType level,
+                                                   DicomTagType tagType) const;
   };
 }
--- a/OrthancServer/Search/HierarchicalMatcher.cpp	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/Search/HierarchicalMatcher.cpp	Tue Jan 15 21:11:44 2019 +0100
@@ -59,15 +59,6 @@
 
   HierarchicalMatcher::~HierarchicalMatcher()
   {
-    for (Constraints::iterator it = constraints_.begin();
-         it != constraints_.end(); ++it)
-    {
-      if (it->second != NULL)
-      {
-        delete it->second;
-      }
-    }
-
     for (Sequences::iterator it = sequences_.begin();
          it != sequences_.end(); ++it)
     {
@@ -98,15 +89,14 @@
         continue;
       }
 
-      ValueRepresentation vr = FromDcmtkBridge::LookupValueRepresentation(tag);
-
-      if (constraints_.find(tag) != constraints_.end() ||
+      if (flatTags_.find(tag) != flatTags_.end() ||
           sequences_.find(tag) != sequences_.end())
       {
+        // A constraint already exists on this tag
         throw OrthancException(ErrorCode_BadRequest);        
       }
 
-      if (vr == ValueRepresentation_Sequence)
+      if (FromDcmtkBridge::LookupValueRepresentation(tag) == ValueRepresentation_Sequence)
       {
         DcmSequenceOfItems& sequence = dynamic_cast<DcmSequenceOfItems&>(*element);
 
@@ -127,12 +117,20 @@
       }
       else
       {
+        flatTags_.insert(tag);
+
         std::set<DicomTag> ignoreTagLength;
         std::auto_ptr<DicomValue> value(FromDcmtkBridge::ConvertLeafElement
                                         (*element, DicomToJsonFlags_None, 
-                                         ORTHANC_MAXIMUM_TAG_LENGTH, encoding, ignoreTagLength));
+                                         0, encoding, ignoreTagLength));
 
-        if (value->IsBinary())
+        // WARNING: Also modify "DatabaseLookup::IsMatch()" if modifying this code
+        if (value.get() == NULL ||
+            value->IsNull())
+        {
+          // This is an universal constraint
+        }
+        else if (value->IsBinary())
         {
           if (!value->GetContent().empty())
           {
@@ -140,26 +138,15 @@
                          << tag.Format() << ") with UN (unknown) value representation. "
                          << "It will be ignored.";
           }
-
-          constraints_[tag] = NULL;
         }
-        else if (value->IsNull() ||
-                 value->GetContent().empty())
+        else if (value->GetContent().empty())
         {
           // This is an universal matcher
-          constraints_[tag] = NULL;
         }
         else
         {
-          // DICOM specifies that searches must be case sensitive, except
-          // for tags with a PN value representation
-          bool sensitive = true;
-          if (vr == ValueRepresentation_PersonName)
-          {
-            sensitive = caseSensitivePN;
-          }
-
-          constraints_[tag] = IFindConstraint::ParseDicomConstraint(tag, value->GetContent(), sensitive);
+          flatConstraints_.AddDicomConstraint
+            (tag, value->GetContent(), caseSensitivePN, true /* mandatory */);
         }
       }
     }
@@ -169,19 +156,23 @@
   std::string HierarchicalMatcher::Format(const std::string& prefix) const
   {
     std::string s;
-    
-    for (Constraints::const_iterator it = constraints_.begin();
-         it != constraints_.end(); ++it)
+
+    std::set<DicomTag> tags;
+    for (size_t i = 0; i < flatConstraints_.GetConstraintsCount(); i++)
     {
-      s += prefix + it->first.Format() + " ";
+      const DicomTagConstraint& c = flatConstraints_.GetConstraint(i);
 
-      if (it->second == NULL)
+      s += c.Format() + "\n";
+      tags.insert(c.GetTag());
+    }
+    
+    // Loop over the universal constraints
+    for (std::set<DicomTag>::const_iterator it = flatTags_.begin();
+         it != flatTags_.end(); ++it)
+    {
+      if (tags.find(*it) == tags.end())
       {
-        s += "*\n";
-      }
-      else
-      {
-        s += it->second->Format() + "\n";
+        s += prefix + it->Format() + " == *\n";
       }
     }
 
@@ -214,34 +205,11 @@
   bool HierarchicalMatcher::MatchInternal(DcmItem& item,
                                           Encoding encoding) const
   {
-    for (Constraints::const_iterator it = constraints_.begin();
-         it != constraints_.end(); ++it)
+    if (!flatConstraints_.IsMatch(item, encoding))
     {
-      if (it->second != NULL)
-      {
-        DcmTagKey tag = ToDcmtkBridge::Convert(it->first);
-
-        DcmElement* element = NULL;
-        if (!item.findAndGetElement(tag, element).good() ||
-            element == NULL)
-        {
-          return false;
-        }
-
-        std::set<DicomTag> ignoreTagLength;
-        std::auto_ptr<DicomValue> value(FromDcmtkBridge::ConvertLeafElement
-                                        (*element, DicomToJsonFlags_None, 
-                                         ORTHANC_MAXIMUM_TAG_LENGTH, encoding, ignoreTagLength));
-
-        if (value->IsNull() ||
-            value->IsBinary() ||
-            !it->second->Match(value->GetContent()))
-        {
-          return false;
-        }
-      }
+      return false;
     }
-
+    
     for (Sequences::const_iterator it = sequences_.begin();
          it != sequences_.end(); ++it)
     {
@@ -283,16 +251,16 @@
   {
     std::auto_ptr<DcmDataset> target(new DcmDataset);
 
-    for (Constraints::const_iterator it = constraints_.begin();
-         it != constraints_.end(); ++it)
+    for (std::set<DicomTag>::const_iterator it = flatTags_.begin();
+         it != flatTags_.end(); ++it)
     {
-      DcmTagKey tag = ToDcmtkBridge::Convert(it->first);
+      DcmTagKey tag = ToDcmtkBridge::Convert(*it);
       
       DcmElement* element = NULL;
       if (source.findAndGetElement(tag, element).good() &&
           element != NULL)
       {
-        std::auto_ptr<DcmElement> cloned(FromDcmtkBridge::CreateElementForTag(it->first));
+        std::auto_ptr<DcmElement> cloned(FromDcmtkBridge::CreateElementForTag(*it));
         cloned->copyFrom(*element);
         target->insert(cloned.release());
       }
--- a/OrthancServer/Search/HierarchicalMatcher.h	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/Search/HierarchicalMatcher.h	Tue Jan 15 21:11:44 2019 +0100
@@ -33,7 +33,7 @@
 
 #pragma once
 
-#include "IFindConstraint.h"
+#include "DatabaseLookup.h"
 #include "../../Core/DicomParsing/ParsedDicomFile.h"
 
 class DcmItem;
@@ -43,11 +43,11 @@
   class HierarchicalMatcher : public boost::noncopyable
   {
   private:
-    typedef std::map<DicomTag, IFindConstraint*>      Constraints;
     typedef std::map<DicomTag, HierarchicalMatcher*>  Sequences;
 
-    Constraints  constraints_;
-    Sequences    sequences_;
+    std::set<DicomTag>  flatTags_;
+    DatabaseLookup      flatConstraints_;
+    Sequences           sequences_;
 
     void Setup(DcmItem& query,
                bool caseSensitivePN,
--- a/OrthancServer/Search/IFindConstraint.cpp	Tue Jan 15 18:46:59 2019 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,131 +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-2019 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 "IFindConstraint.h"
-
-#include "ListConstraint.h"
-#include "RangeConstraint.h"
-#include "ValueConstraint.h"
-#include "WildcardConstraint.h"
-
-#include "../../Core/DicomParsing/FromDcmtkBridge.h"
-#include "../../Core/OrthancException.h"
-
-namespace Orthanc
-{
-  IFindConstraint* IFindConstraint::ParseDicomConstraint(const DicomTag& tag,
-                                                         const std::string& dicomQuery,
-                                                         bool caseSensitive)
-  {
-    ValueRepresentation vr = FromDcmtkBridge::LookupValueRepresentation(tag);
-
-    if (vr == ValueRepresentation_Sequence)
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-
-    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);
-      return new RangeConstraint(lower, upper, caseSensitive);
-    }
-    else if (dicomQuery.find('\\') != std::string::npos)
-    {
-      std::auto_ptr<ListConstraint> constraint(new ListConstraint(caseSensitive));
-
-      std::vector<std::string> items;
-      Toolbox::TokenizeString(items, dicomQuery, '\\');
-
-      for (size_t i = 0; i < items.size(); i++)
-      {
-        constraint->AddAllowedValue(items[i]);
-      }
-
-      return constraint.release();
-    }
-    else if (dicomQuery.find('*') != std::string::npos ||
-             dicomQuery.find('?') != std::string::npos)
-    {
-      return new WildcardConstraint(dicomQuery, caseSensitive);
-    }
-    else
-    {
-      /**
-       * Case-insensitive match for PN value representation (Patient
-       * Name). Case-senstive match for all the other value
-       * representations.
-       *
-       * Reference: DICOM PS 3.4
-       *   - C.2.2.2.1 ("Single Value Matching") 
-       *   - C.2.2.2.4 ("Wild Card Matching")
-       * http://medical.nema.org/Dicom/2011/11_04pu.pdf
-       *
-       * "Except for Attributes with a PN Value Representation, only
-       * entities with values which match exactly the value specified in the
-       * request shall match. This matching is case-sensitive, i.e.,
-       * sensitive to the exact encoding of the key attribute value in
-       * character sets where a letter may have multiple encodings (e.g.,
-       * based on its case, its position in a word, or whether it is
-       * accented)
-       * 
-       * For Attributes with a PN Value Representation (e.g., Patient Name
-       * (0010,0010)), an application may perform literal matching that is
-       * either case-sensitive, or that is insensitive to some or all
-       * aspects of case, position, accent, or other character encoding
-       * variants."
-       *
-       * (0008,0018) UI SOPInstanceUID     => Case-sensitive
-       * (0008,0050) SH AccessionNumber    => Case-sensitive
-       * (0010,0020) LO PatientID          => Case-sensitive
-       * (0020,000D) UI StudyInstanceUID   => Case-sensitive
-       * (0020,000E) UI SeriesInstanceUID  => Case-sensitive
-       **/
-
-      return new ValueConstraint(dicomQuery, caseSensitive);
-    }
-  }
-}
--- a/OrthancServer/Search/IFindConstraint.h	Tue Jan 15 18:46:59 2019 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,60 +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-2019 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 "LookupIdentifierQuery.h"
-
-namespace Orthanc
-{
-  class IFindConstraint : public boost::noncopyable
-  {
-  public:
-    virtual ~IFindConstraint()
-    {
-    }
-
-    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;
-
-    static IFindConstraint* ParseDicomConstraint(const DicomTag& tag,
-                                                 const std::string& dicomQuery,
-                                                 bool caseSensitive);
-  };
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Search/ISqlLookupFormatter.cpp	Tue Jan 15 21:11:44 2019 +0100
@@ -0,0 +1,343 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 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 "ISqlLookupFormatter.h"
+
+#include "../../Core/OrthancException.h"
+#include "DatabaseConstraint.h"
+
+namespace Orthanc
+{
+  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,
+                               ISqlLookupFormatter& formatter,
+                               const DatabaseConstraint& constraint,
+                               size_t index)
+  {
+    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);
+        }
+
+        std::string parameter = formatter.GenerateParameter(constraint.GetSingleValue());
+
+        if (constraint.IsCaseSensitive())
+        {
+          comparison = tag + ".value " + op + " " + parameter;
+        }
+        else
+        {
+          comparison = "lower(" + tag + ".value) " + op + " lower(" + parameter + ")";
+        }
+
+        break;
+      }
+
+      case ConstraintType_List:
+      {
+        for (size_t i = 0; i < constraint.GetValuesCount(); i++)
+        {
+          if (!comparison.empty())
+          {
+            comparison += ", ";
+          }
+            
+          std::string parameter = formatter.GenerateParameter(constraint.GetValue(i));
+
+          if (constraint.IsCaseSensitive())
+          {
+            comparison += parameter;
+          }
+          else
+          {
+            comparison += "lower(" + parameter + ")";
+          }
+        }
+
+        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];
+            }               
+          }
+
+          std::string parameter = formatter.GenerateParameter(escaped);
+
+          if (constraint.IsCaseSensitive())
+          {
+            comparison = (tag + ".value LIKE " + parameter + " " +
+                          formatter.FormatWildcardEscape());
+          }
+          else
+          {
+            comparison = ("lower(" + tag + ".value) LIKE lower(" +
+                          parameter + ") " + formatter.FormatWildcardEscape());
+          }
+        }
+          
+        break;
+      }
+
+      default:
+        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()));
+  }
+  
+
+  void ISqlLookupFormatter::Apply(std::string& sql,
+                                  ISqlLookupFormatter& formatter,
+                                  const std::vector<DatabaseConstraint>& lookup,
+                                  ResourceType queryLevel,
+                                  size_t limit)
+  {
+    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;
+      }
+    }
+    
+    assert(upperLevel <= queryLevel &&
+           queryLevel <= lowerLevel);
+
+    std::string joins, comparisons;
+
+    size_t count = 0;
+    
+    for (size_t i = 0; i < lookup.size(); i++)
+    {
+      std::string comparison;
+      
+      if (FormatComparison(comparison, formatter, lookup[i], count))
+      {
+        std::string join;
+        FormatJoin(join, lookup[i], count);
+        joins += join;
+
+        if (!comparison.empty())
+        {
+          comparisons += " AND " + comparison;
+        }
+        
+        count ++;
+      }
+    }
+
+    sql = ("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 = " +
+            formatter.FormatResourceType(queryLevel) + comparisons);
+
+    if (limit != 0)
+    {
+      sql += " LIMIT " + boost::lexical_cast<std::string>(limit);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Search/ISqlLookupFormatter.h	Tue Jan 15 21:11:44 2019 +0100
@@ -0,0 +1,65 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 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 "../../Core/Enumerations.h"
+
+#include <boost/noncopyable.hpp>
+#include <vector>
+
+namespace Orthanc
+{
+  class DatabaseConstraint;
+  
+  // This class is also used by the "orthanc-databases" project
+  class ISqlLookupFormatter : public boost::noncopyable
+  {
+  public:
+    virtual ~ISqlLookupFormatter()
+    {
+    }
+
+    virtual std::string GenerateParameter(const std::string& value) = 0;
+
+    virtual std::string FormatResourceType(ResourceType level) = 0;
+
+    virtual std::string FormatWildcardEscape() = 0;
+
+    static void Apply(std::string& sql,
+                      ISqlLookupFormatter& formatter,
+                      const std::vector<DatabaseConstraint>& lookup,
+                      ResourceType queryLevel,
+                      size_t limit);
+  };
+}
--- a/OrthancServer/Search/ListConstraint.cpp	Tue Jan 15 18:46:59 2019 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,100 +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-2019 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 "ListConstraint.h"
-
-
-namespace Orthanc
-{
-  void ListConstraint::AddAllowedValue(const std::string& value)
-  {
-    if (isCaseSensitive_)
-    {
-      allowedValues_.insert(value);
-    }
-    else
-    {
-      allowedValues_.insert(Toolbox::ToUpperCaseWithAccents(value));
-    }
-  }
-
-
-  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;
-    
-    if (isCaseSensitive_)
-    {
-      s = value;
-    }
-    else
-    {
-      s = Toolbox::ToUpperCaseWithAccents(value);
-    }
-
-    return allowedValues_.find(s) != allowedValues_.end();
-  }
-
-
-  std::string ListConstraint::Format() const
-  {
-    std::string s;
-
-    for (std::set<std::string>::const_iterator
-           it = allowedValues_.begin(); it != allowedValues_.end(); ++it)
-    {
-      if (!s.empty())
-      {
-        s += "\\";
-      }
-
-      s += *it;
-    }
-
-    return s;
-  }
-}
--- a/OrthancServer/Search/ListConstraint.h	Tue Jan 15 18:46:59 2019 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,74 +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-2019 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 "IFindConstraint.h"
-
-#include <set>
-
-namespace Orthanc
-{
-  class ListConstraint : public IFindConstraint
-  {
-  private:
-    std::set<std::string>  allowedValues_;
-    bool                   isCaseSensitive_;
-
-    ListConstraint(const ListConstraint& other) : 
-      allowedValues_(other.allowedValues_),
-      isCaseSensitive_(other.isCaseSensitive_)
-    {
-    }
-
-  public:
-    ListConstraint(bool isCaseSensitive) : 
-      isCaseSensitive_(isCaseSensitive)
-    {
-    }
-
-    void AddAllowedValue(const std::string& value);
-
-    virtual IFindConstraint* Clone() const
-    {
-      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	Tue Jan 15 18:46:59 2019 +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-2019 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	Tue Jan 15 18:46:59 2019 +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-2019 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	Tue Jan 15 18:46:59 2019 +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-2019 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	Tue Jan 15 18:46:59 2019 +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-2019 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	Tue Jan 15 18:46:59 2019 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,113 +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-2019 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 "RangeConstraint.h"
-
-#include "../../Core/Toolbox.h"
-
-namespace Orthanc
-{
-  RangeConstraint::RangeConstraint(const std::string& lower,
-                                   const std::string& upper,
-                                   bool isCaseSensitive) : 
-    isCaseSensitive_(isCaseSensitive)
-  {
-    if (isCaseSensitive_)
-    {
-      lower_ = lower;
-      upper_ = upper;
-    }
-    else
-    {
-      lower_ = Toolbox::ToUpperCaseWithAccents(lower);
-      upper_ = Toolbox::ToUpperCaseWithAccents(upper);
-    }
-  }
-
-
-  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;
-
-    if (isCaseSensitive_)
-    {
-      v = value;
-    }
-    else
-    {
-      v = Toolbox::ToUpperCaseWithAccents(value);
-    }
-
-    if (lower_.size() == 0 && 
-        upper_.size() == 0)
-    {
-      return false;
-    }
-
-    if (lower_.size() == 0)
-    {
-      return v <= upper_;
-    }
-
-    if (upper_.size() == 0)
-    {
-      return v >= lower_;
-    }
-    
-    return (v >= lower_ && v <= upper_);
-  }
-}
--- a/OrthancServer/Search/RangeConstraint.h	Tue Jan 15 18:46:59 2019 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,74 +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-2019 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 "IFindConstraint.h"
-
-namespace Orthanc
-{
-  class RangeConstraint : public IFindConstraint
-  {
-  private:
-    std::string  lower_;
-    std::string  upper_;
-    bool         isCaseSensitive_;
-
-    RangeConstraint(const RangeConstraint& other) : 
-      lower_(other.lower_),
-      upper_(other.upper_),
-      isCaseSensitive_(other.isCaseSensitive_)
-    {
-    }
-
-  public:
-    RangeConstraint(const std::string& lower,
-                    const std::string& upper,
-                    bool isCaseSensitive);
-
-    virtual IFindConstraint* Clone() const
-    {
-      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
-    {
-      return lower_ + "-" + upper_;
-    }
-  };
-}
--- a/OrthancServer/Search/SetOfResources.cpp	Tue Jan 15 18:46:59 2019 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,157 +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-2019 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 "SetOfResources.h"
-
-#include "../../Core/OrthancException.h"
-
-
-namespace Orthanc
-{
-  void SetOfResources::Intersect(const std::list<int64_t>& resources)
-  {
-    if (resources_.get() == NULL)
-    {
-      resources_.reset(new Resources);
-
-      for (std::list<int64_t>::const_iterator
-             it = resources.begin(); it != resources.end(); ++it)
-      {
-        resources_->insert(*it);
-      }
-    }
-    else
-    {
-      std::auto_ptr<Resources> filtered(new Resources);
-
-      for (std::list<int64_t>::const_iterator
-             it = resources.begin(); it != resources.end(); ++it)
-      {
-        if (resources_->find(*it) != resources_->end())
-        {
-          filtered->insert(*it);
-        }
-      }
-
-      resources_ = filtered;
-    }
-  }
-
-
-  void SetOfResources::GoDown()
-  {
-    if (level_ == ResourceType_Instance)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-
-    if (resources_.get() != NULL)
-    {
-      std::auto_ptr<Resources> children(new Resources);
-
-      for (Resources::const_iterator it = resources_->begin(); 
-           it != resources_->end(); ++it)
-      {
-        std::list<int64_t> tmp;
-        database_.GetChildrenInternalId(tmp, *it);
-
-        for (std::list<int64_t>::const_iterator
-               child = tmp.begin(); child != tmp.end(); ++child)
-        {
-          children->insert(*child);
-        }
-      }
-
-      resources_ = children;
-    }
-
-    switch (level_)
-    {
-      case ResourceType_Patient:
-        level_ = ResourceType_Study;
-        break;
-
-      case ResourceType_Study:
-        level_ = ResourceType_Series;
-        break;
-
-      case ResourceType_Series:
-        level_ = ResourceType_Instance;
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-    }
-  }
-
-
-  void SetOfResources::Flatten(std::list<std::string>& result)
-  {
-    result.clear();
-      
-    if (resources_.get() == NULL)
-    {
-      // All the resources of this level are part of the filter
-      database_.GetAllPublicIds(result, level_);
-    }
-    else
-    {
-      for (Resources::const_iterator it = resources_->begin(); 
-           it != resources_->end(); ++it)
-      {
-        result.push_back(database_.GetPublicId(*it));
-      }
-    }
-  }
-
-
-  void SetOfResources::Flatten(std::list<int64_t>& result)
-  {
-    result.clear();
-      
-    if (resources_.get() == NULL)
-    {
-      // All the resources of this level are part of the filter
-      database_.GetAllInternalIds(result, level_);
-    }
-    else
-    {
-      for (Resources::const_iterator it = resources_->begin(); 
-           it != resources_->end(); ++it)
-      {
-        result.push_back(*it);
-      }
-    }
-  }
-}
--- a/OrthancServer/Search/SetOfResources.h	Tue Jan 15 18:46:59 2019 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,79 +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-2019 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 <set>
-#include <boost/noncopyable.hpp>
-#include <memory>
-
-namespace Orthanc
-{
-  class SetOfResources : public boost::noncopyable
-  {
-  private:
-    typedef std::set<int64_t>  Resources;
-
-    IDatabaseWrapper&         database_;
-    ResourceType              level_;
-    std::auto_ptr<Resources>  resources_;
-    
-  public:
-    SetOfResources(IDatabaseWrapper& database,
-                   ResourceType level) : 
-      database_(database),
-      level_(level)
-    {
-    }
-
-    ResourceType GetLevel() const
-    {
-      return level_;
-    }
-
-    void Intersect(const std::list<int64_t>& resources);
-
-    void GoDown();
-
-    void Flatten(std::list<int64_t>& result);
-
-    void Flatten(std::list<std::string>& result);
-
-    void Clear()
-    {
-      resources_.reset(NULL);
-    }
-  };
-}
--- a/OrthancServer/Search/ValueConstraint.cpp	Tue Jan 15 18:46:59 2019 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,75 +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-2019 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 "ValueConstraint.h"
-
-#include "../../Core/Toolbox.h"
-
-#include <stdio.h>
-
-namespace Orthanc
-{
-  ValueConstraint::ValueConstraint(const std::string& value,
-                                   bool isCaseSensitive) : 
-    isCaseSensitive_(isCaseSensitive)
-  {
-    if (isCaseSensitive)
-    {
-      value_ = value;
-    }
-    else
-    {
-      value_ = Toolbox::ToUpperCaseWithAccents(value);
-    }
-  }
-
-
-  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_)
-    {
-      return value_ == value;
-    }
-    else
-    {
-      return value_ == Toolbox::ToUpperCaseWithAccents(value);
-    }
-  }
-}
--- a/OrthancServer/Search/ValueConstraint.h	Tue Jan 15 18:46:59 2019 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,71 +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-2019 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 "IFindConstraint.h"
-
-namespace Orthanc
-{
-  class ValueConstraint : public IFindConstraint
-  {
-  private:
-    std::string  value_;
-    bool         isCaseSensitive_;
-
-    ValueConstraint(const ValueConstraint& other) : 
-      value_(other.value_),
-      isCaseSensitive_(other.isCaseSensitive_)
-    {
-    }
-
-  public:
-    ValueConstraint(const std::string& value,
-                    bool isCaseSensitive);
-
-    virtual IFindConstraint* Clone() const
-    {
-      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
-    {
-      return value_;
-    }
-  };
-}
--- a/OrthancServer/Search/WildcardConstraint.cpp	Tue Jan 15 18:46:59 2019 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,100 +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-2019 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 "WildcardConstraint.h"
-
-#include <boost/regex.hpp>
-
-namespace Orthanc
-{
-  struct WildcardConstraint::PImpl
-  {
-    boost::regex  pattern_;
-    std::string   wildcard_;
-    bool          isCaseSensitive_;
-
-    PImpl(const std::string& wildcard,
-          bool isCaseSensitive)
-    {
-      isCaseSensitive_ = isCaseSensitive;
-    
-      if (isCaseSensitive)
-      {
-        wildcard_ = wildcard;
-      }
-      else
-      {
-        wildcard_ = Toolbox::ToUpperCaseWithAccents(wildcard);
-      }
-
-      pattern_ = boost::regex(Toolbox::WildcardToRegularExpression(wildcard_));
-    }
-  };
-
-
-  WildcardConstraint::WildcardConstraint(const WildcardConstraint& other) :
-    pimpl_(new PImpl(*other.pimpl_))
-  {
-  }
-
-
-  WildcardConstraint::WildcardConstraint(const std::string& wildcard,
-                                         bool isCaseSensitive) :
-    pimpl_(new PImpl(wildcard, isCaseSensitive))
-  {
-  }
-
-  bool WildcardConstraint::Match(const std::string& value) const
-  {
-    if (pimpl_->isCaseSensitive_)
-    {
-      return boost::regex_match(value, pimpl_->pattern_);
-    }
-    else
-    {
-      return boost::regex_match(Toolbox::ToUpperCaseWithAccents(value), pimpl_->pattern_);
-    }
-  }
-
-  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	Tue Jan 15 18:46:59 2019 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,66 +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-2019 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 "IFindConstraint.h"
-
-#include <boost/shared_ptr.hpp>
-
-namespace Orthanc
-{
-  class WildcardConstraint : public IFindConstraint
-  {
-  private:
-    struct PImpl;
-    boost::shared_ptr<PImpl>  pimpl_;
-
-    WildcardConstraint(const WildcardConstraint& other);
-
-  public:
-    WildcardConstraint(const std::string& wildcard,
-                       bool isCaseSensitive);
-
-    virtual IFindConstraint* Clone() const
-    {
-      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	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/ServerContext.cpp	Tue Jan 15 21:11:44 2019 +0100
@@ -34,15 +34,18 @@
 #include "PrecompiledHeadersServer.h"
 #include "ServerContext.h"
 
+#include "../Core/Cache/SharedArchive.h"
 #include "../Core/DicomParsing/FromDcmtkBridge.h"
 #include "../Core/FileStorage/StorageAccessor.h"
 #include "../Core/HttpServer/FilesystemHttpSender.h"
 #include "../Core/HttpServer/HttpStreamTranscoder.h"
+#include "../Core/JobsEngine/SetOfInstancesJob.h"
 #include "../Core/Logging.h"
 #include "../Plugins/Engine/OrthancPlugins.h"
+
 #include "OrthancConfiguration.h"
 #include "OrthancRestApi/OrthancRestApi.h"
-#include "Search/LookupResource.h"
+#include "Search/DatabaseLookup.h"
 #include "ServerJobs/OrthancJobUnserializer.h"
 #include "ServerToolbox.h"
 
@@ -774,11 +777,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 +809,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 +834,6 @@
 
     size_t countResults = 0;
     size_t skipped = 0;
-    bool complete = true;
 
     const bool isDicomAsJsonNeeded = visitor.IsDicomAsJsonNeeded();
     
--- a/OrthancServer/ServerContext.h	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/ServerContext.h	Tue Jan 15 21:11:44 2019 +0100
@@ -33,29 +33,27 @@
 
 #pragma once
 
-#include "DicomInstanceToStore.h"
 #include "IServerListener.h"
 #include "LuaScripting.h"
 #include "OrthancHttpHandler.h"
 #include "ServerIndex.h"
-#include "Search/LookupResource.h"
 
 #include "../Core/Cache/MemoryCache.h"
-#include "../Core/Cache/SharedArchive.h"
-#include "../Core/DicomParsing/ParsedDicomFile.h"
-#include "../Core/FileStorage/IStorageArea.h"
-#include "../Core/JobsEngine/JobsEngine.h"
-#include "../Core/JobsEngine/SetOfInstancesJob.h"
-#include "../Core/MultiThreading/SharedMessageQueue.h"
-#include "../Core/RestApi/RestApiOutput.h"
-#include "../Plugins/Engine/OrthancPlugins.h"
-
-#include <boost/filesystem.hpp>
-#include <boost/thread.hpp>
 
 
 namespace Orthanc
 {
+  class DicomInstanceToStore;
+  class IStorageArea;
+  class JobsEngine;
+  class OrthancPlugins;
+  class ParsedDicomFile;
+  class RestApiOutput;
+  class SetOfInstancesJob;
+  class SharedArchive;
+  class SharedMessageQueue;
+  
+  
   /**
    * This class is responsible for maintaining the storage area on the
    * filesystem (including compression), as well as the index of the
@@ -363,7 +361,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	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/ServerEnumerations.h	Tue Jan 15 21:11:44 2019 +0100
@@ -56,14 +56,6 @@
     StoreStatus_FilteredOut     // Removed by NewInstanceFilter
   };
 
-  enum IdentifierConstraintType
-  {
-    IdentifierConstraintType_Equal,
-    IdentifierConstraintType_SmallerOrEqual,
-    IdentifierConstraintType_GreaterOrEqual,
-    IdentifierConstraintType_Wildcard        /* Case sensitive, "*" or "?" are the only allowed wildcards */
-  };
-
   enum DicomTagType
   {
     DicomTagType_Identifier,   // Tag that whose value is stored and indexed in the DB
@@ -80,6 +72,17 @@
     ConstraintType_List
   };
 
+  namespace Compatibility
+  {
+    enum IdentifierConstraintType
+    {
+      IdentifierConstraintType_Equal,
+      IdentifierConstraintType_SmallerOrEqual,
+      IdentifierConstraintType_GreaterOrEqual,
+      IdentifierConstraintType_Wildcard        /* Case sensitive, "*" or "?" are the only allowed wildcards */
+    };
+  }
+
 
   /**
    * WARNING: Do not change the explicit values in the enumerations
@@ -93,8 +96,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	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/ServerIndex.cpp	Tue Jan 15 21:11:44 2019 +0100
@@ -38,19 +38,21 @@
 #define NOMINMAX
 #endif
 
-#include "ServerIndexChange.h"
+#include "../Core/DicomFormat/DicomArray.h"
+#include "../Core/DicomParsing/FromDcmtkBridge.h"
+#include "../Core/DicomParsing/ParsedDicomFile.h"
+#include "../Core/Logging.h"
+#include "../Core/Toolbox.h"
+
+#include "Database/ResourcesContent.h"
+#include "DicomInstanceToStore.h"
 #include "EmbeddedResources.h"
 #include "OrthancConfiguration.h"
-#include "../Core/DicomParsing/ParsedDicomFile.h"
+#include "Search/DatabaseLookup.h"
+#include "Search/DicomTagConstraint.h"
+#include "ServerContext.h"
+#include "ServerIndexChange.h"
 #include "ServerToolbox.h"
-#include "../Core/Toolbox.h"
-#include "../Core/Logging.h"
-#include "../Core/DicomFormat/DicomArray.h"
-
-#include "../Core/DicomParsing/FromDcmtkBridge.h"
-#include "ServerContext.h"
-#include "DicomInstanceToStore.h"
-#include "Search/LookupResource.h"
 
 #include <boost/lexical_cast.hpp>
 #include <stdio.h>
@@ -59,6 +61,22 @@
 
 namespace Orthanc
 {
+  static void CopyListToVector(std::vector<std::string>& target,
+                               const std::list<std::string>& source)
+  {
+    target.resize(source.size());
+
+    size_t pos = 0;
+    
+    for (std::list<std::string>::const_iterator
+           it = source.begin(); it != source.end(); ++it)
+    {
+      target[pos] = *it;
+      pos ++;
+    }      
+  }
+
+  
   class ServerIndex::Listener : public IDatabaseListener
   {
   private:
@@ -215,7 +233,7 @@
   {
   private:
     ServerIndex& index_;
-    std::auto_ptr<SQLite::ITransaction> transaction_;
+    std::auto_ptr<IDatabaseWrapper::ITransaction> transaction_;
     bool isCommitted_;
 
   public:
@@ -226,8 +244,6 @@
       transaction_.reset(index_.db_.StartTransaction());
       transaction_->Begin();
 
-      assert(index_.currentStorageSize_ == index_.db_.GetTotalCompressedSize());
-
       index_.listener_->StartTransaction();
     }
 
@@ -245,18 +261,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 +317,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)
@@ -387,8 +502,7 @@
   }
 
 
-  static void ComputeExpectedNumberOfInstances(IDatabaseWrapper& db,
-                                               int64_t series,
+  static bool ComputeExpectedNumberOfInstances(int64_t& target,
                                                const DicomMap& dicomSummary)
   {
     try
@@ -397,28 +511,39 @@
       const DicomValue* value2;
           
       if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_IMAGES_IN_ACQUISITION)) != NULL &&
-          (value2 = dicomSummary.TestAndGetValue(DICOM_TAG_NUMBER_OF_TEMPORAL_POSITIONS)) != NULL)
+          !value->IsNull() &&
+          !value->IsBinary() &&
+          (value2 = dicomSummary.TestAndGetValue(DICOM_TAG_NUMBER_OF_TEMPORAL_POSITIONS)) != NULL &&
+          !value2->IsNull() &&
+          !value2->IsBinary())
       {
         // Patch for series with temporal positions thanks to Will Ryder
         int64_t imagesInAcquisition = boost::lexical_cast<int64_t>(value->GetContent());
         int64_t countTemporalPositions = boost::lexical_cast<int64_t>(value2->GetContent());
-        std::string expected = boost::lexical_cast<std::string>(imagesInAcquisition * countTemporalPositions);
-        db.SetMetadata(series, MetadataType_Series_ExpectedNumberOfInstances, expected);
+        target = imagesInAcquisition * countTemporalPositions;
+        return (target > 0);
       }
 
       else if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_NUMBER_OF_SLICES)) != NULL &&
-               (value2 = dicomSummary.TestAndGetValue(DICOM_TAG_NUMBER_OF_TIME_SLICES)) != NULL)
+               !value->IsNull() &&
+               !value->IsBinary() &&
+               (value2 = dicomSummary.TestAndGetValue(DICOM_TAG_NUMBER_OF_TIME_SLICES)) != NULL &&
+               !value2->IsBinary() &&
+               !value2->IsNull())
       {
         // Support of Cardio-PET images
         int64_t numberOfSlices = boost::lexical_cast<int64_t>(value->GetContent());
         int64_t numberOfTimeSlices = boost::lexical_cast<int64_t>(value2->GetContent());
-        std::string expected = boost::lexical_cast<std::string>(numberOfSlices * numberOfTimeSlices);
-        db.SetMetadata(series, MetadataType_Series_ExpectedNumberOfInstances, expected);
+        target = numberOfSlices * numberOfTimeSlices;
+        return (target > 0);
       }
 
-      else if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_CARDIAC_NUMBER_OF_IMAGES)) != NULL)
+      else if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_CARDIAC_NUMBER_OF_IMAGES)) != NULL &&
+               !value->IsNull() &&
+               !value->IsBinary())
       {
-        db.SetMetadata(series, MetadataType_Series_ExpectedNumberOfInstances, value->GetContent());
+        target = boost::lexical_cast<int64_t>(value->GetContent());
+        return (target > 0);
       }
     }
     catch (OrthancException&)
@@ -427,6 +552,8 @@
     catch (boost::bad_lexical_cast&)
     {
     }
+
+    return false;
   }
 
 
@@ -500,44 +627,6 @@
 
 
 
-  int64_t ServerIndex::CreateResource(const std::string& publicId,
-                                      ResourceType type)
-  {
-    int64_t id = db_.CreateResource(publicId, type);
-
-    ChangeType changeType;
-    switch (type)
-    {
-    case ResourceType_Patient: 
-      changeType = ChangeType_NewPatient; 
-      break;
-
-    case ResourceType_Study: 
-      changeType = ChangeType_NewStudy; 
-      break;
-
-    case ResourceType_Series: 
-      changeType = ChangeType_NewSeries; 
-      break;
-
-    case ResourceType_Instance: 
-      changeType = ChangeType_NewInstance; 
-      break;
-
-    default:
-      throw OrthancException(ErrorCode_InternalError);
-    }
-
-    ServerIndexChange change(changeType, type, publicId);
-    db_.LogChange(id, change);
-
-    assert(listener_.get() != NULL);
-    listener_->SignalChange(change);
-
-    return id;
-  }
-
-
   ServerIndex::ServerIndex(ServerContext& context,
                            IDatabaseWrapper& db,
                            unsigned int threadSleep) : 
@@ -545,13 +634,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();
@@ -598,18 +686,30 @@
   }
 
 
-
-  void ServerIndex::SetInstanceMetadata(std::map<MetadataType, std::string>& instanceMetadata,
-                                        int64_t instance,
-                                        MetadataType metadata,
-                                        const std::string& value)
+  static void SetInstanceMetadata(ResourcesContent& content,
+                                  std::map<MetadataType, std::string>& instanceMetadata,
+                                  int64_t instance,
+                                  MetadataType metadata,
+                                  const std::string& value)
   {
-    db_.SetMetadata(instance, metadata, value);
+    content.AddMetadata(instance, metadata, value);
     instanceMetadata[metadata] = value;
   }
 
 
-
+  void ServerIndex::SignalNewResource(ChangeType changeType,
+                                      ResourceType level,
+                                      const std::string& publicId,
+                                      int64_t internalId)
+  {
+    ServerIndexChange change(changeType, level, publicId);
+    db_.LogChange(internalId, change);
+    
+    assert(listener_.get() != NULL);
+    listener_->SignalChange(change);
+  }
+
+  
   StoreStatus ServerIndex::Store(std::map<MetadataType, std::string>& instanceMetadata,
                                  DicomInstanceToStore& instanceToStore,
                                  const Attachments& attachments)
@@ -619,35 +719,78 @@
     const DicomMap& dicomSummary = instanceToStore.GetSummary();
     const ServerIndex::MetadataMap& metadata = instanceToStore.GetMetadata();
 
+    int64_t expectedInstances;
+    const bool hasExpectedInstances =
+      ComputeExpectedNumberOfInstances(expectedInstances, dicomSummary);
+    
     instanceMetadata.clear();
 
+    const std::string hashPatient = instanceToStore.GetHasher().HashPatient();
+    const std::string hashStudy = instanceToStore.GetHasher().HashStudy();
+    const std::string hashSeries = instanceToStore.GetHasher().HashSeries();
+    const std::string hashInstance = instanceToStore.GetHasher().HashInstance();
+
     try
     {
       Transaction t(*this);
 
+      IDatabaseWrapper::CreateInstanceResult status;
+      int64_t instanceId;
+
       // Check whether this instance is already stored
+      if (!db_.CreateInstance(status, instanceId, hashPatient,
+                              hashStudy, hashSeries, hashInstance))
       {
-        ResourceType type;
-        int64_t tmp;
-        if (db_.LookupResource(tmp, type, instanceToStore.GetHasher().HashInstance()))
+        // The instance already exists
+        
+        if (overwrite_)
         {
-          assert(type == ResourceType_Instance);
-
-          if (overwrite_)
+          // Overwrite the old instance
+          LOG(INFO) << "Overwriting instance: " << hashInstance;
+          db_.DeleteResource(instanceId);
+
+          // Re-create the instance, now that the old one is removed
+          if (!db_.CreateInstance(status, instanceId, hashPatient,
+                                  hashStudy, hashSeries, hashInstance))
           {
-            // Overwrite the old instance
-            LOG(INFO) << "Overwriting instance: " << instanceToStore.GetHasher().HashInstance();
-            db_.DeleteResource(tmp);
-          }
-          else
-          {
-            // Do nothing if the instance already exists
-            db_.GetAllMetadata(instanceMetadata, tmp);
-            return StoreStatus_AlreadyStored;
+            throw OrthancException(ErrorCode_InternalError);
           }
         }
+        else
+        {
+          // Do nothing if the instance already exists and overwriting is disabled
+          db_.GetAllMetadata(instanceMetadata, instanceId);
+          return StoreStatus_AlreadyStored;
+        }
       }
 
+
+      // Warn about the creation of new resources. The order must be
+      // from instance to patient.
+
+      // NB: In theory, could be sped up by grouping the underlying
+      // calls to "db_.LogChange()". However, this would only have an
+      // impact when new patient/study/series get created, which
+      // occurs far less often that creating new instances. The
+      // positive impact looks marginal in practice.
+      SignalNewResource(ChangeType_NewInstance, ResourceType_Instance, hashInstance, instanceId);
+
+      if (status.isNewSeries_)
+      {
+        SignalNewResource(ChangeType_NewSeries, ResourceType_Series, hashSeries, status.seriesId_);
+      }
+      
+      if (status.isNewStudy_)
+      {
+        SignalNewResource(ChangeType_NewStudy, ResourceType_Study, hashStudy, status.studyId_);
+      }
+      
+      if (status.isNewPatient_)
+      {
+        SignalNewResource(ChangeType_NewPatient, ResourceType_Patient, hashPatient, status.patientId_);
+      }
+      
+      
       // Ensure there is enough room in the storage for the new instance
       uint64_t instanceSize = 0;
       for (Attachments::const_iterator it = attachments.begin();
@@ -656,208 +799,165 @@
         instanceSize += it->GetCompressedSize();
       }
 
-      Recycle(instanceSize, instanceToStore.GetHasher().HashPatient());
-
-      // Create the instance
-      int64_t instance = CreateResource(instanceToStore.GetHasher().HashInstance(), ResourceType_Instance);
-      ServerToolbox::StoreMainDicomTags(db_, instance, ResourceType_Instance, dicomSummary);
-
-      // Detect up to which level the patient/study/series/instance
-      // hierarchy must be created
-      int64_t patient = -1, study = -1, series = -1;
-      bool isNewPatient = false;
-      bool isNewStudy = false;
-      bool isNewSeries = false;
-
-      {
-        ResourceType dummy;
-
-        if (db_.LookupResource(series, dummy, instanceToStore.GetHasher().HashSeries()))
-        {
-          assert(dummy == ResourceType_Series);
-          // The patient, the study and the series already exist
-
-          bool ok = (db_.LookupResource(patient, dummy, instanceToStore.GetHasher().HashPatient()) &&
-                     db_.LookupResource(study, dummy, instanceToStore.GetHasher().HashStudy()));
-          assert(ok);
-        }
-        else if (db_.LookupResource(study, dummy, instanceToStore.GetHasher().HashStudy()))
-        {
-          assert(dummy == ResourceType_Study);
-
-          // New series: The patient and the study already exist
-          isNewSeries = true;
-
-          bool ok = db_.LookupResource(patient, dummy, instanceToStore.GetHasher().HashPatient());
-          assert(ok);
-        }
-        else if (db_.LookupResource(patient, dummy, instanceToStore.GetHasher().HashPatient()))
-        {
-          assert(dummy == ResourceType_Patient);
-
-          // New study and series: The patient already exist
-          isNewStudy = true;
-          isNewSeries = true;
-        }
-        else
-        {
-          // New patient, study and series: Nothing exists
-          isNewPatient = true;
-          isNewStudy = true;
-          isNewSeries = true;
-        }
-      }
-
-      // Create the series if needed
-      if (isNewSeries)
-      {
-        series = CreateResource(instanceToStore.GetHasher().HashSeries(), ResourceType_Series);
-        ServerToolbox::StoreMainDicomTags(db_, series, ResourceType_Series, dicomSummary);
-      }
-
-      // Create the study if needed
-      if (isNewStudy)
-      {
-        study = CreateResource(instanceToStore.GetHasher().HashStudy(), ResourceType_Study);
-        ServerToolbox::StoreMainDicomTags(db_, study, ResourceType_Study, dicomSummary);
-      }
-
-      // Create the patient if needed
-      if (isNewPatient)
-      {
-        patient = CreateResource(instanceToStore.GetHasher().HashPatient(), ResourceType_Patient);
-        ServerToolbox::StoreMainDicomTags(db_, patient, ResourceType_Patient, dicomSummary);
-      }
-
-      // Create the parent-to-child links
-      db_.AttachChild(series, instance);
-
-      if (isNewSeries)
-      {
-        db_.AttachChild(study, series);
-      }
-
-      if (isNewStudy)
-      {
-        db_.AttachChild(patient, study);
-      }
-
-      // Sanity checks
-      assert(patient != -1);
-      assert(study != -1);
-      assert(series != -1);
-      assert(instance != -1);
-
+      Recycle(instanceSize, hashPatient /* don't consider the current patient for recycling */);
+      
+     
       // Attach the files to the newly created instance
       for (Attachments::const_iterator it = attachments.begin();
            it != attachments.end(); ++it)
       {
-        db_.AddAttachment(instance, *it);
-      }
-
-      // Attach the user-specified metadata
-      for (MetadataMap::const_iterator 
-             it = metadata.begin(); it != metadata.end(); ++it)
-      {
-        switch (it->first.first)
-        {
-          case ResourceType_Patient:
-            db_.SetMetadata(patient, it->first.second, it->second);
-            break;
-
-          case ResourceType_Study:
-            db_.SetMetadata(study, it->first.second, it->second);
-            break;
-
-          case ResourceType_Series:
-            db_.SetMetadata(series, it->first.second, it->second);
-            break;
-
-          case ResourceType_Instance:
-            SetInstanceMetadata(instanceMetadata, instance, it->first.second, it->second);
-            break;
-
-          default:
-            throw OrthancException(ErrorCode_ParameterOutOfRange);
-        }
+        db_.AddAttachment(instanceId, *it);
       }
 
-      // Attach the auto-computed metadata for the patient/study/series levels
-      std::string now = SystemToolbox::GetNowIsoString(true /* use UTC time (not local time) */);
-      db_.SetMetadata(series, MetadataType_LastUpdate, now);
-      db_.SetMetadata(study, MetadataType_LastUpdate, now);
-      db_.SetMetadata(patient, MetadataType_LastUpdate, now);
-
-      // Attach the auto-computed metadata for the instance level,
-      // reflecting these additions into the input metadata map
-      SetInstanceMetadata(instanceMetadata, instance, MetadataType_Instance_ReceptionDate, now);
-      SetInstanceMetadata(instanceMetadata, instance, MetadataType_Instance_RemoteAet,
-                          instanceToStore.GetOrigin().GetRemoteAetC());
-      SetInstanceMetadata(instanceMetadata, instance, MetadataType_Instance_Origin, 
-                          EnumerationToString(instanceToStore.GetOrigin().GetRequestOrigin()));
-
+      
       {
-        std::string s;
-
-        if (instanceToStore.LookupTransferSyntax(s))
+        ResourcesContent content;
+      
+        // Populate the tags of the newly-created resources
+
+        content.AddResource(instanceId, ResourceType_Instance, dicomSummary);
+
+        if (status.isNewSeries_)
+        {
+          content.AddResource(status.seriesId_, ResourceType_Series, dicomSummary);
+        }
+
+        if (status.isNewStudy_)
         {
-          // New in Orthanc 1.2.0
-          SetInstanceMetadata(instanceMetadata, instance, MetadataType_Instance_TransferSyntax, s);
+          content.AddResource(status.studyId_, ResourceType_Study, dicomSummary);
+        }
+
+        if (status.isNewPatient_)
+        {
+          content.AddResource(status.patientId_, ResourceType_Patient, dicomSummary);
         }
 
-        if (instanceToStore.GetOrigin().LookupRemoteIp(s))
+
+        // Attach the user-specified metadata
+
+        for (MetadataMap::const_iterator 
+               it = metadata.begin(); it != metadata.end(); ++it)
         {
-          // New in Orthanc 1.4.0
-          SetInstanceMetadata(instanceMetadata, instance, MetadataType_Instance_RemoteIp, s);
+          switch (it->first.first)
+          {
+            case ResourceType_Patient:
+              content.AddMetadata(status.patientId_, it->first.second, it->second);
+              break;
+
+            case ResourceType_Study:
+              content.AddMetadata(status.studyId_, it->first.second, it->second);
+              break;
+
+            case ResourceType_Series:
+              content.AddMetadata(status.seriesId_, it->first.second, it->second);
+              break;
+
+            case ResourceType_Instance:
+              SetInstanceMetadata(content, instanceMetadata, instanceId,
+                                  it->first.second, it->second);
+              break;
+
+            default:
+              throw OrthancException(ErrorCode_ParameterOutOfRange);
+          }
         }
 
-        if (instanceToStore.GetOrigin().LookupCalledAet(s))
+        
+        // Attach the auto-computed metadata for the patient/study/series levels
+        std::string now = SystemToolbox::GetNowIsoString(true /* use UTC time (not local time) */);
+        content.AddMetadata(status.seriesId_, MetadataType_LastUpdate, now);
+        content.AddMetadata(status.studyId_, MetadataType_LastUpdate, now);
+        content.AddMetadata(status.patientId_, MetadataType_LastUpdate, now);
+
+        if (status.isNewSeries_ &&
+            hasExpectedInstances)
         {
-          // New in Orthanc 1.4.0
-          SetInstanceMetadata(instanceMetadata, instance, MetadataType_Instance_CalledAet, s);
-        }
-
-        if (instanceToStore.GetOrigin().LookupHttpUsername(s))
-        {
-          // New in Orthanc 1.4.0
-          SetInstanceMetadata(instanceMetadata, instance, MetadataType_Instance_HttpUsername, s);
+          content.AddMetadata(status.seriesId_, MetadataType_Series_ExpectedNumberOfInstances,
+                              boost::lexical_cast<std::string>(expectedInstances));
         }
-      }
-
-      const DicomValue* value;
-      if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_SOP_CLASS_UID)) != NULL &&
-          !value->IsNull() &&
-          !value->IsBinary())
-      {
-        SetInstanceMetadata(instanceMetadata, instance, MetadataType_Instance_SopClassUid, value->GetContent());
-      }
-
-      if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_INSTANCE_NUMBER)) != NULL ||
-          (value = dicomSummary.TestAndGetValue(DICOM_TAG_IMAGE_INDEX)) != NULL)
-      {
-        if (!value->IsNull() && 
+
+        
+        // Attach the auto-computed metadata for the instance level,
+        // reflecting these additions into the input metadata map
+        SetInstanceMetadata(content, instanceMetadata, instanceId,
+                            MetadataType_Instance_ReceptionDate, now);
+        SetInstanceMetadata(content, instanceMetadata, instanceId, MetadataType_Instance_RemoteAet,
+                            instanceToStore.GetOrigin().GetRemoteAetC());
+        SetInstanceMetadata(content, instanceMetadata, instanceId, MetadataType_Instance_Origin, 
+                            EnumerationToString(instanceToStore.GetOrigin().GetRequestOrigin()));
+
+
+        {
+          std::string s;
+
+          if (instanceToStore.LookupTransferSyntax(s))
+          {
+            // New in Orthanc 1.2.0
+            SetInstanceMetadata(content, instanceMetadata, instanceId,
+                                MetadataType_Instance_TransferSyntax, s);
+          }
+
+          if (instanceToStore.GetOrigin().LookupRemoteIp(s))
+          {
+            // New in Orthanc 1.4.0
+            SetInstanceMetadata(content, instanceMetadata, instanceId,
+                                MetadataType_Instance_RemoteIp, s);
+          }
+
+          if (instanceToStore.GetOrigin().LookupCalledAet(s))
+          {
+            // New in Orthanc 1.4.0
+            SetInstanceMetadata(content, instanceMetadata, instanceId,
+                                MetadataType_Instance_CalledAet, s);
+          }
+
+          if (instanceToStore.GetOrigin().LookupHttpUsername(s))
+          {
+            // New in Orthanc 1.4.0
+            SetInstanceMetadata(content, instanceMetadata, instanceId,
+                                MetadataType_Instance_HttpUsername, s);
+          }
+        }
+
+        
+        const DicomValue* value;
+        if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_SOP_CLASS_UID)) != NULL &&
+            !value->IsNull() &&
             !value->IsBinary())
         {
-          SetInstanceMetadata(instanceMetadata, instance, MetadataType_Instance_IndexInSeries, value->GetContent());
+          SetInstanceMetadata(content, instanceMetadata, instanceId,
+                              MetadataType_Instance_SopClassUid, value->GetContent());
         }
+
+
+        if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_INSTANCE_NUMBER)) != NULL ||
+            (value = dicomSummary.TestAndGetValue(DICOM_TAG_IMAGE_INDEX)) != NULL)
+        {
+          if (!value->IsNull() && 
+              !value->IsBinary())
+          {
+            SetInstanceMetadata(content, instanceMetadata, instanceId,
+                                MetadataType_Instance_IndexInSeries, value->GetContent());
+          }
+        }
+
+        
+        db_.SetResourcesContent(content);
       }
 
+  
       // Check whether the series of this new instance is now completed
-      if (isNewSeries)
-      {
-        ComputeExpectedNumberOfInstances(db_, series, dicomSummary);
-      }
-
-      SeriesStatus seriesStatus = GetSeriesStatus(series);
+      SeriesStatus seriesStatus = GetSeriesStatus(status.seriesId_);
       if (seriesStatus == SeriesStatus_Complete)
       {
-        LogChange(series, ChangeType_CompletedSeries, ResourceType_Series, instanceToStore.GetHasher().HashSeries());
+        LogChange(status.seriesId_, ChangeType_CompletedSeries, ResourceType_Series, hashSeries);
       }
+      
 
       // Mark the parent resources of this instance as unstable
-      MarkAsUnstable(series, ResourceType_Series, instanceToStore.GetHasher().HashSeries());
-      MarkAsUnstable(study, ResourceType_Study, instanceToStore.GetHasher().HashStudy());
-      MarkAsUnstable(patient, ResourceType_Patient, instanceToStore.GetHasher().HashPatient());
+      MarkAsUnstable(status.seriesId_, ResourceType_Series, hashSeries);
+      MarkAsUnstable(status.studyId_, ResourceType_Study, hashStudy);
+      MarkAsUnstable(status.patientId_, ResourceType_Patient, hashPatient);
 
       t.Commit(instanceSize);
 
@@ -877,8 +977,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);
@@ -892,32 +991,30 @@
   }          
 
 
-
-  SeriesStatus ServerIndex::GetSeriesStatus(int64_t id)
+  
+  SeriesStatus ServerIndex::GetSeriesStatus(int64_t id,
+                                            int64_t expectedNumberOfInstances)
   {
-    // Get the expected number of instances in this series (from the metadata)
-    int64_t expected;
-    if (!GetMetadataAsInteger(expected, id, MetadataType_Series_ExpectedNumberOfInstances))
-    {
-      return SeriesStatus_Unknown;
-    }
-
-    // Loop over the instances of this series
-    std::list<int64_t> children;
-    db_.GetChildrenInternalId(children, id);
+    std::list<std::string> values;
+    db_.GetChildrenMetadata(values, id, MetadataType_Instance_IndexInSeries);
 
     std::set<int64_t> instances;
-    for (std::list<int64_t>::const_iterator 
-           it = children.begin(); it != children.end(); ++it)
+
+    for (std::list<std::string>::const_iterator
+           it = values.begin(); it != values.end(); ++it)
     {
-      // Get the index of this instance in the series
       int64_t index;
-      if (!GetMetadataAsInteger(index, *it, MetadataType_Instance_IndexInSeries))
+
+      try
+      {
+        index = boost::lexical_cast<int64_t>(*it);
+      }
+      catch (boost::bad_lexical_cast&)
       {
         return SeriesStatus_Unknown;
       }
-
-      if (!(index > 0 && index <= expected))
+      
+      if (!(index > 0 && index <= expectedNumberOfInstances))
       {
         // Out-of-range instance index
         return SeriesStatus_Inconsistent;
@@ -932,7 +1029,7 @@
       instances.insert(index);
     }
 
-    if (static_cast<int64_t>(instances.size()) == expected)
+    if (static_cast<int64_t>(instances.size()) == expectedNumberOfInstances)
     {
       return SeriesStatus_Complete;
     }
@@ -943,6 +1040,21 @@
   }
 
 
+  SeriesStatus ServerIndex::GetSeriesStatus(int64_t id)
+  {
+    // Get the expected number of instances in this series (from the metadata)
+    int64_t expected;
+    if (!GetMetadataAsInteger(expected, id, MetadataType_Series_ExpectedNumberOfInstances))
+    {
+      return SeriesStatus_Unknown;
+    }
+    else
+    {
+      return GetSeriesStatus(id, expected);
+    }
+  }
+
+
   void ServerIndex::MainDicomTagsToJson(Json::Value& target,
                                         int64_t resourceId,
                                         ResourceType resourceType)
@@ -969,6 +1081,7 @@
     }
   }
 
+  
   bool ServerIndex::LookupResource(Json::Value& result,
                                    const std::string& publicId,
                                    ResourceType expectedType)
@@ -1067,9 +1180,13 @@
 
         int64_t i;
         if (GetMetadataAsInteger(i, id, MetadataType_Series_ExpectedNumberOfInstances))
+        {
           result["ExpectedNumberOfInstances"] = static_cast<int>(i);
+        }
         else
+        {
           result["ExpectedNumberOfInstances"] = Json::nullValue;
+        }
 
         break;
       }
@@ -1089,9 +1206,13 @@
 
         int64_t i;
         if (GetMetadataAsInteger(i, id, MetadataType_Instance_IndexInSeries))
+        {
           result["IndexInSeries"] = static_cast<int>(i);
+        }
         else
+        {
           result["IndexInSeries"] = Json::nullValue;
+        }
 
         break;
       }
@@ -1187,7 +1308,9 @@
                         const std::list<T>& log,
                         const std::string& name,
                         bool done,
-                        int64_t since)
+                        int64_t since,
+                        bool hasLast,
+                        int64_t last)
   {
     Json::Value items = Json::arrayValue;
     for (typename std::list<T>::const_iterator
@@ -1202,7 +1325,19 @@
     target[name] = items;
     target["Done"] = done;
 
-    int64_t last = (log.empty() ? since : log.back().GetSeq());
+    if (!hasLast)
+    {
+      // Best-effort guess of the last index in the sequence
+      if (log.empty())
+      {
+        last = since;
+      }
+      else
+      {
+        last = log.back().GetSeq();
+      }
+    }
+    
     target["Last"] = static_cast<int>(last);
   }
 
@@ -1213,6 +1348,8 @@
   {
     std::list<ServerIndexChange> changes;
     bool done;
+    bool hasLast = false;
+    int64_t last = 0;
 
     {
       boost::mutex::scoped_lock lock(mutex_);
@@ -1220,17 +1357,26 @@
       // Fix wrt. Orthanc <= 1.3.2: A transaction was missing, as
       // "GetLastChange()" involves calls to "GetPublicId()"
       Transaction transaction(*this);
+
       db_.GetChanges(changes, done, since, maxResults);
+      if (changes.empty())
+      {
+        last = db_.GetLastChangeIndex();
+        hasLast = true;
+      }
+      
       transaction.Commit(0);
     }
 
-    FormatLog(target, changes, "Changes", done, since);
+    FormatLog(target, changes, "Changes", done, since, hasLast, last);
   }
 
 
   void ServerIndex::GetLastChange(Json::Value& target)
   {
     std::list<ServerIndexChange> changes;
+    bool hasLast = false;
+    int64_t last = 0;
 
     {
       boost::mutex::scoped_lock lock(mutex_);
@@ -1238,11 +1384,18 @@
       // Fix wrt. Orthanc <= 1.3.2: A transaction was missing, as
       // "GetLastChange()" involves calls to "GetPublicId()"
       Transaction transaction(*this);
+
       db_.GetLastChange(changes);
+      if (changes.empty())
+      {
+        last = db_.GetLastChangeIndex();
+        hasLast = true;
+      }
+
       transaction.Commit(0);
     }
 
-    FormatLog(target, changes, "Changes", true, 0);
+    FormatLog(target, changes, "Changes", true, 0, hasLast, last);
   }
 
 
@@ -1348,7 +1501,7 @@
       db_.GetExportedResources(exported, done, since, maxResults);
     }
 
-    FormatLog(target, exported, "Exports", done, since);
+    FormatLog(target, exported, "Exports", done, since, false, -1);
   }
 
 
@@ -1361,7 +1514,7 @@
       db_.GetLastExportedResource(exported);
     }
 
-    FormatLog(target, exported, "Exports", true, 0);
+    FormatLog(target, exported, "Exports", true, 0, false, -1);
   }
 
 
@@ -1369,10 +1522,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 +2182,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 +2195,19 @@
     
     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(c.ConvertToDatabaseConstraint(level, DicomTagType_Identifier));
+
+    std::list<std::string> tmp;
+    
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+      db_.ApplyLookupResources(tmp, NULL, query, level, 0);
+    }
+
+    CopyListToVector(result, tmp);
   }
 
 
@@ -2337,36 +2497,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)
@@ -2432,10 +2562,14 @@
       db_.ClearMainDicomTags(series);
       db_.ClearMainDicomTags(instance);
 
-      ServerToolbox::StoreMainDicomTags(db_, patient, ResourceType_Patient, summary);
-      ServerToolbox::StoreMainDicomTags(db_, study, ResourceType_Study, summary);
-      ServerToolbox::StoreMainDicomTags(db_, series, ResourceType_Series, summary);
-      ServerToolbox::StoreMainDicomTags(db_, instance, ResourceType_Instance, summary);
+      {
+        ResourcesContent content;
+        content.AddResource(patient, ResourceType_Patient, summary);
+        content.AddResource(study, ResourceType_Study, summary);
+        content.AddResource(series, ResourceType_Series, summary);
+        content.AddResource(instance, ResourceType_Instance, summary);
+        db_.SetResourcesContent(content);
+      }
 
       {
         std::string s;
@@ -2460,4 +2594,69 @@
       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;
+        }
+        
+        target.push_back(source.GetConstraint(i).ConvertToDatabaseConstraint(level, type));
+      }
+    }
+  }
+
+
+  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);
+
+    std::list<std::string> resourcesList, instancesList;
+    
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+
+      if (instancesId == NULL)
+      {
+        db_.ApplyLookupResources(resourcesList, NULL, normalized, queryLevel, limit);
+      }
+      else
+      {
+        db_.ApplyLookupResources(resourcesList, &instancesList, normalized, queryLevel, limit);
+      }
+    }
+
+    CopyListToVector(resourcesId, resourcesList);
+
+    if (instancesId != NULL)
+    { 
+      CopyListToVector(*instancesId, instancesList);
+    }
+  }
 }
--- a/OrthancServer/ServerIndex.h	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/ServerIndex.h	Tue Jan 15 21:11:44 2019 +0100
@@ -33,21 +33,20 @@
 
 #pragma once
 
+#include "../Core/Cache/LeastRecentlyUsedIndex.h"
+#include "../Core/DicomFormat/DicomMap.h"
+
+#include "Database/IDatabaseWrapper.h"
+
 #include <boost/thread.hpp>
 #include <boost/noncopyable.hpp>
-#include "../Core/Cache/LeastRecentlyUsedIndex.h"
-#include "../Core/SQLite/Connection.h"
-#include "../Core/DicomFormat/DicomMap.h"
-#include "ServerEnumerations.h"
-
-#include "IDatabaseWrapper.h"
 
 namespace Orthanc
 {
-  class LookupResource;
-  class ServerContext;
+  class DatabaseLookup;
   class DicomInstanceToStore;
   class ParsedDicomFile;
+  class ServerContext;
 
   class ServerIndex : public boost::noncopyable
   {
@@ -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);
@@ -116,15 +116,19 @@
                    ResourceType resourceType,
                    const std::string& publicId);
 
+    void SignalNewResource(ChangeType changeType,
+                           ResourceType level,
+                           const std::string& publicId,
+                           int64_t internalId);
+
     uint64_t IncrementGlobalSequenceInternal(GlobalProperty property);
 
-    int64_t CreateResource(const std::string& publicId,
-                           ResourceType type);
+    void NormalizeLookup(std::vector<DatabaseConstraint>& target,
+                         const DatabaseLookup& source,
+                         ResourceType level) const;
 
-    void SetInstanceMetadata(std::map<MetadataType, std::string>& instanceMetadata,
-                             int64_t instance,
-                             MetadataType metadata,
-                             const std::string& value);
+    SeriesStatus GetSeriesStatus(int64_t id,
+                                 int64_t expectedNumberOfInstances);
 
   public:
     ServerIndex(ServerContext& context,
@@ -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/ServerJobs/ArchiveJob.cpp	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/ServerJobs/ArchiveJob.cpp	Tue Jan 15 21:11:44 2019 +0100
@@ -34,10 +34,12 @@
 #include "../PrecompiledHeadersServer.h"
 #include "ArchiveJob.h"
 
+#include "../../Core/Cache/SharedArchive.h"
 #include "../../Core/Compression/HierarchicalZipWriter.h"
 #include "../../Core/DicomParsing/DicomDirWriter.h"
 #include "../../Core/Logging.h"
 #include "../../Core/OrthancException.h"
+#include "../ServerContext.h"
 
 #include <stdio.h>
 
--- a/OrthancServer/ServerJobs/ArchiveJob.h	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/ServerJobs/ArchiveJob.h	Tue Jan 15 21:11:44 2019 +0100
@@ -35,10 +35,14 @@
 
 #include "../../Core/JobsEngine/IJob.h"
 #include "../../Core/TemporaryFile.h"
-#include "../ServerContext.h"
+
+#include <boost/shared_ptr.hpp>
+#include <stdint.h>
 
 namespace Orthanc
 {
+  class ServerContext;
+  
   class ArchiveJob : public IJob
   {
   private:
--- a/OrthancServer/ServerJobs/DicomModalityStoreJob.cpp	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/ServerJobs/DicomModalityStoreJob.cpp	Tue Jan 15 21:11:44 2019 +0100
@@ -36,6 +36,8 @@
 
 #include "../../Core/Logging.h"
 #include "../../Core/SerializationToolbox.h"
+#include "../ServerContext.h"
+
 
 namespace Orthanc
 {
--- a/OrthancServer/ServerJobs/DicomModalityStoreJob.h	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/ServerJobs/DicomModalityStoreJob.h	Tue Jan 15 21:11:44 2019 +0100
@@ -36,10 +36,10 @@
 #include "../../Core/JobsEngine/SetOfInstancesJob.h"
 #include "../../Core/DicomNetworking/DicomUserConnection.h"
 
-#include "../ServerContext.h"
-
 namespace Orthanc
 {
+  class ServerContext;
+  
   class DicomModalityStoreJob : public SetOfInstancesJob
   {
   private:
--- a/OrthancServer/ServerJobs/DicomMoveScuJob.cpp	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/ServerJobs/DicomMoveScuJob.cpp	Tue Jan 15 21:11:44 2019 +0100
@@ -34,6 +34,7 @@
 #include "DicomMoveScuJob.h"
 
 #include "../../Core/SerializationToolbox.h"
+#include "../ServerContext.h"
 
 namespace Orthanc
 {
--- a/OrthancServer/ServerJobs/DicomMoveScuJob.h	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/ServerJobs/DicomMoveScuJob.h	Tue Jan 15 21:11:44 2019 +0100
@@ -37,10 +37,11 @@
 #include "../../Core/DicomNetworking/DicomUserConnection.h"
 
 #include "../QueryRetrieveHandler.h"
-#include "../ServerContext.h"
 
 namespace Orthanc
 {
+  class ServerContext;
+  
   class DicomMoveScuJob : public SetOfCommandsJob
   {
   private:
--- a/OrthancServer/ServerJobs/MergeStudyJob.cpp	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/ServerJobs/MergeStudyJob.cpp	Tue Jan 15 21:11:44 2019 +0100
@@ -36,6 +36,7 @@
 #include "../../Core/DicomParsing/FromDcmtkBridge.h"
 #include "../../Core/Logging.h"
 #include "../../Core/SerializationToolbox.h"
+#include "../ServerContext.h"
 
 
 namespace Orthanc
--- a/OrthancServer/ServerJobs/MergeStudyJob.h	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/ServerJobs/MergeStudyJob.h	Tue Jan 15 21:11:44 2019 +0100
@@ -33,12 +33,14 @@
 
 #pragma once
 
+#include "../../Core/DicomFormat/DicomMap.h"
 #include "../../Core/JobsEngine/SetOfInstancesJob.h"
-
-#include "../ServerContext.h"
+#include "../DicomInstanceOrigin.h"
 
 namespace Orthanc
 {
+  class ServerContext;
+  
   class MergeStudyJob : public SetOfInstancesJob
   {
   private:
--- a/OrthancServer/ServerJobs/Operations/DeleteResourceOperation.cpp	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/ServerJobs/Operations/DeleteResourceOperation.cpp	Tue Jan 15 21:11:44 2019 +0100
@@ -35,6 +35,7 @@
 #include "DeleteResourceOperation.h"
 
 #include "DicomInstanceOperationValue.h"
+#include "../../ServerContext.h"
 
 #include "../../../Core/Logging.h"
 #include "../../../Core/OrthancException.h"
--- a/OrthancServer/ServerJobs/Operations/DeleteResourceOperation.h	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/ServerJobs/Operations/DeleteResourceOperation.h	Tue Jan 15 21:11:44 2019 +0100
@@ -35,10 +35,10 @@
 
 #include "../../../Core/JobsEngine/Operations/IJobOperation.h"
 
-#include "../../ServerContext.h"
-
 namespace Orthanc
 {
+  class ServerContext;
+  
   class DeleteResourceOperation : public IJobOperation
   {
   private:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/Operations/DicomInstanceOperationValue.cpp	Tue Jan 15 21:11:44 2019 +0100
@@ -0,0 +1,45 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 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 "DicomInstanceOperationValue.h"
+
+#include "../../ServerContext.h"
+
+namespace Orthanc
+{
+  void DicomInstanceOperationValue::ReadDicom(std::string& dicom) const
+  {
+    context_.ReadDicom(dicom, id_);
+  }
+}
--- a/OrthancServer/ServerJobs/Operations/DicomInstanceOperationValue.h	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/ServerJobs/Operations/DicomInstanceOperationValue.h	Tue Jan 15 21:11:44 2019 +0100
@@ -35,10 +35,10 @@
 
 #include "../../../Core/JobsEngine/Operations/JobOperationValue.h"
 
-#include "../../ServerContext.h"
-
 namespace Orthanc
 {
+  class ServerContext;
+  
   class DicomInstanceOperationValue : public JobOperationValue
   {
   private:
@@ -64,10 +64,7 @@
       return id_;
     }
 
-    void ReadDicom(std::string& dicom) const
-    {
-      context_.ReadDicom(dicom, id_);
-    }
+    void ReadDicom(std::string& dicom) const;
 
     virtual JobOperationValue* Clone() const
     {
--- a/OrthancServer/ServerJobs/Operations/ModifyInstanceOperation.cpp	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/ServerJobs/Operations/ModifyInstanceOperation.cpp	Tue Jan 15 21:11:44 2019 +0100
@@ -35,6 +35,7 @@
 #include "ModifyInstanceOperation.h"
 
 #include "DicomInstanceOperationValue.h"
+#include "../../ServerContext.h"
 
 #include "../../../Core/Logging.h"
 #include "../../../Core/SerializationToolbox.h"
--- a/OrthancServer/ServerJobs/Operations/ModifyInstanceOperation.h	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/ServerJobs/Operations/ModifyInstanceOperation.h	Tue Jan 15 21:11:44 2019 +0100
@@ -36,10 +36,10 @@
 #include "../../../Core/JobsEngine/Operations/IJobOperation.h"
 #include "../../../Core/DicomParsing/DicomModification.h"
 
-#include "../../ServerContext.h"
-
 namespace Orthanc
 {
+  class ServerContext;
+  
   class ModifyInstanceOperation : public IJobOperation
   {
   private:
--- a/OrthancServer/ServerJobs/Operations/SystemCallOperation.cpp	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/ServerJobs/Operations/SystemCallOperation.cpp	Tue Jan 15 21:11:44 2019 +0100
@@ -42,6 +42,7 @@
 #include "../../../Core/SerializationToolbox.h"
 #include "../../../Core/TemporaryFile.h"
 #include "../../../Core/Toolbox.h"
+#include "../../../Core/SystemToolbox.h"
 
 namespace Orthanc
 {
--- a/OrthancServer/ServerJobs/OrthancJobUnserializer.cpp	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/ServerJobs/OrthancJobUnserializer.cpp	Tue Jan 15 21:11:44 2019 +0100
@@ -37,6 +37,8 @@
 #include "../../Core/Logging.h"
 #include "../../Core/OrthancException.h"
 #include "../../Core/SerializationToolbox.h"
+#include "../../Plugins/Engine/OrthancPlugins.h"
+#include "../ServerContext.h"
 
 #include "Operations/DeleteResourceOperation.h"
 #include "Operations/DicomInstanceOperationValue.h"
@@ -52,6 +54,7 @@
 #include "MergeStudyJob.h"
 #include "SplitStudyJob.h"
 
+
 namespace Orthanc
 {
   IJob* OrthancJobUnserializer::UnserializeJob(const Json::Value& source)
--- a/OrthancServer/ServerJobs/OrthancJobUnserializer.h	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/ServerJobs/OrthancJobUnserializer.h	Tue Jan 15 21:11:44 2019 +0100
@@ -33,11 +33,12 @@
 
 #pragma once
 
-#include "../ServerContext.h"
 #include "../../Core/JobsEngine/GenericJobUnserializer.h"
 
 namespace Orthanc
 {
+  class ServerContext;
+  
   class OrthancJobUnserializer : public GenericJobUnserializer
   {
   private:
--- a/OrthancServer/ServerJobs/OrthancPeerStoreJob.cpp	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/ServerJobs/OrthancPeerStoreJob.cpp	Tue Jan 15 21:11:44 2019 +0100
@@ -35,6 +35,7 @@
 #include "OrthancPeerStoreJob.h"
 
 #include "../../Core/Logging.h"
+#include "../ServerContext.h"
 
 
 namespace Orthanc
--- a/OrthancServer/ServerJobs/OrthancPeerStoreJob.h	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/ServerJobs/OrthancPeerStoreJob.h	Tue Jan 15 21:11:44 2019 +0100
@@ -36,11 +36,11 @@
 #include "../../Core/JobsEngine/SetOfInstancesJob.h"
 #include "../../Core/HttpClient.h"
 
-#include "../ServerContext.h"
-
 
 namespace Orthanc
 {
+  class ServerContext;
+  
   class OrthancPeerStoreJob : public SetOfInstancesJob
   {
   private:
--- a/OrthancServer/ServerJobs/ResourceModificationJob.cpp	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/ServerJobs/ResourceModificationJob.cpp	Tue Jan 15 21:11:44 2019 +0100
@@ -36,6 +36,7 @@
 
 #include "../../Core/Logging.h"
 #include "../../Core/SerializationToolbox.h"
+#include "../ServerContext.h"
 
 namespace Orthanc
 {
--- a/OrthancServer/ServerJobs/ResourceModificationJob.h	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/ServerJobs/ResourceModificationJob.h	Tue Jan 15 21:11:44 2019 +0100
@@ -34,10 +34,13 @@
 #pragma once
 
 #include "../../Core/JobsEngine/SetOfInstancesJob.h"
-#include "../ServerContext.h"
+#include "../../Core/DicomParsing/DicomModification.h"
+#include "../DicomInstanceOrigin.h"
 
 namespace Orthanc
 {
+  class ServerContext;
+  
   class ResourceModificationJob : public SetOfInstancesJob
   {
   private:
--- a/OrthancServer/ServerJobs/SplitStudyJob.cpp	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/ServerJobs/SplitStudyJob.cpp	Tue Jan 15 21:11:44 2019 +0100
@@ -36,6 +36,8 @@
 #include "../../Core/DicomParsing/FromDcmtkBridge.h"
 #include "../../Core/Logging.h"
 #include "../../Core/SerializationToolbox.h"
+#include "../ServerContext.h"
+
 
 namespace Orthanc
 {
--- a/OrthancServer/ServerJobs/SplitStudyJob.h	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/ServerJobs/SplitStudyJob.h	Tue Jan 15 21:11:44 2019 +0100
@@ -34,11 +34,13 @@
 #pragma once
 
 #include "../../Core/JobsEngine/SetOfInstancesJob.h"
-
-#include "../ServerContext.h"
+#include "../../Core/DicomFormat/DicomTag.h"
+#include "../DicomInstanceOrigin.h"
 
 namespace Orthanc
 {
+  class ServerContext;
+  
   class SplitStudyJob : public SetOfInstancesJob
   {
   private:
--- a/OrthancServer/ServerToolbox.cpp	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/ServerToolbox.cpp	Tue Jan 15 21:11:44 2019 +0100
@@ -35,45 +35,134 @@
 #include "ServerToolbox.h"
 
 #include "../Core/DicomFormat/DicomArray.h"
+#include "../Core/DicomParsing/ParsedDicomFile.h"
 #include "../Core/FileStorage/StorageAccessor.h"
 #include "../Core/Logging.h"
 #include "../Core/OrthancException.h"
+#include "Database/IDatabaseWrapper.h"
+#include "Database/ResourcesContent.h"
+#include "ServerContext.h"
 
 #include <cassert>
 
 namespace Orthanc
 {
+  static const DicomTag PATIENT_IDENTIFIERS[] = 
+  {
+    DICOM_TAG_PATIENT_ID,
+    DICOM_TAG_PATIENT_NAME,
+    DICOM_TAG_PATIENT_BIRTH_DATE
+  };
+
+  static const DicomTag STUDY_IDENTIFIERS[] = 
+  {
+    DICOM_TAG_PATIENT_ID,
+    DICOM_TAG_PATIENT_NAME,
+    DICOM_TAG_PATIENT_BIRTH_DATE,
+    DICOM_TAG_STUDY_INSTANCE_UID,
+    DICOM_TAG_ACCESSION_NUMBER,
+    DICOM_TAG_STUDY_DESCRIPTION,
+    DICOM_TAG_STUDY_DATE
+  };
+
+  static const DicomTag SERIES_IDENTIFIERS[] = 
+  {
+    DICOM_TAG_SERIES_INSTANCE_UID
+  };
+
+  static const DicomTag INSTANCE_IDENTIFIERS[] = 
+  {
+    DICOM_TAG_SOP_INSTANCE_UID
+  };
+
+
+  static void StoreMainDicomTagsInternal(ResourcesContent& target,
+                                         int64_t resource,
+                                         const DicomMap& tags)
+  {
+    DicomArray flattened(tags);
+
+    for (size_t i = 0; i < flattened.GetSize(); i++)
+    {
+      const DicomElement& element = flattened.GetElement(i);
+      const DicomTag& tag = element.GetTag();
+      const DicomValue& value = element.GetValue();
+      if (!value.IsNull() && 
+          !value.IsBinary())
+      {
+        target.AddMainDicomTag(resource, tag, element.GetValue().GetContent());
+      }
+    }
+  }
+
+
+  static void StoreIdentifiers(ResourcesContent& target,
+                               int64_t resource,
+                               ResourceType level,
+                               const DicomMap& map)
+  {
+    const DicomTag* tags;
+    size_t size;
+
+    ServerToolbox::LoadIdentifiers(tags, size, level);
+
+    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() &&
+          !value->IsBinary())
+      {
+        std::string s = ServerToolbox::NormalizeIdentifier(value->GetContent());
+        target.AddIdentifierTag(resource, tags[i], s);
+      }
+    }
+  }
+
+
+  void ResourcesContent::AddResource(int64_t resource,
+                                     ResourceType level,
+                                     const DicomMap& dicomSummary)
+  {
+    StoreIdentifiers(*this, resource, level, dicomSummary);
+
+    DicomMap tags;
+
+    switch (level)
+    {
+      case ResourceType_Patient:
+        dicomSummary.ExtractPatientInformation(tags);
+        break;
+
+      case ResourceType_Study:
+        // Duplicate the patient tags at the study level (new in Orthanc 0.9.5 - db v6)
+        dicomSummary.ExtractPatientInformation(tags);
+        StoreMainDicomTagsInternal(*this, resource, tags);
+
+        dicomSummary.ExtractStudyInformation(tags);
+        break;
+
+      case ResourceType_Series:
+        dicomSummary.ExtractSeriesInformation(tags);
+        break;
+
+      case ResourceType_Instance:
+        dicomSummary.ExtractInstanceInformation(tags);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+    StoreMainDicomTagsInternal(*this, resource, tags);
+  }
+
+
   namespace ServerToolbox
   {
-    static const DicomTag patientIdentifiers[] = 
-    {
-      DICOM_TAG_PATIENT_ID,
-      DICOM_TAG_PATIENT_NAME,
-      DICOM_TAG_PATIENT_BIRTH_DATE
-    };
-
-    static const DicomTag studyIdentifiers[] = 
-    {
-      DICOM_TAG_PATIENT_ID,
-      DICOM_TAG_PATIENT_NAME,
-      DICOM_TAG_PATIENT_BIRTH_DATE,
-      DICOM_TAG_STUDY_INSTANCE_UID,
-      DICOM_TAG_ACCESSION_NUMBER,
-      DICOM_TAG_STUDY_DESCRIPTION,
-      DICOM_TAG_STUDY_DATE
-    };
-
-    static const DicomTag seriesIdentifiers[] = 
-    {
-      DICOM_TAG_SERIES_INSTANCE_UID
-    };
-
-    static const DicomTag instanceIdentifiers[] = 
-    {
-      DICOM_TAG_SOP_INSTANCE_UID
-    };
-
-
     void SimplifyTags(Json::Value& target,
                       const Json::Value& source,
                       DicomToJsonFormat format)
@@ -138,91 +227,6 @@
     }
 
 
-    static void StoreMainDicomTagsInternal(IDatabaseWrapper& database,
-                                           int64_t resource,
-                                           const DicomMap& tags)
-    {
-      DicomArray flattened(tags);
-
-      for (size_t i = 0; i < flattened.GetSize(); i++)
-      {
-        const DicomElement& element = flattened.GetElement(i);
-        const DicomTag& tag = element.GetTag();
-        const DicomValue& value = element.GetValue();
-        if (!value.IsNull() && 
-            !value.IsBinary())
-        {
-          database.SetMainDicomTag(resource, tag, element.GetValue().GetContent());
-        }
-      }
-    }
-
-
-    static void StoreIdentifiers(IDatabaseWrapper& database,
-                                 int64_t resource,
-                                 ResourceType level,
-                                 const DicomMap& map)
-    {
-      const DicomTag* tags;
-      size_t size;
-
-      LoadIdentifiers(tags, size, level);
-
-      for (size_t i = 0; i < size; i++)
-      {
-        const DicomValue* value = map.TestAndGetValue(tags[i]);
-        if (value != NULL &&
-            !value->IsNull() &&
-            !value->IsBinary())
-        {
-          std::string s = NormalizeIdentifier(value->GetContent());
-          database.SetIdentifierTag(resource, tags[i], s);
-        }
-      }
-    }
-
-
-    void StoreMainDicomTags(IDatabaseWrapper& database,
-                            int64_t resource,
-                            ResourceType level,
-                            const DicomMap& dicomSummary)
-    {
-      // WARNING: The database should be locked with a transaction!
-
-      StoreIdentifiers(database, resource, level, dicomSummary);
-
-      DicomMap tags;
-
-      switch (level)
-      {
-        case ResourceType_Patient:
-          dicomSummary.ExtractPatientInformation(tags);
-          break;
-
-        case ResourceType_Study:
-          // Duplicate the patient tags at the study level (new in Orthanc 0.9.5 - db v6)
-          dicomSummary.ExtractPatientInformation(tags);
-          StoreMainDicomTagsInternal(database, resource, tags);
-
-          dicomSummary.ExtractStudyInformation(tags);
-          break;
-
-        case ResourceType_Series:
-          dicomSummary.ExtractSeriesInformation(tags);
-          break;
-
-        case ResourceType_Instance:
-          dicomSummary.ExtractInstanceInformation(tags);
-          break;
-
-        default:
-          throw OrthancException(ErrorCode_InternalError);
-      }
-
-      StoreMainDicomTagsInternal(database, resource, tags);
-    }
-
-
     bool FindOneChildInstance(int64_t& result,
                               IDatabaseWrapper& database,
                               int64_t resource,
@@ -332,7 +336,10 @@
           dicom.ExtractDicomSummary(dicomSummary);
 
           database.ClearMainDicomTags(resource);
-          StoreMainDicomTags(database, resource, level, dicomSummary);
+
+          ResourcesContent tags;
+          tags.AddResource(resource, level, dicomSummary);
+          database.SetResourcesContent(tags);
         }
         catch (OrthancException&)
         {
@@ -351,23 +358,23 @@
       switch (level)
       {
         case ResourceType_Patient:
-          tags = patientIdentifiers;
-          size = sizeof(patientIdentifiers) / sizeof(DicomTag);
+          tags = PATIENT_IDENTIFIERS;
+          size = sizeof(PATIENT_IDENTIFIERS) / sizeof(DicomTag);
           break;
 
         case ResourceType_Study:
-          tags = studyIdentifiers;
-          size = sizeof(studyIdentifiers) / sizeof(DicomTag);
+          tags = STUDY_IDENTIFIERS;
+          size = sizeof(STUDY_IDENTIFIERS) / sizeof(DicomTag);
           break;
 
         case ResourceType_Series:
-          tags = seriesIdentifiers;
-          size = sizeof(seriesIdentifiers) / sizeof(DicomTag);
+          tags = SERIES_IDENTIFIERS;
+          size = sizeof(SERIES_IDENTIFIERS) / sizeof(DicomTag);
           break;
 
         case ResourceType_Instance:
-          tags = instanceIdentifiers;
-          size = sizeof(instanceIdentifiers) / sizeof(DicomTag);
+          tags = INSTANCE_IDENTIFIERS;
+          size = sizeof(INSTANCE_IDENTIFIERS) / sizeof(DicomTag);
           break;
 
         default:
--- a/OrthancServer/ServerToolbox.h	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/ServerToolbox.h	Tue Jan 15 21:11:44 2019 +0100
@@ -33,23 +33,24 @@
 
 #pragma once
 
-#include "ServerContext.h"
+#include "ServerEnumerations.h"
 
 #include <json/json.h>
+#include <boost/noncopyable.hpp>
+#include <list>
 
 namespace Orthanc
 {
+  class ServerContext;
+  class IDatabaseWrapper;
+  class IStorageArea;
+
   namespace ServerToolbox
   {
     void SimplifyTags(Json::Value& target,
                       const Json::Value& source,
                       DicomToJsonFormat format);
 
-    void StoreMainDicomTags(IDatabaseWrapper& database,
-                            int64_t resource,
-                            ResourceType level,
-                            const DicomMap& dicomSummary);
-
     bool FindOneChildInstance(int64_t& result,
                               IDatabaseWrapper& database,
                               int64_t resource,
--- a/OrthancServer/SliceOrdering.cpp	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/SliceOrdering.cpp	Tue Jan 15 21:11:44 2019 +0100
@@ -37,6 +37,7 @@
 #include "../Core/Logging.h"
 #include "../Core/Toolbox.h"
 #include "ServerEnumerations.h"
+#include "ServerIndex.h"
 
 #include <algorithm>
 #include <boost/lexical_cast.hpp>
--- a/OrthancServer/SliceOrdering.h	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/SliceOrdering.h	Tue Jan 15 21:11:44 2019 +0100
@@ -33,10 +33,12 @@
 
 #pragma once
 
-#include "ServerIndex.h"
+#include "../Core/DicomFormat/DicomMap.h"
 
 namespace Orthanc
 {
+  class ServerIndex;
+  
   class SliceOrdering
   {
   private:
--- a/OrthancServer/Upgrade3To4.sql	Tue Jan 15 18:46:59 2019 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,24 +0,0 @@
--- This SQLite script updates the version of the Orthanc database from 3 to 4.
-
--- Add 2 new columns at "AttachedFiles"
-
-ALTER TABLE AttachedFiles ADD COLUMN uncompressedMD5 TEXT;
-ALTER TABLE AttachedFiles ADD COLUMN compressedMD5 TEXT;
-
--- Update the "AttachedFileDeleted" trigger
-
-DROP TRIGGER AttachedFileDeleted;
-
-CREATE TRIGGER AttachedFileDeleted
-AFTER DELETE ON AttachedFiles
-BEGIN
-  SELECT SignalFileDeleted(old.uuid, old.fileType, old.uncompressedSize, 
-                           old.compressionType, old.compressedSize,
-                           -- These 2 arguments are new in Orthanc 0.7.3 (database v4)
-                           old.uncompressedMD5, old.compressedMD5);
-END;
-
--- Change the database version
--- The "1" corresponds to the "GlobalProperty_DatabaseSchemaVersion" enumeration
-
-UPDATE GlobalProperties SET value="4" WHERE property=1;
--- a/OrthancServer/Upgrade4To5.sql	Tue Jan 15 18:46:59 2019 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,66 +0,0 @@
--- This SQLite script updates the version of the Orthanc database from 4 to 5.
-
-
--- Remove 2 indexes to speed up
-
-DROP INDEX MainDicomTagsIndex2;
-DROP INDEX MainDicomTagsIndexValues;
-
-
--- Add a new table to index the DICOM identifiers
-
-CREATE TABLE DicomIdentifiers(
-       id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE,
-       tagGroup INTEGER,
-       tagElement INTEGER,
-       value TEXT,
-       PRIMARY KEY(id, tagGroup, tagElement)
-       );
-
-CREATE INDEX DicomIdentifiersIndex1 ON DicomIdentifiers(id);
-CREATE INDEX DicomIdentifiersIndex2 ON DicomIdentifiers(tagGroup, tagElement);
-CREATE INDEX DicomIdentifiersIndexValues ON DicomIdentifiers(value COLLATE BINARY);
-
-
--- Migrate data from MainDicomTags to MainResourcesTags and MainInstancesTags
-
-INSERT INTO DicomIdentifiers SELECT * FROM MainDicomTags
-       WHERE ((tagGroup = 16 AND tagElement = 32) OR  -- PatientID (0x0010, 0x0020)
-              (tagGroup = 32 AND tagElement = 13) OR  -- StudyInstanceUID (0x0020, 0x000d)
-              (tagGroup = 8  AND tagElement = 80) OR  -- AccessionNumber (0x0008, 0x0050)
-              (tagGroup = 32 AND tagElement = 14) OR  -- SeriesInstanceUID (0x0020, 0x000e)
-              (tagGroup = 8  AND tagElement = 24));   -- SOPInstanceUID (0x0008, 0x0018)
-
-DELETE FROM MainDicomTags
-       WHERE ((tagGroup = 16 AND tagElement = 32) OR  -- PatientID (0x0010, 0x0020)
-              (tagGroup = 32 AND tagElement = 13) OR  -- StudyInstanceUID (0x0020, 0x000d)
-              (tagGroup = 8  AND tagElement = 80) OR  -- AccessionNumber (0x0008, 0x0050)
-              (tagGroup = 32 AND tagElement = 14) OR  -- SeriesInstanceUID (0x0020, 0x000e)
-              (tagGroup = 8  AND tagElement = 24));   -- SOPInstanceUID (0x0008, 0x0018)
-
-
--- Upgrade the "ResourceDeleted" trigger
-
-DROP TRIGGER ResourceDeleted;
-DROP TRIGGER ResourceDeletedParentCleaning;
-
-CREATE TRIGGER ResourceDeleted
-AFTER DELETE ON Resources
-BEGIN
-  SELECT SignalResourceDeleted(old.publicId, old.resourceType);
-  SELECT SignalRemainingAncestor(parent.publicId, parent.resourceType) 
-    FROM Resources AS parent WHERE internalId = old.parentId;
-END;
-
-CREATE TRIGGER ResourceDeletedParentCleaning
-AFTER DELETE ON Resources
-FOR EACH ROW WHEN (SELECT COUNT(*) FROM Resources WHERE parentId = old.parentId) = 0
-BEGIN
-  DELETE FROM Resources WHERE internalId = old.parentId;
-END;
-
-
--- Change the database version
--- The "1" corresponds to the "GlobalProperty_DatabaseSchemaVersion" enumeration
-
-UPDATE GlobalProperties SET value="5" WHERE property=1;
--- a/OrthancServer/main.cpp	Tue Jan 15 18:46:59 2019 +0100
+++ b/OrthancServer/main.cpp	Tue Jan 15 21:11:44 2019 +0100
@@ -1176,7 +1176,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	Tue Jan 15 18:46:59 2019 +0100
+++ b/Plugins/Engine/OrthancPluginDatabase.cpp	Tue Jan 15 21:11:44 2019 +0100
@@ -39,14 +39,70 @@
 #endif
 
 
+#include "../../Core/Logging.h"
 #include "../../Core/OrthancException.h"
-#include "../../Core/Logging.h"
 #include "PluginsEnumerations.h"
 
 #include <cassert>
 
 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,
@@ -77,6 +133,8 @@
     answerChanges_ = NULL;
     answerExportedResources_ = NULL;
     answerDone_ = NULL;
+    answerMatchingResources_ = NULL;
+    answerMatchingInstances_ = NULL;
   }
 
 
@@ -168,15 +226,14 @@
                                                void *payload) : 
     library_(library),
     errorDictionary_(errorDictionary),
-    type_(_OrthancPluginDatabaseAnswerType_None),
     backend_(backend),
     payload_(payload),
-    listener_(NULL),
-    answerDicomMap_(NULL),
-    answerChanges_(NULL),
-    answerExportedResources_(NULL),
-    answerDone_(NULL)
+    listener_(NULL)
   {
+    static const char* const MISSING = "  Missing extension in database index plugin: ";
+    
+    ResetAnswers();
+
     memset(&extensions_, 0, sizeof(extensions_));
 
     size_t size = sizeof(extensions_);
@@ -186,6 +243,83 @@
     }
 
     memcpy(&extensions_, extensions, size);
+
+    bool isOptimal = true;
+
+    if (extensions_.lookupResources == NULL)
+    {
+      LOG(INFO) << MISSING << "LookupIdentifierRange()";
+      isOptimal = false;
+    }
+
+    if (extensions_.createInstance == NULL)
+    {
+      LOG(INFO) << MISSING << "CreateInstance()";
+      isOptimal = false;
+    }
+
+    if (extensions_.setResourcesContent == NULL)
+    {
+      LOG(INFO) << MISSING << "SetResourcesContent()";
+      isOptimal = false;
+    }
+
+    if (extensions_.getChildrenMetadata == NULL)
+    {
+      LOG(INFO) << MISSING << "GetChildrenMetadata()";
+      isOptimal = false;
+    }
+
+    if (isOptimal)
+    {
+      LOG(INFO) << "The performance of the database index plugin "
+                << "is optimal for this version of Orthanc";
+    }
+    else
+    {
+      LOG(WARNING) << "Performance warning in the database index: "
+                   << "Some extensions are missing in the plugin";
+    }
+
+    if (extensions_.getLastChangeIndex == NULL)
+    {
+      LOG(WARNING) << "The database extension GetLastChangeIndex() is missing";
+    }
+
+    if (extensions_.tagMostRecentPatient == NULL)
+    {
+      LOG(WARNING) << "The database extension TagMostRecentPatient() is missing "
+                   << "(affected by issue 58)";
+    }
+  }
+
+
+  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);
+    }
   }
 
 
@@ -281,7 +415,7 @@
     if (extensions_.getAllInternalIds == NULL)
     {
       throw OrthancException(ErrorCode_DatabasePlugin,
-                             "The database plugin does not implement the GetAllInternalIds primitive");
+                             "The database plugin does not implement the mandatory GetAllInternalIds() extension");
     }
 
     ResetAnswers();
@@ -610,58 +744,6 @@
   }
 
 
-  void OrthancPluginDatabase::LookupIdentifier(std::list<int64_t>& result,
-                                               ResourceType level,
-                                               const DicomTag& tag,
-                                               IdentifierConstraintType type,
-                                               const std::string& value)
-  {
-    if (extensions_.lookupIdentifier3 == NULL)
-    {
-      throw OrthancException(ErrorCode_DatabasePlugin,
-                             "The database plugin does not implement the LookupIdentifier3 primitive");
-    }
-
-    OrthancPluginDicomTag tmp;
-    tmp.group = tag.GetGroup();
-    tmp.element = tag.GetElement();
-    tmp.value = value.c_str();
-
-    ResetAnswers();
-    CheckSuccess(extensions_.lookupIdentifier3(GetContext(), payload_, Plugins::Convert(level),
-                                               &tmp, Plugins::Convert(type)));
-    ForwardAnswers(result);
-  }
-
-
-  void OrthancPluginDatabase::LookupIdentifierRange(std::list<int64_t>& result,
-                                                    ResourceType level,
-                                                    const DicomTag& tag,
-                                                    const std::string& start,
-                                                    const std::string& end)
-  {
-    if (extensions_.lookupIdentifierRange == NULL)
-    {
-      // Default implementation, for plugins using Orthanc SDK <= 1.3.2
-
-      LookupIdentifier(result, level, tag, IdentifierConstraintType_GreaterOrEqual, start);
-
-      std::list<int64_t> b;
-      LookupIdentifier(result, level, tag, IdentifierConstraintType_SmallerOrEqual, end);
-
-      result.splice(result.end(), b);
-    }
-    else
-    {
-      ResetAnswers();
-      CheckSuccess(extensions_.lookupIdentifierRange(GetContext(), payload_, Plugins::Convert(level),
-                                                     tag.GetGroup(), tag.GetElement(),
-                                                     start.c_str(), end.c_str()));
-      ForwardAnswers(result);
-    }
-  }
-
-
   bool OrthancPluginDatabase::LookupMetadata(std::string& target,
                                              int64_t id,
                                              MetadataType type)
@@ -737,7 +819,7 @@
     if (extensions_.clearMainDicomTags == NULL)
     {
       throw OrthancException(ErrorCode_DatabasePlugin,
-                             "Your custom index plugin does not implement the ClearMainDicomTags() extension");
+                             "Your custom index plugin does not implement the mandatory ClearMainDicomTags() extension");
     }
 
     CheckSuccess(extensions_.clearMainDicomTags(payload_, id));
@@ -786,52 +868,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 +931,7 @@
   {
     if (extensions_.upgradeDatabase != NULL)
     {
-      Transaction transaction(backend_, payload_, errorDictionary_);
+      Transaction transaction(*this);
       transaction.Begin();
 
       OrthancPluginErrorCode code = extensions_.upgradeDatabase(
@@ -901,7 +940,7 @@
 
       if (code == OrthancPluginErrorCode_Success)
       {
-        transaction.Commit();
+        transaction.Commit(0);
       }
       else
       {
@@ -970,6 +1009,17 @@
           answerExportedResources_->clear();
           break;
 
+        case _OrthancPluginDatabaseAnswerType_MatchingResource:
+          assert(answerMatchingResources_ != NULL);
+          answerMatchingResources_->clear();
+
+          if (answerMatchingInstances_ != NULL)
+          {
+            answerMatchingInstances_->clear();
+          }
+          
+          break;
+
         default:
           throw OrthancException(ErrorCode_DatabasePlugin,
                                  "Unhandled type of answer for custom index plugin: " +
@@ -1054,7 +1104,8 @@
         }
         else
         {
-          const OrthancPluginChange& change = *reinterpret_cast<const OrthancPluginChange*>(answer.valueGeneric);
+          const OrthancPluginChange& change =
+            *reinterpret_cast<const OrthancPluginChange*>(answer.valueGeneric);
           assert(answerChanges_ != NULL);
           answerChanges_->push_back
             (ServerIndexChange(change.seq,
@@ -1098,10 +1149,286 @@
         break;
       }
 
+      case _OrthancPluginDatabaseAnswerType_MatchingResource:
+      {
+        const OrthancPluginMatchingResource& match = 
+          *reinterpret_cast<const OrthancPluginMatchingResource*>(answer.valueGeneric);
+
+        if (match.resourceId == NULL)
+        {
+          throw OrthancException(ErrorCode_DatabasePlugin);
+        }
+
+        assert(answerMatchingResources_ != NULL);
+        answerMatchingResources_->push_back(match.resourceId);
+
+        if (answerMatchingInstances_ != NULL)
+        {
+          if (match.someInstanceId == NULL)
+          {
+            throw OrthancException(ErrorCode_DatabasePlugin);
+          }
+
+          answerMatchingInstances_->push_back(match.someInstanceId);
+        }
+ 
+        break;
+      }
+
       default:
         throw OrthancException(ErrorCode_DatabasePlugin,
                                "Unhandled type of answer for custom index plugin: " +
                                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::list<std::string>& resourcesId,
+                                                   std::list<std::string>* instancesId,
+                                                   const std::vector<DatabaseConstraint>& lookup,
+                                                   ResourceType queryLevel,
+                                                   size_t limit)
+  {
+    if (extensions_.lookupResources == NULL)
+    {
+      // Fallback to compatibility mode
+      ILookupResources::Apply
+        (*this, *this, resourcesId, instancesId, lookup, queryLevel, limit);
+    }
+    else
+    {
+      std::vector<OrthancPluginDatabaseConstraint> constraints;
+      std::vector< std::vector<const char*> > constraintsValues;
+
+      constraints.resize(lookup.size());
+      constraintsValues.resize(lookup.size());
+
+      for (size_t i = 0; i < lookup.size(); i++)
+      {
+        lookup[i].EncodeForPlugins(constraints[i], constraintsValues[i]);
+      }
+
+      ResetAnswers();
+      answerMatchingResources_ = &resourcesId;
+      answerMatchingInstances_ = instancesId;
+      
+      CheckSuccess(extensions_.lookupResources(GetContext(), payload_, lookup.size(),
+                                               (lookup.empty() ? NULL : &constraints[0]),
+                                               Plugins::Convert(queryLevel),
+                                               limit, (instancesId == NULL ? 0 : 1)));
+    }
+  }
+
+
+  bool OrthancPluginDatabase::CreateInstance(
+    IDatabaseWrapper::CreateInstanceResult& result,
+    int64_t& instanceId,
+    const std::string& patient,
+    const std::string& study,
+    const std::string& series,
+    const std::string& instance)
+  {
+    if (extensions_.createInstance == NULL)
+    {
+      // Fallback to compatibility mode
+      return ICreateInstance::Apply
+        (*this, result, instanceId, patient, study, series, instance);
+    }
+    else
+    {
+      OrthancPluginCreateInstanceResult output;
+      memset(&output, 0, sizeof(output));
+
+      CheckSuccess(extensions_.createInstance(&output, payload_, patient.c_str(),
+                                              study.c_str(), series.c_str(), instance.c_str()));
+
+      instanceId = output.instanceId;
+      
+      if (output.isNewInstance)
+      {
+        result.isNewPatient_ = output.isNewPatient;
+        result.isNewStudy_ = output.isNewStudy;
+        result.isNewSeries_ = output.isNewSeries;
+        result.patientId_ = output.patientId;
+        result.studyId_ = output.studyId;
+        result.seriesId_ = output.seriesId;
+        return true;
+      }
+      else
+      {
+        return false;
+      }
+    }
+  }
+
+
+  void OrthancPluginDatabase::LookupIdentifier(std::list<int64_t>& result,
+                                               ResourceType level,
+                                               const DicomTag& tag,
+                                               Compatibility::IdentifierConstraintType type,
+                                               const std::string& value)
+  {
+    if (extensions_.lookupIdentifier3 == NULL)
+    {
+      throw OrthancException(ErrorCode_DatabasePlugin,
+                             "The database plugin does not implement the mandatory LookupIdentifier3() extension");
+    }
+
+    OrthancPluginDicomTag tmp;
+    tmp.group = tag.GetGroup();
+    tmp.element = tag.GetElement();
+    tmp.value = value.c_str();
+
+    ResetAnswers();
+    CheckSuccess(extensions_.lookupIdentifier3(GetContext(), payload_, Plugins::Convert(level),
+                                               &tmp, Compatibility::Convert(type)));
+    ForwardAnswers(result);
+  }
+
+
+  void OrthancPluginDatabase::LookupIdentifierRange(std::list<int64_t>& result,
+                                                    ResourceType level,
+                                                    const DicomTag& tag,
+                                                    const std::string& start,
+                                                    const std::string& end)
+  {
+    if (extensions_.lookupIdentifierRange == NULL)
+    {
+      // Default implementation, for plugins using Orthanc SDK <= 1.3.2
+
+      LookupIdentifier(result, level, tag, Compatibility::IdentifierConstraintType_GreaterOrEqual, start);
+
+      std::list<int64_t> b;
+      LookupIdentifier(result, level, tag, Compatibility::IdentifierConstraintType_SmallerOrEqual, end);
+
+      result.splice(result.end(), b);
+    }
+    else
+    {
+      ResetAnswers();
+      CheckSuccess(extensions_.lookupIdentifierRange(GetContext(), payload_, Plugins::Convert(level),
+                                                     tag.GetGroup(), tag.GetElement(),
+                                                     start.c_str(), end.c_str()));
+      ForwardAnswers(result);
+    }
+  }
+
+
+  void OrthancPluginDatabase::SetResourcesContent(const Orthanc::ResourcesContent& content)
+  {
+    if (extensions_.setResourcesContent == NULL)
+    {
+      ISetResourcesContent::Apply(*this, content);
+    }
+    else
+    {
+      std::vector<OrthancPluginResourcesContentTags> identifierTags;
+      std::vector<OrthancPluginResourcesContentTags> mainDicomTags;
+      std::vector<OrthancPluginResourcesContentMetadata> metadata;
+
+      identifierTags.reserve(content.GetListTags().size());
+      mainDicomTags.reserve(content.GetListTags().size());
+      metadata.reserve(content.GetListMetadata().size());
+
+      for (ResourcesContent::ListTags::const_iterator
+             it = content.GetListTags().begin(); it != content.GetListTags().end(); ++it)
+      {
+        OrthancPluginResourcesContentTags tmp;
+        tmp.resource = it->resourceId_;
+        tmp.group = it->tag_.GetGroup();
+        tmp.element = it->tag_.GetElement();
+        tmp.value = it->value_.c_str();
+
+        if (it->isIdentifier_)
+        {
+          identifierTags.push_back(tmp);
+        }
+        else
+        {
+          mainDicomTags.push_back(tmp);
+        }
+      }
+
+      for (ResourcesContent::ListMetadata::const_iterator
+             it = content.GetListMetadata().begin(); it != content.GetListMetadata().end(); ++it)
+      {
+        OrthancPluginResourcesContentMetadata tmp;
+        tmp.resource = it->resourceId_;
+        tmp.metadata = it->metadata_;
+        tmp.value = it->value_.c_str();
+        metadata.push_back(tmp);
+      }
+
+      assert(identifierTags.size() + mainDicomTags.size() == content.GetListTags().size() &&
+             metadata.size() == content.GetListMetadata().size());
+       
+      CheckSuccess(extensions_.setResourcesContent(
+                     payload_,
+                     identifierTags.size(),
+                     (identifierTags.empty() ? NULL : &identifierTags[0]),
+                     mainDicomTags.size(),
+                     (mainDicomTags.empty() ? NULL : &mainDicomTags[0]),
+                     metadata.size(),
+                     (metadata.empty() ? NULL : &metadata[0])));
+    }
+  }
+
+
+
+  void OrthancPluginDatabase::GetChildrenMetadata(std::list<std::string>& target,
+                                                  int64_t resourceId,
+                                                  MetadataType metadata)
+  {
+    if (extensions_.getChildrenMetadata == NULL)
+    {
+      IGetChildrenMetadata::Apply(*this, target, resourceId, metadata);
+    }
+    else
+    {
+      ResetAnswers();
+      CheckSuccess(extensions_.getChildrenMetadata
+                   (GetContext(), payload_, resourceId, static_cast<int32_t>(metadata)));
+      ForwardAnswers(target);
+    }
+  }
+
+
+  int64_t OrthancPluginDatabase::GetLastChangeIndex()
+  {
+    if (extensions_.getLastChangeIndex == NULL)
+    {
+      // This was the default behavior in Orthanc <= 1.5.1
+      // https://groups.google.com/d/msg/orthanc-users/QhzB6vxYeZ0/YxabgqpfBAAJ
+      return 0;
+    }
+    else
+    {
+      int64_t result = 0;
+      CheckSuccess(extensions_.getLastChangeIndex(&result, payload_));
+      return result;
+    }
+  }
+
+  
+  void OrthancPluginDatabase::TagMostRecentPatient(int64_t patient)
+  {
+    if (extensions_.tagMostRecentPatient != NULL)
+    {
+      CheckSuccess(extensions_.tagMostRecentPatient(payload_, patient));
+    }
+  }
 }
--- a/Plugins/Engine/OrthancPluginDatabase.h	Tue Jan 15 18:46:59 2019 +0100
+++ b/Plugins/Engine/OrthancPluginDatabase.h	Tue Jan 15 21:11:44 2019 +0100
@@ -36,13 +36,21 @@
 #if ORTHANC_ENABLE_PLUGINS == 1
 
 #include "../../Core/SharedLibrary.h"
-#include "../../OrthancServer/IDatabaseWrapper.h"
+#include "../../OrthancServer/Database/Compatibility/ICreateInstance.h"
+#include "../../OrthancServer/Database/Compatibility/IGetChildrenMetadata.h"
+#include "../../OrthancServer/Database/Compatibility/ILookupResources.h"
+#include "../../OrthancServer/Database/Compatibility/ISetResourcesContent.h"
 #include "../Include/orthanc/OrthancCDatabasePlugin.h"
 #include "PluginsErrorDictionary.h"
 
 namespace Orthanc
 {
-  class OrthancPluginDatabase : public IDatabaseWrapper
+  class OrthancPluginDatabase :
+    public IDatabaseWrapper,
+    public Compatibility::ICreateInstance,
+    public Compatibility::IGetChildrenMetadata,
+    public Compatibility::ILookupResources,
+    public Compatibility::ISetResourcesContent
   {
   private:
     class Transaction;
@@ -57,6 +65,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_;
@@ -67,6 +78,8 @@
     std::list<ServerIndexChange>*  answerChanges_;
     std::list<ExportedResource>*   answerExportedResources_;
     bool*                          answerDone_;
+    std::list<std::string>*        answerMatchingResources_;
+    std::list<std::string>*        answerMatchingInstances_;
 
     OrthancPluginDatabaseContext* GetContext()
     {
@@ -93,12 +106,11 @@
                           size_t extensionsSize,
                           void *payload);
 
-    virtual void Open()
-    {
-      CheckSuccess(backend_.open(payload_));
-    }
+    virtual void Open() 
+      ORTHANC_OVERRIDE;
 
-    virtual void Close()
+    virtual void Close() 
+      ORTHANC_OVERRIDE
     {
       CheckSuccess(backend_.close(payload_));
     }
@@ -109,165 +121,249 @@
     }
 
     virtual void AddAttachment(int64_t id,
-                               const FileInfo& attachment);
+                               const FileInfo& attachment) 
+      ORTHANC_OVERRIDE;
 
     virtual void AttachChild(int64_t parent,
-                             int64_t child);
+                             int64_t child) 
+      ORTHANC_OVERRIDE;
 
-    virtual void ClearChanges();
+    virtual void ClearChanges() 
+      ORTHANC_OVERRIDE;
 
-    virtual void ClearExportedResources();
+    virtual void ClearExportedResources() 
+      ORTHANC_OVERRIDE;
 
     virtual int64_t CreateResource(const std::string& publicId,
-                                   ResourceType type);
+                                   ResourceType type) 
+      ORTHANC_OVERRIDE;
 
     virtual void DeleteAttachment(int64_t id,
-                                  FileContentType attachment);
+                                  FileContentType attachment) 
+      ORTHANC_OVERRIDE;
 
     virtual void DeleteMetadata(int64_t id,
-                                MetadataType type);
+                                MetadataType type) 
+      ORTHANC_OVERRIDE;
 
-    virtual void DeleteResource(int64_t id);
+    virtual void DeleteResource(int64_t id) 
+      ORTHANC_OVERRIDE;
 
-    virtual void FlushToDisk()
+    virtual void FlushToDisk() 
+      ORTHANC_OVERRIDE
     {
     }
 
-    virtual bool HasFlushToDisk() const
+    virtual bool HasFlushToDisk() const 
+      ORTHANC_OVERRIDE
     {
       return false;
     }
 
     virtual void GetAllMetadata(std::map<MetadataType, std::string>& target,
-                                int64_t id);
-
-    virtual void GetAllInternalIds(std::list<int64_t>& target,
-                                   ResourceType resourceType);
+                                int64_t id) 
+      ORTHANC_OVERRIDE;
 
     virtual void GetAllPublicIds(std::list<std::string>& target,
-                                 ResourceType resourceType);
+                                 ResourceType resourceType) 
+      ORTHANC_OVERRIDE;
 
     virtual void GetAllPublicIds(std::list<std::string>& target,
                                  ResourceType resourceType,
                                  size_t since,
-                                 size_t limit);
+                                 size_t limit) 
+      ORTHANC_OVERRIDE;
 
     virtual void GetChanges(std::list<ServerIndexChange>& target /*out*/,
                             bool& done /*out*/,
                             int64_t since,
-                            uint32_t maxResults);
+                            uint32_t maxResults) 
+      ORTHANC_OVERRIDE;
 
     virtual void GetChildrenInternalId(std::list<int64_t>& target,
-                                       int64_t id);
+                                       int64_t id) 
+      ORTHANC_OVERRIDE;
 
     virtual void GetChildrenPublicId(std::list<std::string>& target,
-                                     int64_t id);
+                                     int64_t id) 
+      ORTHANC_OVERRIDE;
 
     virtual void GetExportedResources(std::list<ExportedResource>& target /*out*/,
                                       bool& done /*out*/,
                                       int64_t since,
-                                      uint32_t maxResults);
+                                      uint32_t maxResults) 
+      ORTHANC_OVERRIDE;
 
-    virtual void GetLastChange(std::list<ServerIndexChange>& target /*out*/);
+    virtual void GetLastChange(std::list<ServerIndexChange>& target /*out*/) 
+      ORTHANC_OVERRIDE;
 
-    virtual void GetLastExportedResource(std::list<ExportedResource>& target /*out*/);
+    virtual void GetLastExportedResource(std::list<ExportedResource>& target /*out*/) 
+      ORTHANC_OVERRIDE;
 
     virtual void GetMainDicomTags(DicomMap& map,
-                                  int64_t id);
+                                  int64_t id) 
+      ORTHANC_OVERRIDE;
 
-    virtual std::string GetPublicId(int64_t resourceId);
+    virtual std::string GetPublicId(int64_t resourceId) 
+      ORTHANC_OVERRIDE;
 
-    virtual uint64_t GetResourceCount(ResourceType resourceType);
+    virtual uint64_t GetResourceCount(ResourceType resourceType) 
+      ORTHANC_OVERRIDE;
 
-    virtual ResourceType GetResourceType(int64_t resourceId);
+    virtual ResourceType GetResourceType(int64_t resourceId) 
+      ORTHANC_OVERRIDE;
 
-    virtual uint64_t GetTotalCompressedSize();
+    virtual uint64_t GetTotalCompressedSize() 
+      ORTHANC_OVERRIDE;
     
-    virtual uint64_t GetTotalUncompressedSize();
+    virtual uint64_t GetTotalUncompressedSize() 
+      ORTHANC_OVERRIDE;
 
-    virtual bool IsExistingResource(int64_t internalId);
+    virtual bool IsExistingResource(int64_t internalId) 
+      ORTHANC_OVERRIDE;
 
-    virtual bool IsProtectedPatient(int64_t internalId);
+    virtual bool IsProtectedPatient(int64_t internalId) 
+      ORTHANC_OVERRIDE;
 
     virtual void ListAvailableMetadata(std::list<MetadataType>& target,
-                                       int64_t id);
+                                       int64_t id) 
+      ORTHANC_OVERRIDE;
 
     virtual void ListAvailableAttachments(std::list<FileContentType>& target,
-                                          int64_t id);
+                                          int64_t id) 
+      ORTHANC_OVERRIDE;
 
     virtual void LogChange(int64_t internalId,
-                           const ServerIndexChange& change);
+                           const ServerIndexChange& change) 
+      ORTHANC_OVERRIDE;
 
-    virtual void LogExportedResource(const ExportedResource& resource);
+    virtual void LogExportedResource(const ExportedResource& resource) 
+      ORTHANC_OVERRIDE;
     
     virtual bool LookupAttachment(FileInfo& attachment,
                                   int64_t id,
-                                  FileContentType contentType);
+                                  FileContentType contentType) 
+      ORTHANC_OVERRIDE;
 
     virtual bool LookupGlobalProperty(std::string& target,
-                                      GlobalProperty property);
+                                      GlobalProperty property) 
+      ORTHANC_OVERRIDE;
+
+    virtual bool LookupMetadata(std::string& target,
+                                int64_t id,
+                                MetadataType type) 
+      ORTHANC_OVERRIDE;
+
+    virtual bool LookupParent(int64_t& parentId,
+                              int64_t resourceId) 
+      ORTHANC_OVERRIDE;
+
+    virtual bool LookupResource(int64_t& id,
+                                ResourceType& type,
+                                const std::string& publicId) 
+      ORTHANC_OVERRIDE;
+
+    virtual bool SelectPatientToRecycle(int64_t& internalId) 
+      ORTHANC_OVERRIDE;
+
+    virtual bool SelectPatientToRecycle(int64_t& internalId,
+                                        int64_t patientIdToAvoid) 
+      ORTHANC_OVERRIDE;
+
+    virtual void SetGlobalProperty(GlobalProperty property,
+                                   const std::string& value) 
+      ORTHANC_OVERRIDE;
+
+    virtual void ClearMainDicomTags(int64_t id) 
+      ORTHANC_OVERRIDE;
+
+    virtual void SetMainDicomTag(int64_t id,
+                                 const DicomTag& tag,
+                                 const std::string& value) 
+      ORTHANC_OVERRIDE;
+
+    virtual void SetIdentifierTag(int64_t id,
+                                  const DicomTag& tag,
+                                  const std::string& value) 
+      ORTHANC_OVERRIDE;
+
+    virtual void SetMetadata(int64_t id,
+                             MetadataType type,
+                             const std::string& value) 
+      ORTHANC_OVERRIDE;
 
+    virtual void SetProtectedPatient(int64_t internalId, 
+                                     bool isProtected) 
+      ORTHANC_OVERRIDE;
+
+    virtual IDatabaseWrapper::ITransaction* StartTransaction() 
+      ORTHANC_OVERRIDE;
+
+    virtual void SetListener(IDatabaseListener& listener) 
+      ORTHANC_OVERRIDE
+    {
+      listener_ = &listener;
+    }
+
+    virtual unsigned int GetDatabaseVersion() 
+      ORTHANC_OVERRIDE;
+
+    virtual void Upgrade(unsigned int targetVersion,
+                         IStorageArea& storageArea) 
+      ORTHANC_OVERRIDE;
+
+    void AnswerReceived(const _OrthancPluginDatabaseAnswer& answer);
+
+    virtual bool IsDiskSizeAbove(uint64_t threshold) 
+      ORTHANC_OVERRIDE;
+
+    virtual void ApplyLookupResources(std::list<std::string>& resourcesId,
+                                      std::list<std::string>* instancesId,
+                                      const std::vector<DatabaseConstraint>& lookup,
+                                      ResourceType queryLevel,
+                                      size_t limit)
+      ORTHANC_OVERRIDE;
+
+    virtual bool CreateInstance(CreateInstanceResult& result,
+                                int64_t& instanceId,
+                                const std::string& patient,
+                                const std::string& study,
+                                const std::string& series,
+                                const std::string& instance)
+      ORTHANC_OVERRIDE;
+
+    // From the "ILookupResources" interface
+    virtual void GetAllInternalIds(std::list<int64_t>& target,
+                                   ResourceType resourceType) 
+      ORTHANC_OVERRIDE;
+
+    // From the "ILookupResources" interface
     virtual void LookupIdentifier(std::list<int64_t>& result,
                                   ResourceType level,
                                   const DicomTag& tag,
-                                  IdentifierConstraintType type,
-                                  const std::string& value);
-
+                                  Compatibility::IdentifierConstraintType type,
+                                  const std::string& value)
+      ORTHANC_OVERRIDE;
+    
+    // From the "ILookupResources" interface
     virtual void LookupIdentifierRange(std::list<int64_t>& result,
                                        ResourceType level,
                                        const DicomTag& tag,
                                        const std::string& start,
-                                       const std::string& end);
-
-    virtual bool LookupMetadata(std::string& target,
-                                int64_t id,
-                                MetadataType type);
-
-    virtual bool LookupParent(int64_t& parentId,
-                              int64_t resourceId);
+                                       const std::string& end)
+      ORTHANC_OVERRIDE;
 
-    virtual bool LookupResource(int64_t& id,
-                                ResourceType& type,
-                                const std::string& publicId);
-
-    virtual bool SelectPatientToRecycle(int64_t& internalId);
-
-    virtual bool SelectPatientToRecycle(int64_t& internalId,
-                                        int64_t patientIdToAvoid);
-
-    virtual void SetGlobalProperty(GlobalProperty property,
-                                   const std::string& value);
-
-    virtual void ClearMainDicomTags(int64_t id);
+    virtual void SetResourcesContent(const Orthanc::ResourcesContent& content)
+      ORTHANC_OVERRIDE;
 
-    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 SetMetadata(int64_t id,
-                             MetadataType type,
-                             const std::string& value);
+    virtual void GetChildrenMetadata(std::list<std::string>& target,
+                                     int64_t resourceId,
+                                     MetadataType metadata)
+      ORTHANC_OVERRIDE;
 
-    virtual void SetProtectedPatient(int64_t internalId, 
-                                     bool isProtected);
-
-    virtual SQLite::ITransaction* StartTransaction();
-
-    virtual void SetListener(IDatabaseListener& listener)
-    {
-      listener_ = &listener;
-    }
-
-    virtual unsigned int GetDatabaseVersion();
-
-    virtual void Upgrade(unsigned int targetVersion,
-                         IStorageArea& storageArea);
-
-    void AnswerReceived(const _OrthancPluginDatabaseAnswer& answer);
+    virtual int64_t GetLastChangeIndex() ORTHANC_OVERRIDE;
+  
+    virtual void TagMostRecentPatient(int64_t patient) ORTHANC_OVERRIDE;
   };
 }
 
--- a/Plugins/Engine/OrthancPlugins.cpp	Tue Jan 15 18:46:59 2019 +0100
+++ b/Plugins/Engine/OrthancPlugins.cpp	Tue Jan 15 21:11:44 2019 +0100
@@ -1648,7 +1648,7 @@
         throw OrthancException(ErrorCode_InternalError);
     }
 
-    std::list<std::string> result;
+    std::vector<std::string> result;
 
     {
       PImpl::ServerContextLock lock(*pimpl_);
@@ -1657,7 +1657,7 @@
 
     if (result.size() == 1)
     {
-      *p.result = CopyString(result.front());
+      *p.result = CopyString(result[0]);
     }
     else
     {
--- a/Plugins/Engine/PluginsEnumerations.cpp	Tue Jan 15 18:46:59 2019 +0100
+++ b/Plugins/Engine/PluginsEnumerations.cpp	Tue Jan 15 21:11:44 2019 +0100
@@ -43,23 +43,23 @@
 
 namespace Orthanc
 {
-  namespace Plugins
+  namespace Compatibility
   {
-    OrthancPluginResourceType Convert(ResourceType type)
+    OrthancPluginIdentifierConstraint Convert(IdentifierConstraintType constraint)
     {
-      switch (type)
+      switch (constraint)
       {
-        case ResourceType_Patient:
-          return OrthancPluginResourceType_Patient;
+        case Compatibility::IdentifierConstraintType_Equal:
+          return OrthancPluginIdentifierConstraint_Equal;
 
-        case ResourceType_Study:
-          return OrthancPluginResourceType_Study;
+        case Compatibility::IdentifierConstraintType_GreaterOrEqual:
+          return OrthancPluginIdentifierConstraint_GreaterOrEqual;
 
-        case ResourceType_Series:
-          return OrthancPluginResourceType_Series;
+        case Compatibility::IdentifierConstraintType_SmallerOrEqual:
+          return OrthancPluginIdentifierConstraint_SmallerOrEqual;
 
-        case ResourceType_Instance:
-          return OrthancPluginResourceType_Instance;
+        case Compatibility::IdentifierConstraintType_Wildcard:
+          return OrthancPluginIdentifierConstraint_Wildcard;
 
         default:
           throw OrthancException(ErrorCode_ParameterOutOfRange);
@@ -67,28 +67,31 @@
     }
 
 
-    ResourceType Convert(OrthancPluginResourceType type)
+    IdentifierConstraintType Convert(OrthancPluginIdentifierConstraint constraint)
     {
-      switch (type)
+      switch (constraint)
       {
-        case OrthancPluginResourceType_Patient:
-          return ResourceType_Patient;
+        case OrthancPluginIdentifierConstraint_Equal:
+          return Compatibility::IdentifierConstraintType_Equal;
 
-        case OrthancPluginResourceType_Study:
-          return ResourceType_Study;
+        case OrthancPluginIdentifierConstraint_GreaterOrEqual:
+          return Compatibility::IdentifierConstraintType_GreaterOrEqual;
 
-        case OrthancPluginResourceType_Series:
-          return ResourceType_Series;
+        case OrthancPluginIdentifierConstraint_SmallerOrEqual:
+          return Compatibility::IdentifierConstraintType_SmallerOrEqual;
 
-        case OrthancPluginResourceType_Instance:
-          return ResourceType_Instance;
+        case OrthancPluginIdentifierConstraint_Wildcard:
+          return Compatibility::IdentifierConstraintType_Wildcard;
 
         default:
           throw OrthancException(ErrorCode_ParameterOutOfRange);
       }
     }
+  }
 
 
+  namespace Plugins
+  {
     OrthancPluginChangeType Convert(ChangeType type)
     {
       switch (type)
@@ -266,50 +269,6 @@
     }
 
 
-    OrthancPluginIdentifierConstraint Convert(IdentifierConstraintType constraint)
-    {
-      switch (constraint)
-      {
-        case IdentifierConstraintType_Equal:
-          return OrthancPluginIdentifierConstraint_Equal;
-
-        case IdentifierConstraintType_GreaterOrEqual:
-          return OrthancPluginIdentifierConstraint_GreaterOrEqual;
-
-        case IdentifierConstraintType_SmallerOrEqual:
-          return OrthancPluginIdentifierConstraint_SmallerOrEqual;
-
-        case IdentifierConstraintType_Wildcard:
-          return OrthancPluginIdentifierConstraint_Wildcard;
-
-        default:
-          throw OrthancException(ErrorCode_ParameterOutOfRange);
-      }
-    }
-
-
-    IdentifierConstraintType Convert(OrthancPluginIdentifierConstraint constraint)
-    {
-      switch (constraint)
-      {
-        case OrthancPluginIdentifierConstraint_Equal:
-          return IdentifierConstraintType_Equal;
-
-        case OrthancPluginIdentifierConstraint_GreaterOrEqual:
-          return IdentifierConstraintType_GreaterOrEqual;
-
-        case OrthancPluginIdentifierConstraint_SmallerOrEqual:
-          return IdentifierConstraintType_SmallerOrEqual;
-
-        case OrthancPluginIdentifierConstraint_Wildcard:
-          return IdentifierConstraintType_Wildcard;
-
-        default:
-          throw OrthancException(ErrorCode_ParameterOutOfRange);
-      }
-    }
-
-
     OrthancPluginInstanceOrigin Convert(RequestOrigin origin)
     {
       switch (origin)
--- a/Plugins/Engine/PluginsEnumerations.h	Tue Jan 15 18:46:59 2019 +0100
+++ b/Plugins/Engine/PluginsEnumerations.h	Tue Jan 15 21:11:44 2019 +0100
@@ -35,17 +35,27 @@
 
 #if ORTHANC_ENABLE_PLUGINS == 1
 
+/**
+ * NB: Conversions to/from "OrthancPluginConstraintType" and
+ * "OrthancPluginResourceType" are located in file
+ * "../../OrthancServer/Search/DatabaseConstraint.h" to be shared with
+ * the "orthanc-databases" project.
+ **/
+
 #include "../Include/orthanc/OrthancCPlugin.h"
-#include "../../OrthancServer/ServerEnumerations.h"
+#include "../../OrthancServer/Search/DatabaseConstraint.h"
 
 namespace Orthanc
 {
+  namespace Compatibility
+  {
+    OrthancPluginIdentifierConstraint Convert(IdentifierConstraintType constraint);
+
+    IdentifierConstraintType Convert(OrthancPluginIdentifierConstraint constraint);
+  }
+
   namespace Plugins
   {
-    OrthancPluginResourceType Convert(ResourceType type);
-
-    ResourceType Convert(OrthancPluginResourceType type);
-
     OrthancPluginChangeType Convert(ChangeType type);
 
     OrthancPluginPixelFormat Convert(PixelFormat format);
@@ -58,10 +68,6 @@
 
     DicomToJsonFormat Convert(OrthancPluginDicomToJsonFormat format);
 
-    OrthancPluginIdentifierConstraint Convert(IdentifierConstraintType constraint);
-
-    IdentifierConstraintType Convert(OrthancPluginIdentifierConstraint constraint);
-
     OrthancPluginInstanceOrigin Convert(RequestOrigin origin);
 
     OrthancPluginHttpMethod Convert(HttpMethod method);
--- a/Plugins/Include/orthanc/OrthancCDatabasePlugin.h	Tue Jan 15 18:46:59 2019 +0100
+++ b/Plugins/Include/orthanc/OrthancCDatabasePlugin.h	Tue Jan 15 21:11:44 2019 +0100
@@ -75,6 +75,7 @@
     _OrthancPluginDatabaseAnswerType_Int64 = 15,
     _OrthancPluginDatabaseAnswerType_Resource = 16,
     _OrthancPluginDatabaseAnswerType_String = 17,
+    _OrthancPluginDatabaseAnswerType_MatchingResource = 18,  /* New in Orthanc 1.5.2 */
 
     _OrthancPluginDatabaseAnswerType_INTERNAL = 0x7fffffff
   } _OrthancPluginDatabaseAnswerType;
@@ -120,6 +121,55 @@
     const char*                sopInstanceUid;
   } OrthancPluginExportedResource;
 
+  typedef struct   /* New in Orthanc 1.5.2 */
+  {
+    OrthancPluginResourceType    level;
+    uint16_t                     tagGroup;
+    uint16_t                     tagElement;
+    uint8_t                      isIdentifierTag;
+    uint8_t                      isCaseSensitive;
+    uint8_t                      isMandatory;
+    OrthancPluginConstraintType  type;
+    uint32_t                     valuesCount;
+    const char* const*           values;
+  } OrthancPluginDatabaseConstraint;
+
+  typedef struct   /* New in Orthanc 1.5.2 */
+  {
+    const char*  resourceId;
+    const char*  someInstanceId;  /* Can be NULL if not requested */
+  } OrthancPluginMatchingResource;
+
+  typedef struct   /* New in Orthanc 1.5.2 */
+  {
+    /* Mandatory field */
+    uint8_t  isNewInstance;
+    int64_t  instanceId;
+
+    /* The following fields must only be set if "isNewInstance" is "true" */
+    uint8_t  isNewPatient;
+    uint8_t  isNewStudy;
+    uint8_t  isNewSeries;
+    int64_t  patientId;
+    int64_t  studyId;
+    int64_t  seriesId;
+  } OrthancPluginCreateInstanceResult;
+
+  typedef struct  /* New in Orthanc 1.5.2 */
+  {
+    int64_t      resource;
+    uint16_t     group;
+    uint16_t     element;
+    const char*  value;
+  } OrthancPluginResourcesContentTags;
+    
+  typedef struct  /* New in Orthanc 1.5.2 */
+  {
+    int64_t      resource;
+    int32_t      metadata;
+    const char*  value;
+  } OrthancPluginResourcesContentMetadata;
+
 
   typedef struct
   {
@@ -272,6 +322,19 @@
     context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
   }
 
+  ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerMatchingResource(
+    OrthancPluginContext*                 context,
+    OrthancPluginDatabaseContext*         database,
+    const OrthancPluginMatchingResource*  match)
+  {
+    _OrthancPluginDatabaseAnswer params;
+    memset(&params, 0, sizeof(params));
+    params.database = database;
+    params.type = _OrthancPluginDatabaseAnswerType_MatchingResource;
+    params.valueGeneric = match;
+    context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
+  }
+
   ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseSignalDeletedAttachment(
     OrthancPluginContext*          context,
     OrthancPluginDatabaseContext*  database,
@@ -638,6 +701,10 @@
 
   typedef struct
   {
+    /**
+     * Base extensions since Orthanc 1.0.0
+     **/
+    
     /* Output: Use OrthancPluginDatabaseAnswerString() */
     OrthancPluginErrorCode  (*getAllPublicIdsWithLimit) (
       /* outputs */
@@ -683,6 +750,11 @@
       const OrthancPluginDicomTag* tag,
       OrthancPluginIdentifierConstraint constraint);
 
+
+    /**
+     * Extensions since Orthanc 1.4.0
+     **/
+    
     /* Output: Use OrthancPluginDatabaseAnswerInt64() */
     OrthancPluginErrorCode  (*lookupIdentifierRange) (
       /* outputs */
@@ -694,7 +766,66 @@
       uint16_t element,
       const char* start,
       const char* end);
-   } OrthancPluginDatabaseExtensions;
+
+    
+    /**
+     * Extensions since Orthanc 1.5.2
+     **/
+    
+    /* Ouput: Use OrthancPluginDatabaseAnswerMatchingResource */
+    OrthancPluginErrorCode  (*lookupResources) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload,
+      uint32_t constraintsCount,
+      const OrthancPluginDatabaseConstraint* constraints,
+      OrthancPluginResourceType queryLevel,
+      uint32_t limit,
+      uint8_t requestSomeInstance);
+
+    
+    OrthancPluginErrorCode  (*createInstance) (
+      /* output */
+      OrthancPluginCreateInstanceResult* output,
+      /* inputs */
+      void* payload,
+      const char* hashPatient,
+      const char* hashStudy,
+      const char* hashSeries,
+      const char* hashInstance);
+
+    OrthancPluginErrorCode  (*setResourcesContent) (
+      /* inputs */
+      void* payload,
+      uint32_t countIdentifierTags,
+      const OrthancPluginResourcesContentTags* identifierTags,
+      uint32_t countMainDicomTags,
+      const OrthancPluginResourcesContentTags* mainDicomTags,
+      uint32_t countMetadata,
+      const OrthancPluginResourcesContentMetadata* metadata);
+
+    /* Ouput: Use OrthancPluginDatabaseAnswerString */
+    OrthancPluginErrorCode  (*getChildrenMetadata) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload,
+      int64_t resourceId,
+      int32_t metadata);
+
+    OrthancPluginErrorCode  (*getLastChangeIndex) (
+      /* outputs */
+      int64_t* target,
+      /* inputs */
+      void* payload);
+                   
+    OrthancPluginErrorCode  (*tagMostRecentPatient) (
+      /* inputs */
+      void* payload,
+      int64_t patientId);
+                   
+  } OrthancPluginDatabaseExtensions;
 
 /*<! @endcond */
 
--- a/Plugins/Include/orthanc/OrthancCPlugin.h	Tue Jan 15 18:46:59 2019 +0100
+++ b/Plugins/Include/orthanc/OrthancCPlugin.h	Tue Jan 15 21:11:44 2019 +0100
@@ -119,7 +119,7 @@
 
 #define ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER     1
 #define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER     5
-#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER  0
+#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER  2
 
 
 #if !defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE)
@@ -821,6 +821,7 @@
   /**
    * The constraints on the DICOM identifiers that must be supported
    * by the database plugins.
+   * @deprecated Plugins using OrthancPluginConstraintType will be faster
    **/
   typedef enum
   {
@@ -834,6 +835,22 @@
 
 
   /**
+   * The constraints on the tags (main DICOM tags and identifier tags)
+   * that must be supported by the database plugins.
+   **/
+  typedef enum
+  {
+    OrthancPluginConstraintType_Equal = 1,           /*!< Equal */
+    OrthancPluginConstraintType_SmallerOrEqual = 2,  /*!< Less or equal */
+    OrthancPluginConstraintType_GreaterOrEqual = 3,  /*!< More or equal */
+    OrthancPluginConstraintType_Wildcard = 4,        /*!< Wildcard matching */
+    OrthancPluginConstraintType_List = 5,            /*!< List of values */
+
+    _OrthancPluginConstraintType_INTERNAL = 0x7fffffff
+  } OrthancPluginConstraintType;
+
+
+  /**
    * The origin of a DICOM instance that has been received by Orthanc.
    **/
   typedef enum
@@ -1513,7 +1530,8 @@
         sizeof(int32_t) != sizeof(OrthancPluginCreateDicomFlags) ||
         sizeof(int32_t) != sizeof(OrthancPluginIdentifierConstraint) ||
         sizeof(int32_t) != sizeof(OrthancPluginInstanceOrigin) ||
-        sizeof(int32_t) != sizeof(OrthancPluginJobStepStatus))
+        sizeof(int32_t) != sizeof(OrthancPluginJobStepStatus) ||
+        sizeof(int32_t) != sizeof(OrthancPluginConstraintType))
     {
       /* Mismatch in the size of the enumerations */
       return 0;
--- a/Resources/CMake/OrthancFrameworkParameters.cmake	Tue Jan 15 18:46:59 2019 +0100
+++ b/Resources/CMake/OrthancFrameworkParameters.cmake	Tue Jan 15 21:11:44 2019 +0100
@@ -17,7 +17,7 @@
 # Version of the Orthanc API, can be retrieved from "/system" URI in
 # order to check whether new URI endpoints are available even if using
 # the mainline version of Orthanc
-set(ORTHANC_API_VERSION "1.2")
+set(ORTHANC_API_VERSION "1.3")
 
 
 #####################################################################
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/DatabaseOptimizations/LookupIdentifierQuery.cpp	Tue Jan 15 21:11:44 2019 +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-2019 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	Tue Jan 15 21:11:44 2019 +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-2019 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	Tue Jan 15 21:11:44 2019 +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-2019 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	Tue Jan 15 21:11:44 2019 +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-2019 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	Tue Jan 15 18:46:59 2019 +0100
+++ b/UnitTestsSources/DatabaseLookupTests.cpp	Tue Jan 15 21:11:44 2019 +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/FromDcmtkTests.cpp	Tue Jan 15 18:46:59 2019 +0100
+++ b/UnitTestsSources/FromDcmtkTests.cpp	Tue Jan 15 21:11:44 2019 +0100
@@ -34,21 +34,22 @@
 #include "PrecompiledHeadersUnitTests.h"
 #include "gtest/gtest.h"
 
-#include "../Core/DicomParsing/FromDcmtkBridge.h"
-#include "../Core/DicomParsing/ToDcmtkBridge.h"
+#include "../Core/DicomNetworking/DicomFindAnswers.h"
 #include "../Core/DicomParsing/DicomModification.h"
-#include "../OrthancServer/ServerToolbox.h"
-#include "../Core/OrthancException.h"
+#include "../Core/DicomParsing/FromDcmtkBridge.h"
+#include "../Core/DicomParsing/Internals/DicomImageDecoder.h"
+#include "../Core/DicomParsing/ToDcmtkBridge.h"
+#include "../Core/Endianness.h"
+#include "../Core/Images/Image.h"
 #include "../Core/Images/ImageBuffer.h"
+#include "../Core/Images/ImageProcessing.h"
 #include "../Core/Images/PngReader.h"
 #include "../Core/Images/PngWriter.h"
-#include "../Core/Images/Image.h"
-#include "../Core/Images/ImageProcessing.h"
-#include "../Core/Endianness.h"
+#include "../Core/OrthancException.h"
+#include "../Core/SystemToolbox.h"
+#include "../OrthancServer/ServerToolbox.h"
+#include "../Plugins/Engine/PluginsEnumerations.h"
 #include "../Resources/EncodingTests.h"
-#include "../Core/DicomNetworking/DicomFindAnswers.h"
-#include "../Core/DicomParsing/Internals/DicomImageDecoder.h"
-#include "../Plugins/Engine/PluginsEnumerations.h"
 
 #include <dcmtk/dcmdata/dcelem.h>
 #include <dcmtk/dcmdata/dcdeftag.h>
--- a/UnitTestsSources/MultiThreadingTests.cpp	Tue Jan 15 18:46:59 2019 +0100
+++ b/UnitTestsSources/MultiThreadingTests.cpp	Tue Jan 15 21:11:44 2019 +0100
@@ -42,7 +42,7 @@
 #include "../Core/SerializationToolbox.h"
 #include "../Core/SystemToolbox.h"
 #include "../Core/Toolbox.h"
-#include "../OrthancServer/DatabaseWrapper.h"
+#include "../OrthancServer/Database/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	Tue Jan 15 18:46:59 2019 +0100
+++ b/UnitTestsSources/ServerIndexTests.cpp	Tue Jan 15 21:11:44 2019 +0100
@@ -37,10 +37,9 @@
 #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/Database/SQLiteDatabaseWrapper.h"
+#include "../OrthancServer/Search/DatabaseLookup.h"
 #include "../OrthancServer/ServerContext.h"
-#include "../OrthancServer/ServerIndex.h"
 #include "../OrthancServer/ServerToolbox.h"
 
 #include <ctype.h>
@@ -50,12 +49,6 @@
 
 namespace
 {
-  enum DatabaseWrapperClass
-  {
-    DatabaseWrapperClass_SQLite
-  };
-
-
   class TestDatabaseListener : public IDatabaseListener
   {
   public:
@@ -96,34 +89,24 @@
                 << EnumerationToString(change.GetResourceType()) << ": " 
                 << EnumerationToString(change.GetChangeType());
     }
-
   };
 
 
-  class DatabaseWrapperTest : public ::testing::TestWithParam<DatabaseWrapperClass>
+  class DatabaseWrapperTest : public ::testing::Test
   {
   protected:
-    std::auto_ptr<TestDatabaseListener> listener_;
-    std::auto_ptr<IDatabaseWrapper> index_;
+    std::auto_ptr<TestDatabaseListener>  listener_;
+    std::auto_ptr<SQLiteDatabaseWrapper> index_;
 
+  public:
     DatabaseWrapperTest()
     {
     }
 
-    virtual void SetUp()  ORTHANC_OVERRIDE
+    virtual void SetUp() ORTHANC_OVERRIDE
     {
       listener_.reset(new TestDatabaseListener);
-
-      switch (GetParam())
-      {
-        case DatabaseWrapperClass_SQLite:
-          index_.reset(new DatabaseWrapper());
-          break;
-
-        default:
-          throw OrthancException(ErrorCode_InternalError);
-      }
-
+      index_.reset(new SQLiteDatabaseWrapper);
       index_->SetListener(*listener_);
       index_->Open();
     }
@@ -137,94 +120,35 @@
 
     void CheckTableRecordCount(uint32_t expected, const char* table)
     {
-      switch (GetParam())
-      {
-        case DatabaseWrapperClass_SQLite:
-        {
-          DatabaseWrapper* sqlite = dynamic_cast<DatabaseWrapper*>(index_.get());
-          ASSERT_EQ(expected, sqlite->GetTableRecordCount(table));
-          break;
-        }
-
-        default:
-          throw OrthancException(ErrorCode_InternalError);
-      }
+      ASSERT_EQ(expected, index_->GetTableRecordCount(table));
     }
 
     void CheckNoParent(int64_t id)
     {
       std::string s;
-
-      switch (GetParam())
-      {
-        case DatabaseWrapperClass_SQLite:
-        {
-          DatabaseWrapper* sqlite = dynamic_cast<DatabaseWrapper*>(index_.get());
-          ASSERT_FALSE(sqlite->GetParentPublicId(s, id));
-          break;
-        }
-
-        default:
-          throw OrthancException(ErrorCode_InternalError);
-      }
+      ASSERT_FALSE(index_->GetParentPublicId(s, id));
     }
 
     void CheckParentPublicId(const char* expected, int64_t id)
     {
       std::string s;
-
-      switch (GetParam())
-      {
-        case DatabaseWrapperClass_SQLite:
-        {
-          DatabaseWrapper* sqlite = dynamic_cast<DatabaseWrapper*>(index_.get());
-          ASSERT_TRUE(sqlite->GetParentPublicId(s, id));
-          ASSERT_EQ(expected, s);
-          break;
-        }
-
-        default:
-          throw OrthancException(ErrorCode_InternalError);
-      }
+      ASSERT_TRUE(index_->GetParentPublicId(s, id));
+      ASSERT_EQ(expected, s);
     }
 
     void CheckNoChild(int64_t id)
     {
       std::list<std::string> j;
-
-      switch (GetParam())
-      {
-        case DatabaseWrapperClass_SQLite:
-        {
-          DatabaseWrapper* sqlite = dynamic_cast<DatabaseWrapper*>(index_.get());
-          sqlite->GetChildren(j, id);
-          ASSERT_EQ(0u, j.size());
-          break;
-        }
-
-        default:
-          throw OrthancException(ErrorCode_InternalError);
-      }
+      index_->GetChildren(j, id);
+      ASSERT_EQ(0u, j.size());
     }
 
     void CheckOneChild(const char* expected, int64_t id)
     {
       std::list<std::string> j;
-
-      switch (GetParam())
-      {
-        case DatabaseWrapperClass_SQLite:
-        {
-          DatabaseWrapper* sqlite = dynamic_cast<DatabaseWrapper*>(index_.get());
-          sqlite->GetChildren(j, id);
-          ASSERT_EQ(1u, j.size());
-          ASSERT_EQ(expected, j.front());
-          break;
-        }
-
-        default:
-          throw OrthancException(ErrorCode_InternalError);
-      }
+      index_->GetChildren(j, id);
+      ASSERT_EQ(1u, j.size());
+      ASSERT_EQ(expected, j.front());
     }
 
     void CheckTwoChildren(const char* expected1,
@@ -232,45 +156,52 @@
                           int64_t id)
     {
       std::list<std::string> j;
-
-      switch (GetParam())
-      {
-        case DatabaseWrapperClass_SQLite:
-        {
-          DatabaseWrapper* sqlite = dynamic_cast<DatabaseWrapper*>(index_.get());
-          sqlite->GetChildren(j, id);
-          ASSERT_EQ(2u, j.size());
-          ASSERT_TRUE((expected1 == j.front() && expected2 == j.back()) ||
-                      (expected1 == j.back() && expected2 == j.front()));                    
-          break;
-        }
-
-        default:
-          throw OrthancException(ErrorCode_InternalError);
-      }
+      index_->GetChildren(j, id);
+      ASSERT_EQ(2u, j.size());
+      ASSERT_TRUE((expected1 == j.front() && expected2 == j.back()) ||
+                  (expected1 == j.back() && expected2 == j.front()));                    
     }
 
-
-    void DoLookup(std::list<std::string>& result,
-                  ResourceType level,
-                  const DicomTag& tag,
-                  const std::string& value)
+    void DoLookupIdentifier(std::list<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(c.ConvertToDatabaseConstraint(level, DicomTagType_Identifier));
+      
+      index_->ApplyLookupResources(result, NULL, lookup, level, 0 /* no limit */);
+    }    
+
+    void DoLookupIdentifier2(std::list<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(c1.ConvertToDatabaseConstraint(level, DicomTagType_Identifier));
+      lookup.push_back(c2.ConvertToDatabaseConstraint(level, DicomTagType_Identifier));
+      
+      index_->ApplyLookupResources(result, NULL, lookup, level, 0 /* no limit */);
     }
-
   };
 }
 
 
-INSTANTIATE_TEST_CASE_P(DatabaseWrapperName,
-                        DatabaseWrapperTest,
-                        ::testing::Values(DatabaseWrapperClass_SQLite));
-
-
-TEST_P(DatabaseWrapperTest, Simple)
+TEST_F(DatabaseWrapperTest, Simple)
 {
   int64_t a[] = {
     index_->CreateResource("a", ResourceType_Patient),   // 0
@@ -464,7 +395,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(), 
@@ -473,9 +412,7 @@
 }
 
 
-
-
-TEST_P(DatabaseWrapperTest, Upward)
+TEST_F(DatabaseWrapperTest, Upward)
 {
   int64_t a[] = {
     index_->CreateResource("a", ResourceType_Patient),   // 0
@@ -526,7 +463,7 @@
 }
 
 
-TEST_P(DatabaseWrapperTest, PatientRecycling)
+TEST_F(DatabaseWrapperTest, PatientRecycling)
 {
   std::vector<int64_t> patients;
   for (int i = 0; i < 10; i++)
@@ -587,7 +524,7 @@
 }
 
 
-TEST_P(DatabaseWrapperTest, PatientProtection)
+TEST_F(DatabaseWrapperTest, PatientProtection)
 {
   std::vector<int64_t> patients;
   for (int i = 0; i < 5; i++)
@@ -669,14 +606,13 @@
 }
 
 
-
 TEST(ServerIndex, Sequence)
 {
   const std::string path = "UnitTestsStorage";
 
   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);
@@ -693,8 +629,7 @@
 }
 
 
-
-TEST_P(DatabaseWrapperTest, LookupIdentifier)
+TEST_F(DatabaseWrapperTest, LookupIdentifier)
 {
   int64_t a[] = {
     index_->CreateResource("a", ResourceType_Study),   // 0
@@ -710,73 +645,56 @@
 
   std::list<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());
 }
 
 
-
 TEST(ServerIndex, AttachmentRecycling)
 {
   const std::string path = "UnitTestsStorage";
 
   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 +768,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 +782,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	Tue Jan 15 18:46:59 2019 +0100
+++ b/UnitTestsSources/UnitTestsMain.cpp	Tue Jan 15 21:11:44 2019 +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__)