changeset 5248:a7d95f951f8a db-protobuf

replaced "WithLabels" and "WithoutLabels", by "Labels" and "LabelsConstraint"
author Sebastien Jodogne <s.jodogne@gmail.com>
date Fri, 07 Apr 2023 22:18:37 +0200
parents eb2684260c19
children f22c8fac764b
files NEWS OrthancServer/OrthancExplorer/explorer.js OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto OrthancServer/Sources/Database/IDatabaseWrapper.h OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp OrthancServer/Sources/Database/StatelessDatabaseOperations.h OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp OrthancServer/Sources/Search/DatabaseLookup.cpp OrthancServer/Sources/Search/DatabaseLookup.h OrthancServer/Sources/Search/ISqlLookupFormatter.cpp OrthancServer/Sources/Search/ISqlLookupFormatter.h OrthancServer/Sources/ServerContext.cpp OrthancServer/UnitTestsSources/ServerIndexTests.cpp
diffstat 17 files changed, 152 insertions(+), 125 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Fri Apr 07 15:44:12 2023 +0200
+++ b/NEWS	Fri Apr 07 22:18:37 2023 +0200
@@ -11,7 +11,7 @@
 
 * API version upgraded to 20
 * New URIs "/.../{id}/labels/{label}" to test/set/remove labels
-* "/tools/find" accepts the "WithLabels" and "WithoutLabels" arguments
+* "/tools/find" accepts the "Labels" and "LabelsConstraint" arguments
 * "/patients/{id}", "/studies/{id}", "/series/{id}" and "/instances/{id}"
   contain the "Labels" field
 * "/system": added "UserMetadata" and "HasLabels"
--- a/OrthancServer/OrthancExplorer/explorer.js	Fri Apr 07 15:44:12 2023 +0200
+++ b/OrthancServer/OrthancExplorer/explorer.js	Fri Apr 07 22:18:37 2023 +0200
@@ -566,7 +566,7 @@
       }
       else if (input.id == 'lookup-study-labels') {
         // New in Orthanc 1.12.0
-        lookup['WithLabels'] = input.value.split(' ');
+        lookup['Labels'] = input.value.split(' ');
       }
       else {
         console.error('Unknown lookup field: ' + input.id);
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp	Fri Apr 07 15:44:12 2023 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp	Fri Apr 07 22:18:37 2023 +0200
@@ -565,12 +565,11 @@
                                       std::list<std::string>* instancesId,
                                       const std::vector<DatabaseConstraint>& lookup,
                                       ResourceType queryLevel,
-                                      const std::set<std::string>& withLabels,
-                                      const std::set<std::string>& withoutLabels,
+                                      const std::set<std::string>& labels,
+                                      LabelsConstraint labelsConstraint,
                                       uint32_t limit) ORTHANC_OVERRIDE
     {
-      if (!withLabels.empty() ||
-          !withoutLabels.empty())
+      if (!labels.empty())
       {
         throw OrthancException(ErrorCode_InternalError);  // "HasLabelsSupport()" has returned "false"
       }
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp	Fri Apr 07 15:44:12 2023 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp	Fri Apr 07 22:18:37 2023 +0200
@@ -800,12 +800,11 @@
                                       std::list<std::string>* instancesId, // Can be NULL if not needed
                                       const std::vector<DatabaseConstraint>& lookup,
                                       ResourceType queryLevel,
-                                      const std::set<std::string>& withLabels,
-                                      const std::set<std::string>& withoutLabels,
+                                      const std::set<std::string>& labels,
+                                      LabelsConstraint labelsConstraint,
                                       uint32_t limit) ORTHANC_OVERRIDE
     {
-      if (!withLabels.empty() ||
-          !withoutLabels.empty())
+      if (!labels.empty())
       {
         throw OrthancException(ErrorCode_InternalError);  // "HasLabelsSupport()" has returned "false"
       }
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp	Fri Apr 07 15:44:12 2023 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp	Fri Apr 07 22:18:37 2023 +0200
@@ -915,13 +915,12 @@
                                       std::list<std::string>* instancesId, // Can be NULL if not needed
                                       const std::vector<DatabaseConstraint>& lookup,
                                       ResourceType queryLevel,
-                                      const std::set<std::string>& withLabels,
-                                      const std::set<std::string>& withoutLabels,
+                                      const std::set<std::string>& labels,
+                                      LabelsConstraint labelsConstraint,
                                       uint32_t limit) ORTHANC_OVERRIDE
     {
       if (!database_.HasLabelsSupport() &&
-          (!withLabels.empty() ||
-           !withoutLabels.empty()))
+          !labels.empty())
       {
         throw OrthancException(ErrorCode_InternalError);
       }
