changeset 550:9ed9a91bde33 find-refactoring

un-sharing DatabaseConstraint and ISqlLookupFormatter with Orthanc core
author Sebastien Jodogne <s.jodogne@gmail.com>
date Mon, 09 Sep 2024 15:04:48 +0200
parents cd9766f294fa (current diff) e620f36b8e09 (diff)
children 7f45f23b10d0
files Framework/Plugins/DatabaseBackendAdapterV4.cpp Framework/Plugins/DatabaseConstraint.cpp Framework/Plugins/DatabaseConstraint.h Framework/Plugins/IDatabaseBackend.h Framework/Plugins/ISqlLookupFormatter.cpp Framework/Plugins/ISqlLookupFormatter.h Framework/Plugins/IndexUnitTests.h Framework/Plugins/MessagesToolbox.cpp Framework/Plugins/MessagesToolbox.h Resources/CMake/DatabasesPluginConfiguration.cmake Resources/Orthanc/Databases/DatabaseConstraint.cpp Resources/Orthanc/Databases/DatabaseConstraint.h Resources/Orthanc/Databases/ISqlLookupFormatter.cpp Resources/Orthanc/Databases/ISqlLookupFormatter.h Resources/SyncOrthancFolder.py
diffstat 16 files changed, 1726 insertions(+), 1548 deletions(-) [+]
line wrap: on
line diff
--- a/Framework/Plugins/DatabaseBackendAdapterV4.cpp	Mon Sep 09 12:48:52 2024 +0200
+++ b/Framework/Plugins/DatabaseBackendAdapterV4.cpp	Mon Sep 09 15:04:48 2024 +0200
@@ -91,28 +91,6 @@
   }
 
 