@@ -976,14 +975,27 @@
         }
       }
 
-      for (std::set<std::string>::const_iterator it = withLabels.begin(); it != withLabels.end(); ++it)
+      for (std::set<std::string>::const_iterator it = labels.begin(); it != labels.end(); ++it)
       {
-        request.mutable_lookup_resources()->add_with_labels(*it);
+        request.mutable_lookup_resources()->add_labels(*it);
       }
-      
-      for (std::set<std::string>::const_iterator it = withoutLabels.begin(); it != withoutLabels.end(); ++it)
+
+      switch (labelsConstraint)
       {
-        request.mutable_lookup_resources()->add_without_labels(*it);
+        case LabelsConstraint_All:
+          request.mutable_lookup_resources()->set_labels_constraint(DatabasePluginMessages::LABELS_CONSTRAINT_ALL);
+          break;
+            
+        case LabelsConstraint_Any:
+          request.mutable_lookup_resources()->set_labels_constraint(DatabasePluginMessages::LABELS_CONSTRAINT_ANY);
+          break;
+            
+        case LabelsConstraint_None:
+          request.mutable_lookup_resources()->set_labels_constraint(DatabasePluginMessages::LABELS_CONSTRAINT_NONE);
+          break;
+            
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
       }
       
       DatabasePluginMessages::TransactionResponse response;
--- a/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto	Fri Apr 07 15:44:12 2023 +0200
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto	Fri Apr 07 22:18:37 2023 +0200
@@ -71,6 +71,12 @@
   CONSTRAINT_LIST = 4;
 }
 
+enum LabelsConstraintType {
+  LABELS_CONSTRAINT_ALL = 0;
+  LABELS_CONSTRAINT_ANY = 1;
+  LABELS_CONSTRAINT_NONE = 2;
+}
+
 message ServerIndexChange {
   int64         seq = 1;
   int32         change_type = 2;   // opaque "ChangeType" in Orthanc
@@ -658,8 +664,8 @@
     ResourceType query_level = 2;
     uint32 limit = 3;
     bool retrieve_instances_ids = 4;
-    repeated string with_labels = 5;     // New in Orthanc 1.12.0
-    repeated string without_labels = 6;  // New in Orthanc 1.12.0
+    repeated string labels = 5;                  // New in Orthanc 1.12.0
+    LabelsConstraintType labels_constraint = 6;  // New in Orthanc 1.12.0
   }
   message Response {
     repeated string resources_ids = 1;
--- a/OrthancServer/Sources/Database/IDatabaseWrapper.h	Fri Apr 07 15:44:12 2023 +0200
+++ b/OrthancServer/Sources/Database/IDatabaseWrapper.h	Fri Apr 07 22:18:37 2023 +0200
@@ -26,6 +26,7 @@
 #include "../../../OrthancFramework/Sources/FileStorage/FileInfo.h"
 #include "../../../OrthancFramework/Sources/FileStorage/IStorageArea.h"
 #include "../ExportedResource.h"
+#include "../Search/ISqlLookupFormatter.h"
 #include "../ServerIndexChange.h"
 #include "IDatabaseListener.h"
 
@@ -200,8 +201,8 @@
                                         std::list<std::string>* instancesId, // Can be NULL if not needed
                                         const std::vector<DatabaseConstraint>& lookup,
                                         ResourceType queryLevel,
-                                        const std::set<std::string>& withLabels,
-                                        const std::set<std::string>& withoutLabels,
+                                        const std::set<std::string>& labels,
+                                        LabelsConstraint labelsConstraint,
                                         uint32_t limit) = 0;
 
       // Returns "true" iff. the instance is new and has been inserted
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp	Fri Apr 07 15:44:12 2023 +0200
+++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp	Fri Apr 07 22:18:37 2023 +0200
@@ -341,14 +341,14 @@
                                       std::list<std::string>* instancesId,
                                       const std::vector<DatabaseConstraint>& lookup,
                                       ResourceType queryLevel,
-                                      const std::set<std::string>& withLabels,
-                                      const std::set<std::string>& withoutLabels,
+                                      const std::set<std::string>& labels,
+                                      LabelsConstraint labelsConstraint,
                                       uint32_t limit) ORTHANC_OVERRIDE
     {
       LookupFormatter formatter;
 
       std::string sql;
-      LookupFormatter::Apply(sql, formatter, lookup, queryLevel, withLabels, withoutLabels, limit);
+      LookupFormatter::Apply(sql, formatter, lookup, queryLevel, labels, labelsConstraint, limit);
 
       sql = "CREATE TEMPORARY TABLE Lookup AS " + sql;
     
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Fri Apr 07 15:44:12 2023 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Fri Apr 07 22:18:37 2023 +0200
@@ -1651,9 +1651,8 @@
       {
         // TODO - CANDIDATE FOR "TransactionType_Implicit"
         std::list<std::string> tmp;
-        std::set<std::string> withLabels;
-        std::set<std::string> withoutLabels;
-        transaction.ApplyLookupResources(tmp, NULL, query_, level_, withLabels, withoutLabels, 0);
+        std::set<std::string> labels;
+        transaction.ApplyLookupResources(tmp, NULL, query_, level_, labels, LabelsConstraint_Any, 0);
         CopyListToVector(result_, tmp);
       }
     };
@@ -1918,25 +1917,16 @@
   }
 
 
-  static void CheckValidLabels(const std::set<std::string>& labels)
-  {
-    for (std::set<std::string>::const_iterator it = labels.begin(); it != labels.end(); ++it)
-    {
-      ServerToolbox::CheckValidLabel(*it);
-    }
-  }
-  
-
   void StatelessDatabaseOperations::ApplyLookupResources(std::vector<std::string>& resourcesId,
                                                          std::vector<std::string>* instancesId,
                                                          const DatabaseLookup& lookup,
                                                          ResourceType queryLevel,
-                                                         const std::set<std::string>& withLabels,
-                                                         const std::set<std::string>& withoutLabels,
+                                                         const std::set<std::string>& labels,
+                                                         LabelsConstraint labelsConstraint,
                                                          uint32_t limit)
   {
     class Operations : public ReadOnlyOperationsT6<bool, const std::vector<DatabaseConstraint>&, ResourceType,
-                                                   const std::set<std::string>&, const std::set<std::string>&, size_t>
+                                                   const std::set<std::string>&, LabelsConstraint, size_t>
     {
     private:
       std::list<std::string>  resourcesList_;
@@ -1970,20 +1960,22 @@
       }
     };
 