-  static Orthanc::ResourceType Convert2(Orthanc::DatabasePluginMessages::ResourceType resourceType)
-  {
-    switch (resourceType)
-    {
-      case Orthanc::DatabasePluginMessages::RESOURCE_PATIENT:
-        return Orthanc::ResourceType_Patient;
-
-      case Orthanc::DatabasePluginMessages::RESOURCE_STUDY:
-        return Orthanc::ResourceType_Study;
-
-      case Orthanc::DatabasePluginMessages::RESOURCE_SERIES:
-        return Orthanc::ResourceType_Series;
-
-      case Orthanc::DatabasePluginMessages::RESOURCE_INSTANCE:
-        return Orthanc::ResourceType_Instance;
-
-      default:
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
-    }
-  }
-
-
   class Output : public IDatabaseBackendOutput
   {
   private:
@@ -472,7 +450,7 @@
         for (int i = 0; i < request.open().identifier_tags().size(); i++)
         {
           const Orthanc::DatabasePluginMessages::Open_Request_IdentifierTag& tag = request.open().identifier_tags(i);
-          identifierTags.push_back(IdentifierTag(Convert2(tag.level()),
+          identifierTags.push_back(IdentifierTag(MessagesToolbox::Convert(tag.level()),
                                                  Orthanc::DicomTag(tag.group(), tag.element()),
                                                  tag.name()));
         }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Plugins/DatabaseConstraint.cpp	Mon Sep 09 15:04:48 2024 +0200
@@ -0,0 +1,389 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+/**
+ * NB: Until 2024-09-09, this file was synchronized with the following
+ * folder from the Orthanc main project:
+ * https://orthanc.uclouvain.be/hg/orthanc/file/default/OrthancServer/Sources/Search/
+ **/
+
+
+#include "DatabaseConstraint.h"
+
+#include <OrthancException.h>
+
+#include <boost/lexical_cast.hpp>
+#include <cassert>
+
+
+namespace Orthanc
+{
+  namespace Plugins
+  {
+    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);
+      }
+    }
+
+
+    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);
+      }
+    }
+
+
+#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
+
+
+#if ORTHANC_PLUGINS_HAS_INTEGRATED_FIND == 1
+  DatabaseConstraint::DatabaseConstraint(const Orthanc::DatabasePluginMessages::DatabaseConstraint& constraint) :
+    level_(OrthancDatabases::MessagesToolbox::Convert(constraint.level())),
+    tag_(constraint.tag_group(), constraint.tag_element()),
+    isIdentifier_(constraint.is_identifier_tag()),
+    caseSensitive_(constraint.is_case_sensitive()),
+    mandatory_(constraint.is_mandatory())
+  {
+    switch (constraint.type())
+    {
+      case Orthanc::DatabasePluginMessages::CONSTRAINT_EQUAL:
+        constraintType_ = Orthanc::ConstraintType_Equal;
+        break;
+
+      case Orthanc::DatabasePluginMessages::CONSTRAINT_SMALLER_OR_EQUAL:
+        constraintType_ = Orthanc::ConstraintType_SmallerOrEqual;
+        break;
+
+      case Orthanc::DatabasePluginMessages::CONSTRAINT_GREATER_OR_EQUAL:
+        constraintType_ = Orthanc::ConstraintType_GreaterOrEqual;
+        break;
+
+      case Orthanc::DatabasePluginMessages::CONSTRAINT_WILDCARD:
+        constraintType_ = Orthanc::ConstraintType_Wildcard;
+        break;
+
+      case Orthanc::DatabasePluginMessages::CONSTRAINT_LIST:
+        constraintType_ = Orthanc::ConstraintType_List;
+        break;
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    if (constraintType_ != ConstraintType_List &&
+        constraint.values().size() != 1)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    values_.resize(constraint.values().size());
+
+    for (int i = 0; i < constraint.values().size(); i++)
+    {
+      values_[i] = 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
+
+
+  void DatabaseConstraints::Clear()
+  {
+    for (size_t i = 0; i < constraints_.size(); i++)
+    {
+      assert(constraints_[i] != NULL);
+      delete constraints_[i];
+    }
+
+    constraints_.clear();
+  }
+
+
+  void DatabaseConstraints::AddConstraint(DatabaseConstraint* constraint)
+  {
+    if (constraint == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    else
+    {
+      constraints_.push_back(constraint);
+    }
+  }
+
+
+  const DatabaseConstraint& DatabaseConstraints::GetConstraint(size_t index) const
+  {
+    if (index >= constraints_.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      assert(constraints_[index] != NULL);
+      return *constraints_[index];
+    }
+  }
+
+
+  std::string DatabaseConstraints::Format() const
+  {
+    std::string s;
+
+    for (size_t i = 0; i < constraints_.size(); i++)
+    {
+      assert(constraints_[i] != NULL);
+      const DatabaseConstraint& constraint = *constraints_[i];
+      s += "Constraint " + boost::lexical_cast<std::string>(i) + " at " + EnumerationToString(constraint.GetLevel()) +
+        ": " + constraint.GetTag().Format();
+
+      switch (constraint.GetConstraintType())
+      {
+        case ConstraintType_Equal:
+          s += " == " + constraint.GetSingleValue();
+          break;
+
+        case ConstraintType_SmallerOrEqual:
+          s += " <= " + constraint.GetSingleValue();
+          break;
+
+        case ConstraintType_GreaterOrEqual:
+          s += " >= " + constraint.GetSingleValue();
+          break;
+
+        case ConstraintType_Wildcard:
+          s += " ~~ " + constraint.GetSingleValue();
+          break;
+
+        case ConstraintType_List:
+        {
+          s += " in [ ";
+          bool first = true;
+          for (size_t j = 0; j < constraint.GetValuesCount(); j++)
+          {
+            if (first)
+            {
+              first = false;
+            }
+            else
+            {
+              s += ", ";
+            }
+            s += constraint.GetValue(j);
+          }
+          s += "]";
+          break;
+        }
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+
+      s += "\n";
+    }
+
+    return s;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Plugins/DatabaseConstraint.h	Mon Sep 09 15:04:48 2024 +0200
@@ -0,0 +1,171 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+/**
+ * NB: Until 2024-09-09, this file was synchronized with the following
+ * folder from the Orthanc main project:
+ * https://orthanc.uclouvain.be/hg/orthanc/file/default/OrthancServer/Sources/Search/
+ **/
+
+
+#pragma once
+
+#include "MessagesToolbox.h"
+
+#include <DicomFormat/DicomMap.h>
+
+#include <deque>
+
+namespace Orthanc
+{
+  enum ConstraintType
+  {
+    ConstraintType_Equal,
+    ConstraintType_SmallerOrEqual,
+    ConstraintType_GreaterOrEqual,
+    ConstraintType_Wildcard,
+    ConstraintType_List
+  };
+
+  namespace Plugins
+  {
+    OrthancPluginResourceType Convert(ResourceType type);
+
+    ResourceType Convert(OrthancPluginResourceType type);
+
+#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
+    OrthancPluginConstraintType Convert(ConstraintType constraint);
+#endif
+
+#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
+    ConstraintType Convert(OrthancPluginConstraintType constraint);
+#endif
+  }
+
+
+  class DatabaseConstraint : public boost::noncopyable
+  {
+  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
+    explicit DatabaseConstraint(const OrthancPluginDatabaseConstraint& constraint);
+#endif
+
+#if ORTHANC_PLUGINS_HAS_INTEGRATED_FIND == 1
+    explicit DatabaseConstraint(const Orthanc::DatabasePluginMessages::DatabaseConstraint& 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
+  };
+
+
+  class DatabaseConstraints : public boost::noncopyable
+  {
+  private:
+    std::deque<DatabaseConstraint*>  constraints_;
+
+  public:
+    ~DatabaseConstraints()
+    {
+      Clear();
+    }
+
+    void Clear();
+
+    void AddConstraint(DatabaseConstraint* constraint);  // Takes ownership
+
+    bool IsEmpty() const
+    {
+      return constraints_.empty();
+    }
+
+    size_t GetSize() const
+    {
+      return constraints_.size();
+    }
+
+    const DatabaseConstraint& GetConstraint(size_t index) const;
+
+    std::string Format() const;
+  };
+}
--- a/Framework/Plugins/IDatabaseBackend.h	Mon Sep 09 12:48:52 2024 +0200
+++ b/Framework/Plugins/IDatabaseBackend.h	Mon Sep 09 15:04:48 2024 +0200
@@ -24,22 +24,14 @@
 
 #pragma once
 
-#include "../../Resources/Orthanc/Databases/ISqlLookupFormatter.h"
 #include "../Common/DatabaseManager.h"
 #include "../Common/DatabasesEnumerations.h"
 #include "IDatabaseBackendOutput.h"
+#include "ISqlLookupFormatter.h"
 #include "IdentifierTag.h"
 
 #include <list>
 
-#include <orthanc/OrthancCPlugin.h>
-
-#if defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE)         // Macro introduced in Orthanc 1.3.1
-#  if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 5)
-#    include <OrthancDatabasePlugin.pb.h>  // Include protobuf messages for "Find()"
-#  endif
-#endif
-
 namespace OrthancDatabases
 {
   class IDatabaseBackend : public boost::noncopyable
--- a/Framework/Plugins/IDatabaseBackendOutput.h	Mon Sep 09 12:48:52 2024 +0200
+++ b/Framework/Plugins/IDatabaseBackendOutput.h	Mon Sep 09 15:04:48 2024 +0200
@@ -23,7 +23,7 @@
 
 #pragma once
 
-#include "../../Resources/Orthanc/Databases/DatabaseConstraint.h"
+#include "DatabaseConstraint.h"
 
 namespace OrthancDatabases
 {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Plugins/ISqlLookupFormatter.cpp	Mon Sep 09 15:04:48 2024 +0200
@@ -0,0 +1,942 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+/**
+ * NB: Until 2024-09-09, this file was synchronized with the following
+ * folder from the Orthanc main project:
+ * https://orthanc.uclouvain.be/hg/orthanc/file/default/OrthancServer/Sources/Search/
+ **/
+
+
+#include "ISqlLookupFormatter.h"
+
+#include "DatabaseConstraint.h"
+
+#include <OrthancException.h>
+#include <Toolbox.h>
+
+#include <cassert>
+#include <boost/lexical_cast.hpp>
+#include <list>
+
+
+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,
+                               bool escapeBrackets)
+  {
+    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 if (escapeBrackets && value[i] == '[')
+            {
+              escaped += "\\[";
+            }
+            else if (escapeBrackets && 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()));
+  }
+
+
+  static std::string Join(const std::list<std::string>& values,
+                          const std::string& prefix,
+                          const std::string& separator)
+  {
+    if (values.empty())
+    {
+      return "";
+    }
+    else
+    {
+      std::string s = prefix;
+
+      bool first = true;
+      for (std::list<std::string>::const_iterator it = values.begin(); it != values.end(); ++it)
+      {
+        if (first)
+        {
+          first = false;
+        }
+        else
+        {
+          s += separator;
+        }
+
+        s += *it;
+      }
+
+      return s;
+    }
+  }
+
+  static bool FormatComparison2(std::string& target,
+                                ISqlLookupFormatter& formatter,
+                                const DatabaseConstraint& constraint,
+                                bool escapeBrackets)
+  {
+    std::string comparison;
+    std::string tagFilter = ("tagGroup = " + boost::lexical_cast<std::string>(constraint.GetTag().GetGroup())
+                              + " AND tagElement = " + boost::lexical_cast<std::string>(constraint.GetTag().GetElement()));
+
+    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 = " AND value " + op + " " + parameter;
+        }
+        else
+        {
+          comparison = " AND lower(value) " + op + " lower(" + parameter + ")";
+        }
+
+        break;
+      }
+
+      case ConstraintType_List:
+      {
+        std::vector<std::string> comparisonValues;
+        for (size_t i = 0; i < constraint.GetValuesCount(); i++)
+        {
+          std::string parameter = formatter.GenerateParameter(constraint.GetValue(i));
+
+          if (constraint.IsCaseSensitive())
+          {
+            comparisonValues.push_back(parameter);
+          }
+          else
+          {
+            comparisonValues.push_back("lower(" + parameter + ")");
+          }
+        }
+
+        std::string values;
+        Toolbox::JoinStrings(values, comparisonValues, ", ");
+
+        if (constraint.IsCaseSensitive())
+        {
+          comparison = " AND value IN (" + values + ")";
+        }
+        else
+        {
+          comparison = " AND lower(value) IN (" + values + ")";
+        }
+
+        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 if (escapeBrackets && value[i] == '[')
+            {
+              escaped += "\\[";
+            }
+            else if (escapeBrackets && value[i] == ']')
+            {
+              escaped += "\\]";
+            }
+            else
+            {
+              escaped += value[i];
+            }
+          }
+
+          std::string parameter = formatter.GenerateParameter(escaped);
+
+          if (constraint.IsCaseSensitive())
+          {
+            comparison = " AND value LIKE " + parameter + " " + formatter.FormatWildcardEscape();
+          }
+          else
+          {
+            comparison = " AND lower(value) LIKE lower(" + parameter + ") " + formatter.FormatWildcardEscape();
+          }
+        }
+
+        break;
+      }
+
+      default:
+        return false;
+    }
+
+    if (constraint.IsMandatory())
+    {
+      target = tagFilter + comparison;
+    }
+    else if (comparison.empty())
+    {
+      target = tagFilter + " AND value IS NULL";
+    }
+    else
+    {
+      target = tagFilter + " AND value IS NULL OR " + comparison;
+    }
+
+    return true;
+  }
+
+
+  void ISqlLookupFormatter::GetLookupLevels(ResourceType& lowerLevel,
+                                            ResourceType& upperLevel,
+                                            const ResourceType& queryLevel,
+                                            const DatabaseConstraints& lookup)
+  {
+    assert(ResourceType_Patient < ResourceType_Study &&
+           ResourceType_Study < ResourceType_Series &&
+           ResourceType_Series < ResourceType_Instance);
+
+    lowerLevel = queryLevel;
+    upperLevel = queryLevel;
+
+    for (size_t i = 0; i < lookup.GetSize(); i++)
+    {
+      ResourceType level = lookup.GetConstraint(i).GetLevel();
+
+      if (level < upperLevel)
+      {
+        upperLevel = level;
+      }
+
+      if (level > lowerLevel)
+      {
+        lowerLevel = level;
+      }
+    }
+  }
+
+
+  void ISqlLookupFormatter::Apply(std::string& sql,
+                                  ISqlLookupFormatter& formatter,
+                                  const DatabaseConstraints& lookup,
+                                  ResourceType queryLevel,
+                                  const std::set<std::string>& labels,
+                                  LabelsConstraint labelsConstraint,
+                                  size_t limit)
+  {
+    ResourceType lowerLevel, upperLevel;
+    GetLookupLevels(lowerLevel, upperLevel, queryLevel, lookup);
+
+    assert(upperLevel <= queryLevel &&
+           queryLevel <= lowerLevel);
+
+    const bool escapeBrackets = formatter.IsEscapeBrackets();
+
+    std::string joins, comparisons;
+
+    size_t count = 0;
+
+    for (size_t i = 0; i < lookup.GetSize(); i++)
+    {
+      const DatabaseConstraint& constraint = lookup.GetConstraint(i);
+
+      std::string comparison;
+
+      if (FormatComparison(comparison, formatter, constraint, count, escapeBrackets))
+      {
+        std::string join;
+        FormatJoin(join, constraint, 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");
+    }
+
+    std::list<std::string> where;
+    where.push_back(FormatLevel(queryLevel) + ".resourceType = " +
+                    formatter.FormatResourceType(queryLevel) + comparisons);
+
+    if (!labels.empty())
+    {
+      /**
+       * "In SQL Server, NOT EXISTS and NOT IN predicates are the best
+       * way to search for missing values, as long as both columns in
+       * question are NOT NULL."
+       * https://explainextended.com/2009/09/15/not-in-vs-not-exists-vs-left-join-is-null-sql-server/
+       **/
+
+      std::list<std::string> formattedLabels;
+      for (std::set<std::string>::const_iterator it = labels.begin(); it != labels.end(); ++it)
+      {
+        formattedLabels.push_back(formatter.GenerateParameter(*it));
+      }
+
+      std::string condition;
+      switch (labelsConstraint)
+      {
+        case LabelsConstraint_Any:
+          condition = "> 0";
+          break;
+
+        case LabelsConstraint_All:
+          condition = "= " + boost::lexical_cast<std::string>(labels.size());
+          break;
+
+        case LabelsConstraint_None:
+          condition = "= 0";
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+
+      where.push_back("(SELECT COUNT(1) FROM Labels AS selectedLabels WHERE selectedLabels.id = " + FormatLevel(queryLevel) +
+                      ".internalId AND selectedLabels.label IN (" + Join(formattedLabels, "", ", ") + ")) " + condition);
+    }
+
+    sql += joins + Join(where, " WHERE ", " AND ");
+
+    if (limit != 0)
+    {
+      sql += " LIMIT " + boost::lexical_cast<std::string>(limit);
+    }
+  }
+
+
+#if ORTHANC_PLUGINS_HAS_INTEGRATED_FIND == 1
+  static ResourceType DetectLevel(const Orthanc::DatabasePluginMessages::Find_Request& request)
+  {
+    // This corresponds to "Orthanc::OrthancIdentifiers()::DetectLevel()" in the Orthanc core
+    if (!request.orthanc_id_patient().empty() &&
+        request.orthanc_id_study().empty() &&
+        request.orthanc_id_series().empty() &&
+        request.orthanc_id_instance().empty())
+    {
+      return ResourceType_Patient;
+    }
+    else if (!request.orthanc_id_study().empty() &&
+             request.orthanc_id_series().empty() &&
+             request.orthanc_id_instance().empty())
+    {
+      return ResourceType_Study;
+    }
+    else if (!request.orthanc_id_series().empty() &&
+             request.orthanc_id_instance().empty())
+    {
+      return ResourceType_Series;
+    }
+    else if (!request.orthanc_id_instance().empty())
+    {
+      return ResourceType_Instance;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+  }
+
+  void ISqlLookupFormatter::Apply(std::string& sql,
+                                  ISqlLookupFormatter& formatter,
+                                  const Orthanc::DatabasePluginMessages::Find_Request& request)
+  {
+    const bool escapeBrackets = formatter.IsEscapeBrackets();
+    ResourceType queryLevel = OrthancDatabases::MessagesToolbox::Convert(request.level());
+    const std::string& strQueryLevel = FormatLevel(queryLevel);
+
+    DatabaseConstraints constraints;
+
+    for (int i = 0; i < request.dicom_tag_constraints().size(); i++)
+    {
+      constraints.AddConstraint(new DatabaseConstraint(request.dicom_tag_constraints(i)));
+    }
+
+    ResourceType lowerLevel, upperLevel;
+    GetLookupLevels(lowerLevel, upperLevel, queryLevel, constraints);
+
+    assert(upperLevel <= queryLevel &&
+           queryLevel <= lowerLevel);
+
+
+    sql = ("SELECT " +
+           strQueryLevel + ".publicId, " +
+           strQueryLevel + ".internalId" +
+           " FROM Resources AS " + strQueryLevel);
+
+
+    std::string joins, comparisons;
+
+    const bool isOrthancIdentifiersDefined = (!request.orthanc_id_patient().empty() ||
+                                              !request.orthanc_id_study().empty() ||
+                                              !request.orthanc_id_series().empty() ||
+                                              !request.orthanc_id_instance().empty());
+
+    if (isOrthancIdentifiersDefined &&
+        Orthanc::IsResourceLevelAboveOrEqual(DetectLevel(request), queryLevel))
+    {
+      // single child resource matching, there should not be other constraints (at least for now)
+      if (request.dicom_tag_constraints().size() != 0 ||
+          request.labels().size() != 0 ||
+          request.has_limits())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+      }
+
+      ResourceType topParentLevel = DetectLevel(request);
+      const std::string& strTopParentLevel = FormatLevel(topParentLevel);
+
+      std::string publicId;
+      switch (topParentLevel)
+      {
+        case ResourceType_Patient:
+          publicId = request.orthanc_id_patient();
+          break;
+
+        case ResourceType_Study:
+          publicId = request.orthanc_id_study();
+          break;
+
+        case ResourceType_Series:
+          publicId = request.orthanc_id_series();
+          break;
+
+        case ResourceType_Instance:
+          publicId = request.orthanc_id_instance();
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+
+      if (publicId.empty())
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      comparisons = " AND " + strTopParentLevel + ".publicId = " + formatter.GenerateParameter(publicId);
+
+      for (int level = queryLevel; level > topParentLevel; level--)
+      {
+        sql += (" INNER JOIN Resources " +
+                FormatLevel(static_cast<ResourceType>(level - 1)) + " ON " +
+                FormatLevel(static_cast<ResourceType>(level - 1)) + ".internalId=" +
+                FormatLevel(static_cast<ResourceType>(level)) + ".parentId");
+      }
+    }
+    else
+    {
+      size_t count = 0;
+
+      for (size_t i = 0; i < constraints.GetSize(); i++)
+      {
+        const DatabaseConstraint& constraint = constraints.GetConstraint(i);
+
+        std::string comparison;
+
+        if (FormatComparison(comparison, formatter, constraint, count, escapeBrackets))
+        {
+          std::string join;
+          FormatJoin(join, constraint, count);
+          joins += join;
+
+          if (!comparison.empty())
+          {
+            comparisons += " AND " + comparison;
+          }
+
+          count ++;
+        }
+      }
+    }
+
+    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");
+    }
+
+    std::list<std::string> where;
+    where.push_back(strQueryLevel + ".resourceType = " +
+                    formatter.FormatResourceType(queryLevel) + comparisons);
+
+
+    if (!request.labels().empty())
+    {
+      /**
+       * "In SQL Server, NOT EXISTS and NOT IN predicates are the best
+       * way to search for missing values, as long as both columns in
+       * question are NOT NULL."
+       * https://explainextended.com/2009/09/15/not-in-vs-not-exists-vs-left-join-is-null-sql-server/
+       **/
+
+      std::list<std::string> formattedLabels;
+      for (int i = 0; i < request.labels().size(); i++)
+      {
+        formattedLabels.push_back(formatter.GenerateParameter(request.labels(i)));
+      }
+
+      std::string condition;
+      switch (request.labels_constraint())
+      {
+        case Orthanc::DatabasePluginMessages::LABELS_CONSTRAINT_ANY:
+          condition = "> 0";
+          break;
+
+        case Orthanc::DatabasePluginMessages::LABELS_CONSTRAINT_ALL:
+          condition = "= " + boost::lexical_cast<std::string>(request.labels().size());
+          break;
+
+        case Orthanc::DatabasePluginMessages::LABELS_CONSTRAINT_NONE:
+          condition = "= 0";
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+
+      where.push_back("(SELECT COUNT(1) FROM Labels AS selectedLabels WHERE selectedLabels.id = " + strQueryLevel +
+                      ".internalId AND selectedLabels.label IN (" + Join(formattedLabels, "", ", ") + ")) " + condition);
+    }
+
+    sql += joins + Join(where, " WHERE ", " AND ");
+
+    if (request.has_limits())
+    {
+      sql += formatter.FormatLimits(request.limits().since(), request.limits().count());
+    }
+
+  }
+#endif
+
+
+  void ISqlLookupFormatter::ApplySingleLevel(std::string& sql,
+                                             ISqlLookupFormatter& formatter,
+                                             const DatabaseConstraints& lookup,
+                                             ResourceType queryLevel,
+                                             const std::set<std::string>& labels,
+                                             LabelsConstraint labelsConstraint,
+                                             size_t limit
+                                             )
+  {
+    ResourceType lowerLevel, upperLevel;
+    GetLookupLevels(lowerLevel, upperLevel, queryLevel, lookup);
+
+    assert(upperLevel == queryLevel &&
+           queryLevel == lowerLevel);
+
+    const bool escapeBrackets = formatter.IsEscapeBrackets();
+
+    std::vector<std::string> mainDicomTagsComparisons, dicomIdentifiersComparisons;
+
+    for (size_t i = 0; i < lookup.GetSize(); i++)
+    {
+      const DatabaseConstraint& constraint = lookup.GetConstraint(i);
+
+      std::string comparison;
+
+      if (FormatComparison2(comparison, formatter, constraint, escapeBrackets))
+      {
+        if (!comparison.empty())
+        {
+          if (constraint.IsIdentifier())
+          {
+            dicomIdentifiersComparisons.push_back(comparison);
+          }
+          else
+          {
+            mainDicomTagsComparisons.push_back(comparison);
+          }
+        }
+      }
+    }
+
+    sql = ("SELECT publicId, internalId "
+           "FROM Resources "
+           "WHERE resourceType = " + formatter.FormatResourceType(queryLevel)
+            + " ");
+
+    if (dicomIdentifiersComparisons.size() > 0)
+    {
+      for (std::vector<std::string>::const_iterator it = dicomIdentifiersComparisons.begin(); it < dicomIdentifiersComparisons.end(); ++it)
+      {
+        sql += (" AND internalId IN (SELECT id FROM DicomIdentifiers WHERE " + *it + ") ");
+      }
+    }
+
+    if (mainDicomTagsComparisons.size() > 0)
+    {
+      for (std::vector<std::string>::const_iterator it = mainDicomTagsComparisons.begin(); it < mainDicomTagsComparisons.end(); ++it)
+      {
+        sql += (" AND internalId IN (SELECT id FROM MainDicomTags WHERE " + *it + ") ");
+      }
+    }
+
+    if (!labels.empty())
+    {
+      /**
+       * "In SQL Server, NOT EXISTS and NOT IN predicates are the best
+       * way to search for missing values, as long as both columns in
+       * question are NOT NULL."
+       * https://explainextended.com/2009/09/15/not-in-vs-not-exists-vs-left-join-is-null-sql-server/
+       **/
+
+      std::list<std::string> formattedLabels;
+      for (std::set<std::string>::const_iterator it = labels.begin(); it != labels.end(); ++it)
+      {
+        formattedLabels.push_back(formatter.GenerateParameter(*it));
+      }
+
+      std::string condition;
+      std::string inOrNotIn;
+      switch (labelsConstraint)
+      {
+        case LabelsConstraint_Any:
+          condition = "> 0";
+          inOrNotIn = "IN";
+          break;
+
+        case LabelsConstraint_All:
+          condition = "= " + boost::lexical_cast<std::string>(labels.size());
+          inOrNotIn = "IN";
+          break;
+
+        case LabelsConstraint_None:
+          condition = "> 0";
+          inOrNotIn = "NOT IN";
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+
+      sql += (" AND internalId " + inOrNotIn + " (SELECT id"
+                                 " FROM (SELECT id, COUNT(1) AS labelsCount "
+                                        "FROM Labels "
+                                        "WHERE label IN (" + Join(formattedLabels, "", ", ") + ") GROUP BY id"
+                                        ") AS temp "
+                                 " WHERE labelsCount " + condition + ")");
+    }
+
+    if (limit != 0)
+    {
+      sql += " LIMIT " + boost::lexical_cast<std::string>(limit);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Plugins/ISqlLookupFormatter.h	Mon Sep 09 15:04:48 2024 +0200
@@ -0,0 +1,99 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+/**
+ * NB: Until 2024-09-09, this file was synchronized with the following
+ * folder from the Orthanc main project:
+ * https://orthanc.uclouvain.be/hg/orthanc/file/default/OrthancServer/Sources/Search/
+ **/
+
+
+#pragma once
+
+#include "MessagesToolbox.h"
+
+#include <boost/noncopyable.hpp>
+#include <vector>
+
+namespace Orthanc
+{
+  class DatabaseConstraints;
+  class FindRequest;
+
+  enum LabelsConstraint
+  {
+    LabelsConstraint_All,
+    LabelsConstraint_Any,
+    LabelsConstraint_None
+  };
+
+  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;
+
+    virtual std::string FormatLimits(uint64_t since, uint64_t count) = 0;
+
+    /**
+     * Whether to escape '[' and ']', which is only needed for
+     * MSSQL. New in Orthanc 1.10.0, from the following changeset:
+     * https://orthanc.uclouvain.be/hg/orthanc-databases/rev/389c037387ea
+     **/
+    virtual bool IsEscapeBrackets() const = 0;
+
+    static void GetLookupLevels(ResourceType& lowerLevel,
+                                ResourceType& upperLevel,
+                                const ResourceType& queryLevel,
+                                const DatabaseConstraints& lookup);
+
+    static void Apply(std::string& sql,
+                      ISqlLookupFormatter& formatter,
+                      const DatabaseConstraints& lookup,
+                      ResourceType queryLevel,
+                      const std::set<std::string>& labels,  // New in Orthanc 1.12.0
+                      LabelsConstraint labelsConstraint,    // New in Orthanc 1.12.0
+                      size_t limit);
+
+    static void ApplySingleLevel(std::string& sql,
+                                 ISqlLookupFormatter& formatter,
+                                 const DatabaseConstraints& lookup,
+                                 ResourceType queryLevel,
+                                 const std::set<std::string>& labels,  // New in Orthanc 1.12.0
+                                 LabelsConstraint labelsConstraint,    // New in Orthanc 1.12.0
+                                 size_t limit);
+
+#if ORTHANC_PLUGINS_HAS_INTEGRATED_FIND == 1
+    static void Apply(std::string& sql,
+                      ISqlLookupFormatter& formatter,
+                      const Orthanc::DatabasePluginMessages::Find_Request& request);
+#endif
+  };
+}
--- a/Framework/Plugins/IndexUnitTests.h	Mon Sep 09 12:48:52 2024 +0200
+++ b/Framework/Plugins/IndexUnitTests.h	Mon Sep 09 15:04:48 2024 +0200
@@ -29,8 +29,6 @@
 
 #include <Compatibility.h>  // For std::unique_ptr<>
 
-#include <orthanc/OrthancCDatabasePlugin.h>
-
 #include <gtest/gtest.h>
 #include <list>
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Plugins/MessagesToolbox.cpp	Mon Sep 09 15:04:48 2024 +0200
@@ -0,0 +1,52 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "MessagesToolbox.h"
+
+
+namespace OrthancDatabases
+{
+  namespace MessagesToolbox
+  {
+    Orthanc::ResourceType Convert(Orthanc::DatabasePluginMessages::ResourceType resourceType)
+    {
+      switch (resourceType)
+      {
+        case Orthanc::DatabasePluginMessages::RESOURCE_PATIENT:
+          return Orthanc::ResourceType_Patient;
+
+        case Orthanc::DatabasePluginMessages::RESOURCE_STUDY:
+          return Orthanc::ResourceType_Study;
+
+        case Orthanc::DatabasePluginMessages::RESOURCE_SERIES:
+          return Orthanc::ResourceType_Series;
+
+        case Orthanc::DatabasePluginMessages::RESOURCE_INSTANCE:
+          return Orthanc::ResourceType_Instance;
+
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Plugins/MessagesToolbox.h	Mon Sep 09 15:04:48 2024 +0200
@@ -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-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include <orthanc/OrthancCDatabasePlugin.h>
+
+// Ensure that "ORTHANC_PLUGINS_VERSION_IS_ABOVE" is defined
+#include "../../Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h"
+
+#if defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE)         // Macro introduced in Orthanc 1.3.1
+#  if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 5)
+#    include <OrthancDatabasePlugin.pb.h>
+#  endif
+#endif
+
+
+#define ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT 0
+
+#if defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE)
+#  if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 5, 2)
+#    undef  ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT
+#    define ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT 1
+#  endif
+#endif
+
+
+#define ORTHANC_PLUGINS_HAS_INTEGRATED_FIND 0
+
+#if defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE)
+#  if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 5)
+#    undef  ORTHANC_PLUGINS_HAS_INTEGRATED_FIND
+#    define ORTHANC_PLUGINS_HAS_INTEGRATED_FIND 1
+#  endif
+#endif
+
+
+#include <Enumerations.h>
+
+
+namespace OrthancDatabases
+{
+  namespace MessagesToolbox
+  {
+    Orthanc::ResourceType Convert(Orthanc::DatabasePluginMessages::ResourceType resourceType);
+  }
+}
--- a/Resources/CMake/DatabasesPluginConfiguration.cmake	Mon Sep 09 12:48:52 2024 +0200
+++ b/Resources/CMake/DatabasesPluginConfiguration.cmake	Mon Sep 09 15:04:48 2024 +0200
@@ -92,8 +92,6 @@
 
 add_definitions(
   -DHAS_ORTHANC_EXCEPTION=1
-  -DORTHANC_BUILDING_SERVER_LIBRARY=0
-  -DORTHANC_ENABLE_PLUGINS=1  # To build "DatabaseConstraint.h" imported from Orthanc core
   -DORTHANC_OPTIMAL_VERSION_MAJOR=${ORTHANC_OPTIMAL_VERSION_MAJOR}
   -DORTHANC_OPTIMAL_VERSION_MINOR=${ORTHANC_OPTIMAL_VERSION_MINOR}
   -DORTHANC_OPTIMAL_VERSION_REVISION=${ORTHANC_OPTIMAL_VERSION_REVISION}
@@ -114,10 +112,11 @@
   ${ORTHANC_DATABASES_ROOT}/Framework/Plugins/DatabaseBackendAdapterV2.cpp
   ${ORTHANC_DATABASES_ROOT}/Framework/Plugins/DatabaseBackendAdapterV3.cpp
   ${ORTHANC_DATABASES_ROOT}/Framework/Plugins/DatabaseBackendAdapterV4.cpp
+  ${ORTHANC_DATABASES_ROOT}/Framework/Plugins/DatabaseConstraint.cpp
+  ${ORTHANC_DATABASES_ROOT}/Framework/Plugins/ISqlLookupFormatter.cpp
   ${ORTHANC_DATABASES_ROOT}/Framework/Plugins/IndexBackend.cpp
   ${ORTHANC_DATABASES_ROOT}/Framework/Plugins/IndexConnectionsPool.cpp
+  ${ORTHANC_DATABASES_ROOT}/Framework/Plugins/MessagesToolbox.cpp
   ${ORTHANC_DATABASES_ROOT}/Framework/Plugins/StorageBackend.cpp
-  ${ORTHANC_DATABASES_ROOT}/Resources/Orthanc/Databases/DatabaseConstraint.cpp
-  ${ORTHANC_DATABASES_ROOT}/Resources/Orthanc/Databases/ISqlLookupFormatter.cpp
   ${ORTHANC_DATABASES_ROOT}/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp
   )
--- a/Resources/Orthanc/Databases/DatabaseConstraint.cpp	Mon Sep 09 12:48:52 2024 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,348 +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-2023 Osimis S.A., Belgium
- * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, 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.
- * 
- * 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/>.
- **/
-
-
-#if !defined(ORTHANC_BUILDING_SERVER_LIBRARY)
-#  error Macro ORTHANC_BUILDING_SERVER_LIBRARY must be defined
-#endif
-
-#if ORTHANC_BUILDING_SERVER_LIBRARY == 1
-#  include "../PrecompiledHeadersServer.h"
-#endif
-
-#include "DatabaseConstraint.h"
-
-#if ORTHANC_BUILDING_SERVER_LIBRARY == 1
-#  include "../../../OrthancFramework/Sources/OrthancException.h"
-#else
-#  include <OrthancException.h>
-#endif
-
-#include <boost/lexical_cast.hpp>
-#include <cassert>
-
-
-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    
-
-
-  void DatabaseConstraints::Clear()
-  {
-    for (size_t i = 0; i < constraints_.size(); i++)
-    {
-      assert(constraints_[i] != NULL);
-      delete constraints_[i];
-    }
-
-    constraints_.clear();
-  }
-
-
-  void DatabaseConstraints::AddConstraint(DatabaseConstraint* constraint)
-  {
-    if (constraint == NULL)
-    {
-      throw OrthancException(ErrorCode_NullPointer);
-    }
-    else
-    {
-      constraints_.push_back(constraint);
-    }
-  }
-
-
-  const DatabaseConstraint& DatabaseConstraints::GetConstraint(size_t index) const
-  {
-    if (index >= constraints_.size())
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-    else
-    {
-      assert(constraints_[index] != NULL);
-      return *constraints_[index];
-    }
-  }
-
-
-  std::string DatabaseConstraints::Format() const
-  {
-    std::string s;
-
-    for (size_t i = 0; i < constraints_.size(); i++)
-    {
-      assert(constraints_[i] != NULL);
-      const DatabaseConstraint& constraint = *constraints_[i];
-      s += "Constraint " + boost::lexical_cast<std::string>(i) + " at " + EnumerationToString(constraint.GetLevel()) +
-        ": " + constraint.GetTag().Format();
-
-      switch (constraint.GetConstraintType())
-      {
-        case ConstraintType_Equal:
-          s += " == " + constraint.GetSingleValue();
-          break;
-
-        case ConstraintType_SmallerOrEqual:
-          s += " <= " + constraint.GetSingleValue();
-          break;
-
-        case ConstraintType_GreaterOrEqual:
-          s += " >= " + constraint.GetSingleValue();
-          break;
-
-        case ConstraintType_Wildcard:
-          s += " ~~ " + constraint.GetSingleValue();
-          break;
-
-        case ConstraintType_List:
-        {
-          s += " in [ ";
-          bool first = true;
-          for (size_t j = 0; j < constraint.GetValuesCount(); j++)
-          {
-            if (first)
-            {
-              first = false;
-            }
-            else
-            {
-              s += ", ";
-            }
-            s += constraint.GetValue(j);
-          }
-          s += "]";
-          break;
-        }
-
-        default:
-          throw OrthancException(ErrorCode_InternalError);
-      }
-
-      s += "\n";
-    }
-
-    return s;
-  }
-}
--- a/Resources/Orthanc/Databases/DatabaseConstraint.h	Mon Sep 09 12:48:52 2024 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,184 +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-2023 Osimis S.A., Belgium
- * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, 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.
- * 
- * 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
-
-#if !defined(ORTHANC_BUILDING_SERVER_LIBRARY)
-#  error Macro ORTHANC_BUILDING_SERVER_LIBRARY must be defined
-#endif
-
-#if ORTHANC_BUILDING_SERVER_LIBRARY == 1
-#  include "../../../OrthancFramework/Sources/DicomFormat/DicomMap.h"
-#else
-// This is for the "orthanc-databases" project to reuse this file
-#  include <DicomFormat/DicomMap.h>
-#endif
-
-#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
-
-#include <deque>
-
-namespace Orthanc
-{
-  enum ConstraintType
-  {
-    ConstraintType_Equal,
-    ConstraintType_SmallerOrEqual,
-    ConstraintType_GreaterOrEqual,
-    ConstraintType_Wildcard,
-    ConstraintType_List
-  };
-
-  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 : public boost::noncopyable
-  {
-  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
-    explicit 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    
-  };
-
-
-  class DatabaseConstraints : public boost::noncopyable
-  {
-  private:
-    std::deque<DatabaseConstraint*>  constraints_;
-
-  public:
-    ~DatabaseConstraints()
-    {
-      Clear();
-    }
-
-    void Clear();
-
-    void AddConstraint(DatabaseConstraint* constraint);  // Takes ownership
-
-    bool IsEmpty() const
-    {
-      return constraints_.empty();
-    }
-
-    size_t GetSize() const
-    {
-      return constraints_.size();
-    }
-
-    const DatabaseConstraint& GetConstraint(size_t index) const;
-
-    std::string Format() const;
-  };
-}
--- a/Resources/Orthanc/Databases/ISqlLookupFormatter.cpp	Mon Sep 09 12:48:52 2024 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,876 +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-2023 Osimis S.A., Belgium
- * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, 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.
- * 
- * 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/>.
- **/
-
-
-#if !defined(ORTHANC_BUILDING_SERVER_LIBRARY)
-#  error Macro ORTHANC_BUILDING_SERVER_LIBRARY must be defined
-#endif
-
-#if ORTHANC_BUILDING_SERVER_LIBRARY == 1
-#  include "../PrecompiledHeadersServer.h"
-#endif
-
-#include "ISqlLookupFormatter.h"
-
-#if ORTHANC_BUILDING_SERVER_LIBRARY == 1
-#  include "../../../OrthancFramework/Sources/OrthancException.h"
-#  include "../../../OrthancFramework/Sources/Toolbox.h"
-#  include "../Database/FindRequest.h"
-#else
-#  include <OrthancException.h>
-#  include <Toolbox.h>
-#endif
-
-#include "DatabaseConstraint.h"
-
-#include <cassert>
-#include <boost/lexical_cast.hpp>
-#include <list>
-
-
-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,
-                               bool escapeBrackets)
-  {
-    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 if (escapeBrackets && value[i] == '[')
-            {
-              escaped += "\\[";
-            }
-            else if (escapeBrackets && 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()));
-  }
-
-
-  static std::string Join(const std::list<std::string>& values,
-                          const std::string& prefix,
-                          const std::string& separator)
-  {
-    if (values.empty())
-    {
-      return "";
-    }
-    else
-    {
-      std::string s = prefix;
-
-      bool first = true;
-      for (std::list<std::string>::const_iterator it = values.begin(); it != values.end(); ++it)
-      {
-        if (first)
-        {
-          first = false;
-        }
-        else
-        {
-          s += separator;
-        }
-
-        s += *it;
-      }
-
-      return s;
-    }
-  }
-
-  static bool FormatComparison2(std::string& target,
-                                ISqlLookupFormatter& formatter,
-                                const DatabaseConstraint& constraint,
-                                bool escapeBrackets)
-  {
-    std::string comparison;
-    std::string tagFilter = ("tagGroup = " + boost::lexical_cast<std::string>(constraint.GetTag().GetGroup())
-                              + " AND tagElement = " + boost::lexical_cast<std::string>(constraint.GetTag().GetElement()));
-
-    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 = " AND value " + op + " " + parameter;
-        }
-        else
-        {
-          comparison = " AND lower(value) " + op + " lower(" + parameter + ")";
-        }
-
-        break;
-      }
-
-      case ConstraintType_List:
-      {
-        std::vector<std::string> comparisonValues;
-        for (size_t i = 0; i < constraint.GetValuesCount(); i++)
-        {
-          std::string parameter = formatter.GenerateParameter(constraint.GetValue(i));
-
-          if (constraint.IsCaseSensitive())
-          {
-            comparisonValues.push_back(parameter);
-          }
-          else
-          {
-            comparisonValues.push_back("lower(" + parameter + ")");
-          }
-        }
-
-        std::string values;
-        Toolbox::JoinStrings(values, comparisonValues, ", ");
-
-        if (constraint.IsCaseSensitive())
-        {
-          comparison = " AND value IN (" + values + ")";
-        }
-        else
-        {
-          comparison = " AND lower(value) IN (" + values + ")";
-        }
-            
-        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 if (escapeBrackets && value[i] == '[')
-            {
-              escaped += "\\[";
-            }
-            else if (escapeBrackets && value[i] == ']')
-            {
-              escaped += "\\]";
-            }
-            else
-            {
-              escaped += value[i];
-            }               
-          }
-
-          std::string parameter = formatter.GenerateParameter(escaped);
-
-          if (constraint.IsCaseSensitive())
-          {
-            comparison = " AND value LIKE " + parameter + " " + formatter.FormatWildcardEscape();
-          }
-          else
-          {
-            comparison = " AND lower(value) LIKE lower(" + parameter + ") " + formatter.FormatWildcardEscape();
-          }
-        }
-          
-        break;
-      }
-
-      default:
-        return false;
-    }
-
-    if (constraint.IsMandatory())
-    {
-      target = tagFilter + comparison;
-    }
-    else if (comparison.empty())
-    {
-      target = tagFilter + " AND value IS NULL";
-    }
-    else
-    {
-      target = tagFilter + " AND value IS NULL OR " + comparison;
-    }
-
-    return true;
-  }
-
-
-  void ISqlLookupFormatter::GetLookupLevels(ResourceType& lowerLevel,
-                                            ResourceType& upperLevel,
-                                            const ResourceType& queryLevel,
-                                            const DatabaseConstraints& lookup)
-  {
-    assert(ResourceType_Patient < ResourceType_Study &&
-           ResourceType_Study < ResourceType_Series &&
-           ResourceType_Series < ResourceType_Instance);
-    
-    lowerLevel = queryLevel;
-    upperLevel = queryLevel;
-
-    for (size_t i = 0; i < lookup.GetSize(); i++)
-    {
-      ResourceType level = lookup.GetConstraint(i).GetLevel();
-
-      if (level < upperLevel)
-      {
-        upperLevel = level;
-      }
-
-      if (level > lowerLevel)
-      {
-        lowerLevel = level;
-      }
-    }
-  }
-  
-
-  void ISqlLookupFormatter::Apply(std::string& sql,
-                                  ISqlLookupFormatter& formatter,
-                                  const DatabaseConstraints& lookup,
-                                  ResourceType queryLevel,
-                                  const std::set<std::string>& labels,
-                                  LabelsConstraint labelsConstraint,
-                                  size_t limit)
-  {
-    ResourceType lowerLevel, upperLevel;
-    GetLookupLevels(lowerLevel, upperLevel, queryLevel, lookup);
-
-    assert(upperLevel <= queryLevel &&
-           queryLevel <= lowerLevel);
-
-    const bool escapeBrackets = formatter.IsEscapeBrackets();
-    
-    std::string joins, comparisons;
-
-    size_t count = 0;
-    
-    for (size_t i = 0; i < lookup.GetSize(); i++)
-    {
-      const DatabaseConstraint& constraint = lookup.GetConstraint(i);
-
-      std::string comparison;
-      
-      if (FormatComparison(comparison, formatter, constraint, count, escapeBrackets))
-      {
-        std::string join;
-        FormatJoin(join, constraint, 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");
-    }
-
-    std::list<std::string> where;
-    where.push_back(FormatLevel(queryLevel) + ".resourceType = " +
-                    formatter.FormatResourceType(queryLevel) + comparisons);
-
-    if (!labels.empty())
-    {
-      /**
-       * "In SQL Server, NOT EXISTS and NOT IN predicates are the best
-       * way to search for missing values, as long as both columns in
-       * question are NOT NULL."
-       * https://explainextended.com/2009/09/15/not-in-vs-not-exists-vs-left-join-is-null-sql-server/
-       **/
-
-      std::list<std::string> formattedLabels;
-      for (std::set<std::string>::const_iterator it = labels.begin(); it != labels.end(); ++it)
-      {
-        formattedLabels.push_back(formatter.GenerateParameter(*it));
-      }
-
-      std::string condition;
-      switch (labelsConstraint)
-      {
-        case LabelsConstraint_Any:
-          condition = "> 0";
-          break;
-          
-        case LabelsConstraint_All:
-          condition = "= " + boost::lexical_cast<std::string>(labels.size());
-          break;
-          
-        case LabelsConstraint_None:
-          condition = "= 0";
-          break;
-          
-        default:
-          throw OrthancException(ErrorCode_ParameterOutOfRange);
-      }
-      
-      where.push_back("(SELECT COUNT(1) FROM Labels AS selectedLabels WHERE selectedLabels.id = " + FormatLevel(queryLevel) +
-                      ".internalId AND selectedLabels.label IN (" + Join(formattedLabels, "", ", ") + ")) " + condition);
-    }
-
-    sql += joins + Join(where, " WHERE ", " AND ");
-
-    if (limit != 0)
-    {
-      sql += " LIMIT " + boost::lexical_cast<std::string>(limit);
-    }
-  }
-
-#if ORTHANC_BUILDING_SERVER_LIBRARY == 1
-  void ISqlLookupFormatter::Apply(std::string& sql,
-                                  ISqlLookupFormatter& formatter,
-                                  const FindRequest& request)
-  {
-    const bool escapeBrackets = formatter.IsEscapeBrackets();
-    ResourceType queryLevel = request.GetLevel();
-    const std::string& strQueryLevel = FormatLevel(queryLevel);
-
-    ResourceType lowerLevel, upperLevel;
-    GetLookupLevels(lowerLevel, upperLevel, queryLevel, request.GetDicomTagConstraints());
-
-    assert(upperLevel <= queryLevel &&
-           queryLevel <= lowerLevel);
-
-
-    sql = ("SELECT " +
-           strQueryLevel + ".publicId, " +
-           strQueryLevel + ".internalId" +
-           " FROM Resources AS " + strQueryLevel);
-
-
-    std::string joins, comparisons;
-
-    if (request.GetOrthancIdentifiers().IsDefined() && request.GetOrthancIdentifiers().DetectLevel() <= queryLevel)
-    {
-      // single child resource matching, there should not be other constraints (at least for now)
-      assert(request.GetDicomTagConstraints().GetSize() == 0);
-      assert(request.GetLabels().size() == 0);
-      assert(request.HasLimits() == false);
-
-      ResourceType topParentLevel = request.GetOrthancIdentifiers().DetectLevel();
-      const std::string& strTopParentLevel = FormatLevel(topParentLevel);
-
-      comparisons = " AND " + strTopParentLevel + ".publicId = " + formatter.GenerateParameter(request.GetOrthancIdentifiers().GetLevel(topParentLevel));
-
-      for (int level = queryLevel; level > topParentLevel; level--)
-      {
-        sql += (" INNER JOIN Resources " +
-                FormatLevel(static_cast<ResourceType>(level - 1)) + " ON " +
-                FormatLevel(static_cast<ResourceType>(level - 1)) + ".internalId=" +
-                FormatLevel(static_cast<ResourceType>(level)) + ".parentId");
-      }
-    }
-    else
-    {
-      size_t count = 0;
-      
-      const DatabaseConstraints& dicomTagsConstraints = request.GetDicomTagConstraints();
-      for (size_t i = 0; i < dicomTagsConstraints.GetSize(); i++)
-      {
-        const DatabaseConstraint& constraint = dicomTagsConstraints.GetConstraint(i);
-
-        std::string comparison;
-        
-        if (FormatComparison(comparison, formatter, constraint, count, escapeBrackets))
-        {
-          std::string join;
-          FormatJoin(join, constraint, count);
-          joins += join;
-
-          if (!comparison.empty())
-          {
-            comparisons += " AND " + comparison;
-          }
-          
-          count ++;
-        }
-      }
-    }
-
-    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");
-    }
-
-    std::list<std::string> where;
-    where.push_back(strQueryLevel + ".resourceType = " +
-                    formatter.FormatResourceType(queryLevel) + comparisons);
-
-
-    if (!request.GetLabels().empty())
-    {
-      /**
-       * "In SQL Server, NOT EXISTS and NOT IN predicates are the best
-       * way to search for missing values, as long as both columns in
-       * question are NOT NULL."
-       * https://explainextended.com/2009/09/15/not-in-vs-not-exists-vs-left-join-is-null-sql-server/
-       **/
-
-      const std::set<std::string>& labels = request.GetLabels();
-      std::list<std::string> formattedLabels;
-      for (std::set<std::string>::const_iterator it = labels.begin(); it != labels.end(); ++it)
-      {
-        formattedLabels.push_back(formatter.GenerateParameter(*it));
-      }
-
-      std::string condition;
-      switch (request.GetLabelsConstraint())
-      {
-        case LabelsConstraint_Any:
-          condition = "> 0";
-          break;
-          
-        case LabelsConstraint_All:
-          condition = "= " + boost::lexical_cast<std::string>(labels.size());
-          break;
-          
-        case LabelsConstraint_None:
-          condition = "= 0";
-          break;
-          
-        default:
-          throw OrthancException(ErrorCode_ParameterOutOfRange);
-      }
-      
-      where.push_back("(SELECT COUNT(1) FROM Labels AS selectedLabels WHERE selectedLabels.id = " + strQueryLevel +
-                      ".internalId AND selectedLabels.label IN (" + Join(formattedLabels, "", ", ") + ")) " + condition);
-    }
-
-    sql += joins + Join(where, " WHERE ", " AND ");
-
-    if (request.HasLimits())
-    {
-      sql += formatter.FormatLimits(request.GetLimitsSince(), request.GetLimitsCount());
-    }
-
-  }
-#endif
-
-
-  void ISqlLookupFormatter::ApplySingleLevel(std::string& sql,
-                                             ISqlLookupFormatter& formatter,
-                                             const DatabaseConstraints& lookup,
-                                             ResourceType queryLevel,
-                                             const std::set<std::string>& labels,
-                                             LabelsConstraint labelsConstraint,
-                                             size_t limit
-                                             )
-  {
-    ResourceType lowerLevel, upperLevel;
-    GetLookupLevels(lowerLevel, upperLevel, queryLevel, lookup);
-    
-    assert(upperLevel == queryLevel &&
-           queryLevel == lowerLevel);
-
-    const bool escapeBrackets = formatter.IsEscapeBrackets();
-    
-    std::vector<std::string> mainDicomTagsComparisons, dicomIdentifiersComparisons;
-
-    for (size_t i = 0; i < lookup.GetSize(); i++)
-    {
-      const DatabaseConstraint& constraint = lookup.GetConstraint(i);
-
-      std::string comparison;
-      
-      if (FormatComparison2(comparison, formatter, constraint, escapeBrackets))
-      {
-        if (!comparison.empty())
-        {
-          if (constraint.IsIdentifier())
-          {
-            dicomIdentifiersComparisons.push_back(comparison);
-          }
-          else
-          {
-            mainDicomTagsComparisons.push_back(comparison);
-          }
-        }
-      }
-    }
-
-    sql = ("SELECT publicId, internalId "
-           "FROM Resources "
-           "WHERE resourceType = " + formatter.FormatResourceType(queryLevel) 
-            + " ");
-
-    if (dicomIdentifiersComparisons.size() > 0)
-    {
-      for (std::vector<std::string>::const_iterator it = dicomIdentifiersComparisons.begin(); it < dicomIdentifiersComparisons.end(); ++it)
-      {
-        sql += (" AND internalId IN (SELECT id FROM DicomIdentifiers WHERE " + *it + ") ");
-      }
-    }
-
-    if (mainDicomTagsComparisons.size() > 0)
-    {
-      for (std::vector<std::string>::const_iterator it = mainDicomTagsComparisons.begin(); it < mainDicomTagsComparisons.end(); ++it)
-      {
-        sql += (" AND internalId IN (SELECT id FROM MainDicomTags WHERE " + *it + ") ");
-      }
-    }
-
-    if (!labels.empty())
-    {
-      /**
-       * "In SQL Server, NOT EXISTS and NOT IN predicates are the best
-       * way to search for missing values, as long as both columns in
-       * question are NOT NULL."
-       * https://explainextended.com/2009/09/15/not-in-vs-not-exists-vs-left-join-is-null-sql-server/
-       **/
-
-      std::list<std::string> formattedLabels;
-      for (std::set<std::string>::const_iterator it = labels.begin(); it != labels.end(); ++it)
-      {
-        formattedLabels.push_back(formatter.GenerateParameter(*it));
-      }
-
-      std::string condition;
-      std::string inOrNotIn;
-      switch (labelsConstraint)
-      {
-        case LabelsConstraint_Any:
-          condition = "> 0";
-          inOrNotIn = "IN";
-          break;
-          
-        case LabelsConstraint_All:
-          condition = "= " + boost::lexical_cast<std::string>(labels.size());
-          inOrNotIn = "IN";
-          break;
-          
-        case LabelsConstraint_None:
-          condition = "> 0";
-          inOrNotIn = "NOT IN";
-          break;
-          
-        default:
-          throw OrthancException(ErrorCode_ParameterOutOfRange);
-      }
-      
-      sql += (" AND internalId " + inOrNotIn + " (SELECT id"
-                                 " FROM (SELECT id, COUNT(1) AS labelsCount "
-                                        "FROM Labels "
-                                        "WHERE label IN (" + Join(formattedLabels, "", ", ") + ") GROUP BY id"
-                                        ") AS temp "
-                                 " WHERE labelsCount " + condition + ")");
-    }
-
-    if (limit != 0)
-    {
-      sql += " LIMIT " + boost::lexical_cast<std::string>(limit);
-    }
-  }
-
-}
--- a/Resources/Orthanc/Databases/ISqlLookupFormatter.h	Mon Sep 09 12:48:52 2024 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,97 +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-2023 Osimis S.A., Belgium
- * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, 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.
- * 
- * 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
-
-#if ORTHANC_BUILDING_SERVER_LIBRARY == 1
-#  include "../../../OrthancFramework/Sources/Enumerations.h"
-#else
-#  include <Enumerations.h>
-#endif
-
-#include <boost/noncopyable.hpp>
-#include <vector>
-
-namespace Orthanc
-{
-  class DatabaseConstraints;
-  class FindRequest;
-
-  enum LabelsConstraint
-  {
-    LabelsConstraint_All,
-    LabelsConstraint_Any,
-    LabelsConstraint_None
-  };
-
-  // 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;
-
-    virtual std::string FormatLimits(uint64_t since, uint64_t count) = 0;
-
-    /**
-     * Whether to escape '[' and ']', which is only needed for
-     * MSSQL. New in Orthanc 1.10.0, from the following changeset:
-     * https://orthanc.uclouvain.be/hg/orthanc-databases/rev/389c037387ea
-     **/
-    virtual bool IsEscapeBrackets() const = 0;
-
-    static void GetLookupLevels(ResourceType& lowerLevel,
-                                ResourceType& upperLevel,
-                                const ResourceType& queryLevel,
-                                const DatabaseConstraints& lookup);
-
-    static void Apply(std::string& sql,
-                      ISqlLookupFormatter& formatter,
-                      const DatabaseConstraints& lookup,
-                      ResourceType queryLevel,
-                      const std::set<std::string>& labels,  // New in Orthanc 1.12.0
-                      LabelsConstraint labelsConstraint,    // New in Orthanc 1.12.0
-                      size_t limit);
-
-    static void ApplySingleLevel(std::string& sql,
-                                 ISqlLookupFormatter& formatter,
-                                 const DatabaseConstraints& lookup,
-                                 ResourceType queryLevel,
-                                 const std::set<std::string>& labels,  // New in Orthanc 1.12.0
-                                 LabelsConstraint labelsConstraint,    // New in Orthanc 1.12.0
-                                 size_t limit);
-
-#if ORTHANC_BUILDING_SERVER_LIBRARY == 1
-    static void Apply(std::string& sql,
-                      ISqlLookupFormatter& formatter,
-                      const FindRequest& request);
-#endif
-  };
-}
--- a/Resources/SyncOrthancFolder.py	Mon Sep 09 12:48:52 2024 +0200
+++ b/Resources/SyncOrthancFolder.py	Mon Sep 09 15:04:48 2024 +0200
@@ -40,10 +40,6 @@
     ('default', 'OrthancServer/Plugins/Samples/Common/OrthancPluginException.h', 'Plugins'),
     ('default', 'OrthancServer/Plugins/Samples/Common/OrthancPluginsExports.cmake', 'Plugins'),
     ('default', 'OrthancServer/Plugins/Samples/Common/VersionScriptPlugins.map', 'Plugins'),
-    ('find-refactoring', 'OrthancServer/Sources/Search/DatabaseConstraint.cpp', 'Databases'),
-    ('find-refactoring', 'OrthancServer/Sources/Search/DatabaseConstraint.h', 'Databases'),
-    ('find-refactoring', 'OrthancServer/Sources/Search/ISqlLookupFormatter.cpp', 'Databases'),
-    ('find-refactoring', 'OrthancServer/Sources/Search/ISqlLookupFormatter.h', 'Databases'),
 ]
 
 SDK = [