-    if ((!withLabels.empty() || !withoutLabels.empty()) &&
+    if (!labels.empty() &&
         !db_.HasLabelsSupport())
     {
       throw OrthancException(ErrorCode_NotImplemented, "The database backend doesn't support labels");
     }
 
-    CheckValidLabels(withLabels);
-    CheckValidLabels(withoutLabels);
+    for (std::set<std::string>::const_iterator it = labels.begin(); it != labels.end(); ++it)
+    {
+      ServerToolbox::CheckValidLabel(*it);
+    }
 
     std::vector<DatabaseConstraint> normalized;
     NormalizeLookup(normalized, lookup, queryLevel);
 
     Operations operations;
-    operations.Apply(*this, (instancesId != NULL), normalized, queryLevel, withLabels, withoutLabels, limit);
+    operations.Apply(*this, (instancesId != NULL), normalized, queryLevel, labels, labelsConstraint, limit);
     
     CopyListToVector(resourcesId, operations.GetResourcesList());
 
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Fri Apr 07 15:44:12 2023 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Fri Apr 07 22:18:37 2023 +0200
@@ -207,11 +207,12 @@
                                 std::list<std::string>* instancesId, // Can be NULL if not needed
                                 const std::vector<DatabaseConstraint>& lookup,
                                 ResourceType queryLevel,
-                                const std::set<std::string>& withLabels,
-                                const std::set<std::string>& withoutLabels,
+                                const std::set<std::string>& labels,  // New in Orthanc 1.12.0
+                                LabelsConstraint labelsConstraint,    // New in Orthanc 1.12.0
                                 uint32_t limit)
       {
-        return transaction_.ApplyLookupResources(resourcesId, instancesId, lookup, queryLevel, withLabels, withoutLabels, limit);
+        return transaction_.ApplyLookupResources(resourcesId, instancesId, lookup, queryLevel,
+                                                 labels, labelsConstraint, limit);
       }
 
       void GetAllMetadata(std::map<MetadataType, std::string>& target,
@@ -676,8 +677,8 @@
                               std::vector<std::string>* instancesId,  // Can be NULL if not needed
                               const DatabaseLookup& lookup,
                               ResourceType queryLevel,
-                              const std::set<std::string>& withLabels,
-                              const std::set<std::string>& withoutLabels,
+                              const std::set<std::string>& labels,
+                              LabelsConstraint labelsConstraint,
                               uint32_t limit);
 
     bool DeleteResource(Json::Value& remainingAncestor /* out */,
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Fri Apr 07 15:44:12 2023 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Fri Apr 07 22:18:37 2023 +0200
@@ -3072,8 +3072,8 @@
     static const char* const KEY_QUERY = "Query";
     static const char* const KEY_REQUESTED_TAGS = "RequestedTags";
     static const char* const KEY_SINCE = "Since";
-    static const char* const KEY_WITH_LABELS = "WithLabels";        // New in Orthanc 1.12.0
-    static const char* const KEY_WITHOUT_LABELS = "WithoutLabels";  // New in Orthanc 1.12.0
+    static const char* const KEY_LABELS = "Labels";                       // New in Orthanc 1.12.0
+    static const char* const KEY_LABELS_CONSTRAINT = "LabelsConstraint";  // New in Orthanc 1.12.0
 
     if (call.IsDocumentation())
     {
@@ -3103,10 +3103,10 @@
                          "all Main Dicom Tags to keep backward compatibility with Orthanc prior to 1.11.0.", false)
         .SetRequestField(KEY_QUERY, RestApiCallDocumentation::Type_JsonObject,
                          "Associative array containing the filter on the values of the DICOM tags", true)
-        .SetRequestField(KEY_WITH_LABELS, RestApiCallDocumentation::Type_JsonListOfStrings,
-                         "List of strings specifying which labels must be present in the resources (new in Orthanc 1.12.0)", true)
-        .SetRequestField(KEY_WITHOUT_LABELS, RestApiCallDocumentation::Type_JsonListOfStrings,
-                         "List of strings specifying which labels must not be present in the resources (new in Orthanc 1.12.0)", true)
+        .SetRequestField(KEY_LABELS, RestApiCallDocumentation::Type_JsonListOfStrings,
+                         "List of strings specifying which labels to look for in the resources (new in Orthanc 1.12.0)", true)
+        .SetRequestField(KEY_LABELS_CONSTRAINT, RestApiCallDocumentation::Type_String,
+                         "Constraint on the labels, can be `All`, `Any`, or `None` (defaults to `All`, new in Orthanc 1.12.0)", true)
         .AddAnswerType(MimeType_Json, "JSON array containing either the Orthanc identifiers, or detailed information "
                        "about the reported resources (if `Expand` argument is `true`)");
       return;
@@ -3157,17 +3157,17 @@
       throw OrthancException(ErrorCode_BadRequest, 
                              "Field \"" + std::string(KEY_REQUESTED_TAGS) + "\" must be an array");
     }
-    else if (request.isMember(KEY_WITH_LABELS) &&
-             request[KEY_WITH_LABELS].type() != Json::arrayValue)
+    else if (request.isMember(KEY_LABELS) &&
+             request[KEY_LABELS].type() != Json::arrayValue)
     {
       throw OrthancException(ErrorCode_BadRequest, 
-                             "Field \"" + std::string(KEY_WITH_LABELS) + "\" must be an array of strings");
+                             "Field \"" + std::string(KEY_LABELS) + "\" must be an array of strings");
     }
-    else if (request.isMember(KEY_WITHOUT_LABELS) &&
-             request[KEY_WITHOUT_LABELS].type() != Json::arrayValue)
+    else if (request.isMember(KEY_LABELS_CONSTRAINT) &&
+             request[KEY_LABELS_CONSTRAINT].type() != Json::stringValue)
     {
       throw OrthancException(ErrorCode_BadRequest, 
-                             "Field \"" + std::string(KEY_WITHOUT_LABELS) + "\" must be an array of strings");
+                             "Field \"" + std::string(KEY_LABELS_CONSTRAINT) + "\" must be an array of strings");
     }
     else
     {
@@ -3241,33 +3241,41 @@
         }
       }
 
-      if (request.isMember(KEY_WITH_LABELS))  // New in Orthanc 1.12.0
+      if (request.isMember(KEY_LABELS))  // New in Orthanc 1.12.0
       {
-        for (Json::Value::ArrayIndex i = 0; i < request[KEY_WITH_LABELS].size(); i++)
+        for (Json::Value::ArrayIndex i = 0; i < request[KEY_LABELS].size(); i++)
         {
-          if (request[KEY_WITH_LABELS][i].type() != Json::stringValue)
+          if (request[KEY_LABELS][i].type() != Json::stringValue)
           {
-            throw OrthancException(ErrorCode_BadRequest, "Field \""+ std::string(KEY_WITH_LABELS) + "\" must contain strings");
+            throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_LABELS) + "\" must contain strings");
           }
           else
           {
-            query.AddWithLabel(request[KEY_WITH_LABELS][i].asString());
+            query.AddLabel(request[KEY_LABELS][i].asString());
           }
         }
       }
+
+      query.SetLabelsConstraint(LabelsConstraint_All);
       
-      if (request.isMember(KEY_WITHOUT_LABELS))  // New in Orthanc 1.12.0
+      if (request.isMember(KEY_LABELS_CONSTRAINT))
       {
-        for (Json::Value::ArrayIndex i = 0; i < request[KEY_WITHOUT_LABELS].size(); i++)
+        const std::string& s = request[KEY_LABELS_CONSTRAINT].asString();
+        if (s == "All")
         {
-          if (request[KEY_WITHOUT_LABELS][i].type() != Json::stringValue)
-          {
-            throw OrthancException(ErrorCode_BadRequest, "Field \""+ std::string(KEY_WITHOUT_LABELS) + "\" must contain strings");
-          }
-          else
-          {
-            query.AddWithoutLabel(request[KEY_WITHOUT_LABELS][i].asString());
-          }
+          query.SetLabelsConstraint(LabelsConstraint_All);
+        }
+        else if (s == "Any")
+        {
+          query.SetLabelsConstraint(LabelsConstraint_Any);
+        }
+        else if (s == "None")
+        {
+          query.SetLabelsConstraint(LabelsConstraint_None);
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_LABELS_CONSTRAINT) + "\" must be \"All\", \"Any\", or \"None\"");
         }
       }
       
--- a/OrthancServer/Sources/Search/DatabaseLookup.cpp	Fri Apr 07 15:44:12 2023 +0200
+++ b/OrthancServer/Sources/Search/DatabaseLookup.cpp	Fri Apr 07 22:18:37 2023 +0200
@@ -370,22 +370,12 @@
   }
 
 
-  void DatabaseLookup::AddWithLabel(const std::string& label)
+  void DatabaseLookup::AddLabel(const std::string& label)
   {
     if (!label.empty())
     {
       ServerToolbox::CheckValidLabel(label);
-      withLabels_.insert(label);
-    }
-  }
-  
-
-  void DatabaseLookup::AddWithoutLabel(const std::string& label)
-  {
-    if (!label.empty())
-    {
-      ServerToolbox::CheckValidLabel(label);
-      withoutLabels_.insert(label);
+      labels_.insert(label);
     }
   }
 }
--- a/OrthancServer/Sources/Search/DatabaseLookup.h	Fri Apr 07 15:44:12 2023 +0200
+++ b/OrthancServer/Sources/Search/DatabaseLookup.h	Fri Apr 07 22:18:37 2023 +0200
@@ -22,6 +22,7 @@
 
 #pragma once
 
+#include "../Search/ISqlLookupFormatter.h"
 #include "DicomTagConstraint.h"
 
 class DcmItem;
@@ -32,8 +33,8 @@
   {
   private:
     std::vector<DicomTagConstraint*>  constraints_;
-    std::set<std::string>             withLabels_;
-    std::set<std::string>             withoutLabels_;
+    std::set<std::string>             labels_;
+    LabelsConstraint                  labelsConstraint_;
 
     void AddDicomConstraintInternal(const DicomTag& tag,
                                     ValueRepresentation vr,
@@ -44,7 +45,8 @@
     void AddConstraintInternal(DicomTagConstraint* constraint);  // Takes ownership
 
   public:
-    DatabaseLookup()
+    DatabaseLookup() :
+      labelsConstraint_(LabelsConstraint_All)
     {
     }
 
@@ -95,18 +97,21 @@
 
     void RemoveConstraint(const DicomTag& tag);
 
-    void AddWithLabel(const std::string& label);
-
-    void AddWithoutLabel(const std::string& label);
+    void AddLabel(const std::string& label);
 
-    const std::set<std::string>& GetWithLabels() const
+    void SetLabelsConstraint(LabelsConstraint constraint)
     {
-      return withLabels_;
+      labelsConstraint_ = constraint;
     }
 
-    const std::set<std::string>& GetWithoutLabels() const
+    const std::set<std::string>& GetLabels() const
     {
-      return withoutLabels_;
+      return labels_;
+    }
+
+    LabelsConstraint GetLabelsConstraint() const
+    {
+      return labelsConstraint_;
     }
   };
 }
--- a/OrthancServer/Sources/Search/ISqlLookupFormatter.cpp	Fri Apr 07 15:44:12 2023 +0200
+++ b/OrthancServer/Sources/Search/ISqlLookupFormatter.cpp	Fri Apr 07 22:18:37 2023 +0200
@@ -307,8 +307,8 @@
                                   ISqlLookupFormatter& formatter,
                                   const std::vector<DatabaseConstraint>& lookup,
                                   ResourceType queryLevel,
-                                  const std::set<std::string>& withLabels,
-                                  const std::set<std::string>& withoutLabels,
+                                  const std::set<std::string>& labels,
+                                  LabelsConstraint labelsConstraint,
                                   size_t limit)
   {
     assert(ResourceType_Patient < ResourceType_Study &&
@@ -384,20 +384,7 @@
 
     std::list<std::string> where;
 
-    if (!withLabels.empty())
-    {
-      std::list<std::string> labels;
-      for (std::set<std::string>::const_iterator it = withLabels.begin(); it != withLabels.end(); ++it)
-      {
-        labels.push_back(formatter.GenerateParameter(*it));
-      }
-
-      where.push_back(boost::lexical_cast<std::string>(withLabels.size()) +
-                      " = (SELECT COUNT(1) FROM Labels WHERE internalId = " + FormatLevel(queryLevel) +
-                      ".internalId AND label IN (" + Join(labels, "", ", ") + "))");
-    }
-    
-    if (!withoutLabels.empty())
+    if (!labels.empty())
     {
       /**
        * "In SQL Server, NOT EXISTS and NOT IN predicates are the best
@@ -405,14 +392,34 @@
        * 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> labels;
-      for (std::set<std::string>::const_iterator it = withoutLabels.begin(); it != withoutLabels.end(); ++it)
+
+      std::list<std::string> formattedLabels;
+      for (std::set<std::string>::const_iterator it = labels.begin(); it != labels.end(); ++it)
       {
-        labels.push_back(formatter.GenerateParameter(*it));
+        formattedLabels.push_back(formatter.GenerateParameter(*it));
       }
 
-      where.push_back("NOT EXISTS (SELECT 1 FROM Labels WHERE internalId = " + FormatLevel(queryLevel) +
-                      ".internalId AND label IN (" + Join(labels, "", ", ") + "))");
+      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 WHERE internalId = " + FormatLevel(queryLevel) +
+                      ".internalId AND label IN (" + Join(formattedLabels, "", ", ") + ")) " + condition);
     }
     
     where.push_back(FormatLevel(queryLevel) + ".resourceType = " +
--- a/OrthancServer/Sources/Search/ISqlLookupFormatter.h	Fri Apr 07 15:44:12 2023 +0200
+++ b/OrthancServer/Sources/Search/ISqlLookupFormatter.h	Fri Apr 07 22:18:37 2023 +0200
@@ -35,6 +35,13 @@
 {
   class DatabaseConstraint;
   
+  enum LabelsConstraint
+  {
+    LabelsConstraint_All,
+    LabelsConstraint_Any,
+    LabelsConstraint_None
+  };
+
   // This class is also used by the "orthanc-databases" project
   class ISqlLookupFormatter : public boost::noncopyable
   {
@@ -60,8 +67,8 @@
                       ISqlLookupFormatter& formatter,
                       const std::vector<DatabaseConstraint>& lookup,
                       ResourceType queryLevel,
-                      const std::set<std::string>& withLabels,     // New in Orthanc 1.12.0
-                      const std::set<std::string>& withoutLabels,  // New in Orthanc 1.12.0
+                      const std::set<std::string>& labels,  // New in Orthanc 1.12.0
+                      LabelsConstraint labelsConstraint,    // New in Orthanc 1.12.0
                       size_t limit);
   };
 }
--- a/OrthancServer/Sources/ServerContext.cpp	Fri Apr 07 15:44:12 2023 +0200
+++ b/OrthancServer/Sources/ServerContext.cpp	Fri Apr 07 22:18:37 2023 +0200
@@ -1450,7 +1450,7 @@
     {
       const size_t lookupLimit = (databaseLimit == 0 ? 0 : databaseLimit + 1);
       GetIndex().ApplyLookupResources(resources, &instances, *fastLookup, queryLevel,
-                                      lookup.GetWithLabels(), lookup.GetWithoutLabels(), lookupLimit);
+                                      lookup.GetLabels(), lookup.GetLabelsConstraint(), lookupLimit);
     }
 
     bool complete = (databaseLimit == 0 ||
--- a/OrthancServer/UnitTestsSources/ServerIndexTests.cpp	Fri Apr 07 15:44:12 2023 +0200
+++ b/OrthancServer/UnitTestsSources/ServerIndexTests.cpp	Fri Apr 07 22:18:37 2023 +0200
@@ -169,7 +169,7 @@
       lookup.push_back(c.ConvertToDatabaseConstraint(level, DicomTagType_Identifier));
 
       std::set<std::string> noLabel;
-      transaction_->ApplyLookupResources(result, NULL, lookup, level, noLabel, noLabel, 0 /* no limit */);
+      transaction_->ApplyLookupResources(result, NULL, lookup, level, noLabel, LabelsConstraint_All, 0 /* no limit */);
     }    
 
     void DoLookupIdentifier2(std::list<std::string>& result,
@@ -190,7 +190,7 @@
       lookup.push_back(c2.ConvertToDatabaseConstraint(level, DicomTagType_Identifier));
       
       std::set<std::string> noLabel;
-      transaction_->ApplyLookupResources(result, NULL, lookup, level, noLabel, noLabel, 0 /* no limit */);
+      transaction_->ApplyLookupResources(result, NULL, lookup, level, noLabel, LabelsConstraint_All, 0 /* no limit */);
     }
   };
 }