changeset 5843:7df3d533c294 find-refactoring-clean

integration find-refactoring->find-refactoring-clean
author Sebastien Jodogne <s.jodogne@gmail.com>
date Tue, 29 Oct 2024 13:11:40 +0000
parents 58c549b881ae (current diff) 08e47734328e (diff)
children f924d9a88cd2 0b56ea2fcdfc
files OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp OrthancServer/Sources/Database/StatelessDatabaseOperations.h OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp
diffstat 14 files changed, 531 insertions(+), 292 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Wed Oct 09 11:01:11 2024 +0200
+++ b/NEWS	Tue Oct 29 13:11:40 2024 +0000
@@ -38,9 +38,13 @@
   - 'OrderBy' to order by DICOM Tag or metadata value
   - 'ParentPatient', 'ParentStudy', 'ParentSeries' to retrieve only descendants of an
     Orthanc resource.
-  - 'QueryMetadata' to filter results based on metadata values.
+  - 'MetadataQuery' to filter results based on metadata values.
   - 'ResponseContent' to define what shall be included in the response for each returned
     resource (e.g: Metadata, Children, ...)
+* With DB backend with "HasExtendedFind" support, a new /tools/count-resources API route
+  is similar to tools/find but only returns the number of resources matching the criteria.
+* With DB backend with "HasExtendedFind" support, usage of 'Limit' and 'Since in /tools/find
+  is not allowed if your query includes filtering on DICOM tags that are not stored in DB.
 
 
 Maintenance
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp	Wed Oct 09 11:01:11 2024 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp	Tue Oct 29 13:11:40 2024 +0000
@@ -1475,6 +1475,63 @@
     }
 
 
+    virtual void ExecuteCount(uint64_t& count,
+                              const FindRequest& request,
+                              const Capabilities& capabilities) ORTHANC_OVERRIDE
+    {
+      if (capabilities.HasFindSupport())
+      {
+        DatabasePluginMessages::TransactionRequest dbRequest;
+        dbRequest.mutable_find()->set_level(Convert(request.GetLevel()));
+
+        if (request.GetOrthancIdentifiers().HasPatientId())
+        {
+          dbRequest.mutable_find()->set_orthanc_id_patient(request.GetOrthancIdentifiers().GetPatientId());
+        }
+
+        if (request.GetOrthancIdentifiers().HasStudyId())
+        {
+          dbRequest.mutable_find()->set_orthanc_id_study(request.GetOrthancIdentifiers().GetStudyId());
+        }
+
+        if (request.GetOrthancIdentifiers().HasSeriesId())
+        {
+          dbRequest.mutable_find()->set_orthanc_id_series(request.GetOrthancIdentifiers().GetSeriesId());
+        }
+
+        if (request.GetOrthancIdentifiers().HasInstanceId())
+        {
+          dbRequest.mutable_find()->set_orthanc_id_instance(request.GetOrthancIdentifiers().GetInstanceId());
+        }
+
+        for (size_t i = 0; i < request.GetDicomTagConstraints().GetSize(); i++)
+        {
+          Convert(*dbRequest.mutable_find()->add_dicom_tag_constraints(), request.GetDicomTagConstraints().GetConstraint(i));
+        }
+
+        for (std::deque<DatabaseMetadataConstraint*>::const_iterator it = request.GetMetadataConstraint().begin(); it != request.GetMetadataConstraint().end(); ++it)
+        {
+          Convert(*dbRequest.mutable_find()->add_metadata_constraints(), *(*it)); 
+        }
+
+        for (std::set<std::string>::const_iterator it = request.GetLabels().begin(); it != request.GetLabels().end(); ++it)
+        {
+          dbRequest.mutable_find()->add_labels(*it);
+        }
+
+        dbRequest.mutable_find()->set_labels_constraint(Convert(request.GetLabelsConstraint()));
+
+        DatabasePluginMessages::TransactionResponse dbResponse;
+        ExecuteTransaction(dbResponse, DatabasePluginMessages::OPERATION_COUNT_RESOURCES, dbRequest);
+
+        count = dbResponse.count_resources().count();
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_NotImplemented);
+      }
+    }
+
     virtual void ExecuteFind(FindResponse& response,
                              const FindRequest& request,
                              const Capabilities& capabilities) ORTHANC_OVERRIDE
--- a/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto	Wed Oct 09 11:01:11 2024 +0200
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto	Tue Oct 29 13:11:40 2024 +0000
@@ -314,6 +314,7 @@
   OPERATION_UPDATE_AND_GET_STATISTICS = 49;   // New in Orthanc 1.12.3
   OPERATION_FIND = 50;                        // New in Orthanc 1.12.5
   OPERATION_GET_CHANGES_EXTENDED = 51;        // New in Orthanc 1.12.5
+  OPERATION_COUNT_RESOURCES = 52;             // New in Orthanc 1.12.5
 }
 
 message Rollback {
@@ -963,6 +964,14 @@
   }
 }
 
+message CountResources
+{
+  message Response
+  {
+    uint64 count = 1;
+  }
+}
+
 message TransactionRequest {
   sfixed64              transaction = 1;
   TransactionOperation  operation = 2;
@@ -1019,6 +1028,7 @@
   UpdateAndGetStatistics.Request          update_and_get_statistics = 149;
   Find.Request                            find = 150;
   GetChangesExtended.Request              get_changes_extended = 151;
+  Find.Request                            count_resources = 152;
 }
 
 message TransactionResponse {
@@ -1074,6 +1084,7 @@
   UpdateAndGetStatistics.Response          update_and_get_statistics = 149;
   repeated Find.Response                   find = 150;   // One message per found resources
   GetChangesExtended.Response              get_changes_extended = 151;
+  CountResources.Response                  count_resources = 152;
 }
 
 enum RequestType {
--- a/OrthancServer/Sources/Database/BaseDatabaseWrapper.cpp	Wed Oct 09 11:01:11 2024 +0200
+++ b/OrthancServer/Sources/Database/BaseDatabaseWrapper.cpp	Tue Oct 29 13:11:40 2024 +0000
@@ -58,6 +58,13 @@
 
 
 
+  void BaseDatabaseWrapper::BaseTransaction::ExecuteCount(uint64_t& count,
+                                                          const FindRequest& request,
+                                                          const Capabilities& capabilities)
+  {
+    throw OrthancException(ErrorCode_NotImplemented);  // Not supported
+  }
+
   void BaseDatabaseWrapper::BaseTransaction::ExecuteFind(FindResponse& response,
                                                          const FindRequest& request,
                                                          const Capabilities& capabilities)
--- a/OrthancServer/Sources/Database/BaseDatabaseWrapper.h	Wed Oct 09 11:01:11 2024 +0200
+++ b/OrthancServer/Sources/Database/BaseDatabaseWrapper.h	Tue Oct 29 13:11:40 2024 +0000
@@ -48,6 +48,10 @@
                                           int64_t& compressedSize,
                                           int64_t& uncompressedSize) ORTHANC_OVERRIDE;
 
+      virtual void ExecuteCount(uint64_t& count,
+                                const FindRequest& request,
+                                const Capabilities& capabilities) ORTHANC_OVERRIDE;
+
       virtual void ExecuteFind(FindResponse& response,
                                const FindRequest& request,
                                const Capabilities& capabilities) ORTHANC_OVERRIDE;
--- a/OrthancServer/Sources/Database/Compatibility/GenericFind.cpp	Wed Oct 09 11:01:11 2024 +0200
+++ b/OrthancServer/Sources/Database/Compatibility/GenericFind.cpp	Tue Oct 29 13:11:40 2024 +0000
@@ -123,6 +123,11 @@
         throw OrthancException(ErrorCode_NotImplemented, "The database backend doesn't support labels");
       }
 
+      if (!request.GetOrdering().empty())
+      {
+        throw OrthancException(ErrorCode_NotImplemented, "The database backend doesn't support ordering");
+      }
+
       if (IsRequestWithoutContraint(request) &&
           !request.GetOrthancIdentifiers().HasPatientId() &&
           !request.GetOrthancIdentifiers().HasStudyId() &&
--- a/OrthancServer/Sources/Database/IDatabaseWrapper.h	Wed Oct 09 11:01:11 2024 +0200
+++ b/OrthancServer/Sources/Database/IDatabaseWrapper.h	Tue Oct 29 13:11:40 2024 +0000
@@ -380,10 +380,15 @@
                                           int64_t& uncompressedSize) = 0;
 
       /**
-       * Primitives introduced in Orthanc 1.12.4
+       * Primitives introduced in Orthanc 1.12.5
        **/
 
       // This is only implemented if "HasIntegratedFind()" is "true"
+      virtual void ExecuteCount(uint64_t& count,
+                                const FindRequest& request,
+                                const Capabilities& capabilities) = 0;
+
+      // This is only implemented if "HasIntegratedFind()" is "true"
       virtual void ExecuteFind(FindResponse& response,
                                const FindRequest& request,
                                const Capabilities& capabilities) = 0;
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp	Wed Oct 09 11:01:11 2024 +0200
+++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp	Tue Oct 29 13:11:40 2024 +0000
@@ -502,6 +502,25 @@
 #define STRINGIFY(x) #x
 #define TOSTRING(x) STRINGIFY(x)
 
+    virtual void ExecuteCount(uint64_t& count,
+                              const FindRequest& request,
+                              const Capabilities& capabilities) ORTHANC_OVERRIDE
+    {
+      LookupFormatter formatter;
+      std::string sql;
+
+      std::string lookupSql;
+      LookupFormatter::Apply(lookupSql, formatter, request);
+
+      // base query, retrieve the ordered internalId and publicId of the selected resources
+      sql = "WITH Lookup AS (" + lookupSql + ") SELECT COUNT(*) FROM Lookup";
+      SQLite::Statement s(db_, SQLITE_FROM_HERE_DYNAMIC(sql), sql);
+      formatter.Bind(s);
+
+      s.Step();
+      count = s.ColumnInt64(0);
+    }
+
 
     virtual void ExecuteFind(FindResponse& response,
                              const FindRequest& request,
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Wed Oct 09 11:01:11 2024 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Tue Oct 29 13:11:40 2024 +0000
@@ -586,28 +586,25 @@
                                                    const std::string& publicId,
                                                    ResourceType level)
   {
-    class Operations : public ReadOnlyOperationsT3<std::map<MetadataType, std::string>&, const std::string&, ResourceType>
+    FindRequest request(level);
+    request.SetOrthancId(level, publicId);
+    request.SetRetrieveMetadata(true);
+
+    FindResponse response;
+    ExecuteFind(response, request);
+
+    if (response.GetSize() == 0)
     {
-    public:
-      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
-                              const Tuple& tuple) ORTHANC_OVERRIDE
-      {
-        ResourceType type;
-        int64_t id;
-        if (!transaction.LookupResource(id, type, tuple.get<1>()) ||
-            tuple.get<2>() != type)
-        {
-          throw OrthancException(ErrorCode_UnknownResource);
-        }
-        else
-        {
-          transaction.GetAllMetadata(tuple.get<0>(), id);
-        }
-      }
-    };
-
-    Operations operations;
-    operations.Apply(*this, target, publicId, level);
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+    else if (response.GetSize() == 1)
+    {
+      target = response.GetResourceByIndex(0).GetMetadata(level);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_DatabasePlugin);
+    }
   }
 
 
@@ -650,19 +647,17 @@
   void StatelessDatabaseOperations::GetAllUuids(std::list<std::string>& target,
                                                 ResourceType resourceType)
   {
-    class Operations : public ReadOnlyOperationsT2<std::list<std::string>&, ResourceType>
+    // This method is tested by "orthanc-tests/Plugins/WebDav/Run.py"
+    FindRequest request(resourceType);
+
+    FindResponse response;
+    ExecuteFind(response, request);
+
+    target.clear();
+    for (size_t i = 0; i < response.GetSize(); i++)
     {
-    public:
-      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
-                              const Tuple& tuple) ORTHANC_OVERRIDE
-      {
-        // TODO - CANDIDATE FOR "TransactionType_Implicit"
-        transaction.GetAllPublicIds(tuple.get<0>(), tuple.get<1>());
-      }
-    };
-
-    Operations operations;
-    operations.Apply(*this, target, resourceType);
+      target.push_back(response.GetResourceByIndex(i).GetIdentifier());
+    }
   }
 
 
@@ -3377,6 +3372,33 @@
     return db_.GetDatabaseCapabilities().HasFindSupport();
   }
 
+  void StatelessDatabaseOperations::ExecuteCount(uint64_t& count,
+                                                 const FindRequest& request)
+  {
+    class IntegratedCount : public ReadOnlyOperationsT3<uint64_t&, const FindRequest&,
+                                                       const IDatabaseWrapper::Capabilities&>
+    {
+    public:
+      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
+                              const Tuple& tuple) ORTHANC_OVERRIDE
+      {
+        transaction.ExecuteCount(tuple.get<0>(), tuple.get<1>(), tuple.get<2>());
+      }
+    };
+
+    IDatabaseWrapper::Capabilities capabilities = db_.GetDatabaseCapabilities();
+
+    if (db_.HasIntegratedFind())
+    {
+      IntegratedCount operations;
+      operations.Apply(*this, count, request, capabilities);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
+
   void StatelessDatabaseOperations::ExecuteFind(FindResponse& response,
                                                 const FindRequest& request)
   {
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Wed Oct 09 11:01:11 2024 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Tue Oct 29 13:11:40 2024 +0000
@@ -311,6 +311,13 @@
       bool HasReachedMaxPatientCount(unsigned int maximumPatientCount,
                                      const std::string& patientId);
 
+      void ExecuteCount(uint64_t& count,
+                        const FindRequest& request,
+                        const IDatabaseWrapper::Capabilities& capabilities)
+      {
+        transaction_.ExecuteCount(count, request, capabilities);
+      }
+
       void ExecuteFind(FindResponse& response,
                        const FindRequest& request,
                        const IDatabaseWrapper::Capabilities& capabilities)
@@ -750,5 +757,9 @@
 
     void ExecuteFind(FindResponse& response,
                      const FindRequest& request);
+
+    void ExecuteCount(uint64_t& count,
+                      const FindRequest& request);
+
   };
 }
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Wed Oct 09 11:01:11 2024 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Tue Oct 29 13:11:40 2024 +0000
@@ -3039,6 +3039,13 @@
   }
 
 
+  enum FindType
+  {
+    FindType_Find,
+    FindType_Count
+  };
+
+  template <enum FindType requestType>
   static void Find(RestApiPostCall& call)
   {
     static const char* const KEY_CASE_SENSITIVE = "CaseSensitive";
@@ -3057,57 +3064,73 @@
     static const char* const KEY_PARENT_PATIENT = "ParentPatient";        // New in Orthanc 1.12.5
     static const char* const KEY_PARENT_STUDY = "ParentStudy";            // New in Orthanc 1.12.5
     static const char* const KEY_PARENT_SERIES = "ParentSeries";          // New in Orthanc 1.12.5
-    static const char* const KEY_QUERY_METADATA = "QueryMetadata";        // New in Orthanc 1.12.5
+    static const char* const KEY_METADATA_QUERY = "MetadataQuery";        // New in Orthanc 1.12.5
     static const char* const KEY_RESPONSE_CONTENT = "ResponseContent";    // New in Orthanc 1.12.5
 
     if (call.IsDocumentation())
     {
       OrthancRestApi::DocumentDicomFormat(call, DicomToJsonFormat_Human);
 
-      call.GetDocumentation()
-        .SetTag("System")
-        .SetSummary("Look for local resources")
-        .SetDescription("This URI can be used to perform a search on the content of the local Orthanc server, "
-                        "in a way that is similar to querying remote DICOM modalities using C-FIND SCU: "
-                        "https://orthanc.uclouvain.be/book/users/rest.html#performing-finds-within-orthanc")
+      RestApiCallDocumentation& doc = call.GetDocumentation();
+
+      doc.SetTag("System")
         .SetRequestField(KEY_CASE_SENSITIVE, RestApiCallDocumentation::Type_Boolean,
                          "Enable case-sensitive search for PN value representations (defaults to configuration option `CaseSensitivePN`)", false)
-        .SetRequestField(KEY_EXPAND, RestApiCallDocumentation::Type_Boolean,
-                         "Also retrieve the content of the matching resources, not only their Orthanc identifiers", false)
         .SetRequestField(KEY_LEVEL, RestApiCallDocumentation::Type_String,
                          "Level of the query (`Patient`, `Study`, `Series` or `Instance`)", true)
-        .SetRequestField(KEY_LIMIT, RestApiCallDocumentation::Type_Number,
-                         "Limit the number of reported resources", false)
-        .SetRequestField(KEY_SINCE, RestApiCallDocumentation::Type_Number,
-                         "Show only the resources since the provided index (in conjunction with `Limit`)", false)
-        .SetRequestField(KEY_REQUESTED_TAGS, RestApiCallDocumentation::Type_JsonListOfStrings,
-                         "A list of DICOM tags to include in the response (applicable only if \"Expand\" is set to true).  "
-                         "The tags requested tags are returned in the 'RequestedTags' field in the response.  "
-                         "Note that, if you are requesting tags that are not listed in the Main Dicom Tags stored in DB, building the response "
-                         "might be slow since Orthanc will need to access the DICOM files.  If not specified, Orthanc will return "
-                         "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_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)
-        .SetRequestField(KEY_ORDER_BY, RestApiCallDocumentation::Type_JsonListOfObjects,
-                         "Array of associative arrays containing the requested ordering (new in Orthanc 1.12.5)", true)
         .SetRequestField(KEY_PARENT_PATIENT, RestApiCallDocumentation::Type_String,
                          "Limit the reported resources to descendants of this patient (new in Orthanc 1.12.5)", true)
         .SetRequestField(KEY_PARENT_STUDY, RestApiCallDocumentation::Type_String,
                          "Limit the reported resources to descendants of this study (new in Orthanc 1.12.5)", true)
         .SetRequestField(KEY_PARENT_SERIES, RestApiCallDocumentation::Type_String,
                          "Limit the reported resources to descendants of this series (new in Orthanc 1.12.5)", true)
-        .SetRequestField(KEY_QUERY_METADATA, RestApiCallDocumentation::Type_JsonObject,
-                         "Associative array containing the filter on the values of the metadata (new in Orthanc 1.12.5)", true)
-        .SetRequestField(KEY_RESPONSE_CONTENT, RestApiCallDocumentation::Type_JsonListOfStrings,
-                         "Defines the content of response for each returned resource.  Allowed values are `MainDicomTags`, "
-                         "`Metadata`, `Children`, `Parent`, `Labels`, `Status`, `IsStable`, `Attachments`.  "
-                         "(new in Orthanc 1.12.5)", true)
-        .AddAnswerType(MimeType_Json, "JSON array containing either the Orthanc identifiers, or detailed information "
-                       "about the reported resources (if `Expand` argument is `true`)");
+        .SetRequestField(KEY_METADATA_QUERY, RestApiCallDocumentation::Type_JsonObject,
+                         "Associative array containing the filter on the values of the metadata (new in Orthanc 1.12.5)", true);
+      
+      switch (requestType)
+      {
+        case FindType_Find:
+          doc.SetSummary("Look for local resources")
+          .SetDescription("This URI can be used to perform a search on the content of the local Orthanc server, "
+                          "in a way that is similar to querying remote DICOM modalities using C-FIND SCU: "
+                          "https://orthanc.uclouvain.be/book/users/rest.html#performing-finds-within-orthanc")
+          .SetRequestField(KEY_EXPAND, RestApiCallDocumentation::Type_Boolean,
+                          "Also retrieve the content of the matching resources, not only their Orthanc identifiers", false)
+          .SetRequestField(KEY_LIMIT, RestApiCallDocumentation::Type_Number,
+                          "Limit the number of reported resources", false)
+          .SetRequestField(KEY_SINCE, RestApiCallDocumentation::Type_Number,
+                          "Show only the resources since the provided index (in conjunction with `Limit`)", false)
+          .SetRequestField(KEY_REQUESTED_TAGS, RestApiCallDocumentation::Type_JsonListOfStrings,
+                          "A list of DICOM tags to include in the response (applicable only if \"Expand\" is set to true).  "
+                          "The tags requested tags are returned in the 'RequestedTags' field in the response.  "
+                          "Note that, if you are requesting tags that are not listed in the Main Dicom Tags stored in DB, building the response "
+                          "might be slow since Orthanc will need to access the DICOM files.  If not specified, Orthanc will return "
+                          "all Main Dicom Tags to keep backward compatibility with Orthanc prior to 1.11.0.", false)
+          .SetRequestField(KEY_ORDER_BY, RestApiCallDocumentation::Type_JsonListOfObjects,
+                          "Array of associative arrays containing the requested ordering (new in Orthanc 1.12.5)", true)
+          .SetRequestField(KEY_RESPONSE_CONTENT, RestApiCallDocumentation::Type_JsonListOfStrings,
+                          "Defines the content of response for each returned resource.  Allowed values are `MainDicomTags`, "
+                          "`Metadata`, `Children`, `Parent`, `Labels`, `Status`, `IsStable`, `Attachments`.  "
+                          "(new in Orthanc 1.12.5)", true)
+          .AddAnswerType(MimeType_Json, "JSON array containing either the Orthanc identifiers, or detailed information "
+                        "about the reported resources (if `Expand` argument is `true`)");
+          break;
+        case FindType_Count:
+          doc.SetSummary("Count local resources")
+          .SetDescription("This URI can be used to count the resources that are matching criterias on the content of the local Orthanc server, "
+                          "in a way that is similar to tools/find")
+          .AddAnswerType(MimeType_Json, "A JSON object with the `Count` of matching resources");
+          break;
+        default:
+          throw OrthancException(ErrorCode_NotImplemented);
+      }
+        
       return;
     }
 
@@ -3138,30 +3161,6 @@
       throw OrthancException(ErrorCode_BadRequest, 
                              "Field \"" + std::string(KEY_CASE_SENSITIVE) + "\" must be a Boolean");
     }
-    else if (request.isMember(KEY_LIMIT) && 
-             request[KEY_LIMIT].type() != Json::intValue)
-    {
-      throw OrthancException(ErrorCode_BadRequest, 
-                             "Field \"" + std::string(KEY_LIMIT) + "\" must be an integer");
-    }
-    else if (request.isMember(KEY_SINCE) &&
-             request[KEY_SINCE].type() != Json::intValue)
-    {
-      throw OrthancException(ErrorCode_BadRequest, 
-                             "Field \"" + std::string(KEY_SINCE) + "\" must be an integer");
-    }
-    else if (request.isMember(KEY_REQUESTED_TAGS) &&
-             request[KEY_REQUESTED_TAGS].type() != Json::arrayValue)
-    {
-      throw OrthancException(ErrorCode_BadRequest, 
-                             "Field \"" + std::string(KEY_REQUESTED_TAGS) + "\" must be an array");
-    }
-    else if (request.isMember(KEY_RESPONSE_CONTENT) &&
-             request[KEY_RESPONSE_CONTENT].type() != Json::arrayValue)
-    {
-      throw OrthancException(ErrorCode_BadRequest, 
-                             "Field \"" + std::string(KEY_RESPONSE_CONTENT) + "\" must be an array");
-    }
     else if (request.isMember(KEY_LABELS) &&
              request[KEY_LABELS].type() != Json::arrayValue)
     {
@@ -3174,17 +3173,11 @@
       throw OrthancException(ErrorCode_BadRequest, 
                              "Field \"" + std::string(KEY_LABELS_CONSTRAINT) + "\" must be an array of strings");
     }
-    else if (request.isMember(KEY_ORDER_BY) &&
-             request[KEY_ORDER_BY].type() != Json::arrayValue)
+    else if (request.isMember(KEY_METADATA_QUERY) &&
+             request[KEY_METADATA_QUERY].type() != Json::objectValue)
     {
       throw OrthancException(ErrorCode_BadRequest, 
-                             "Field \"" + std::string(KEY_ORDER_BY) + "\" must be an array");
-    }
-    else if (request.isMember(KEY_QUERY_METADATA) &&
-             request[KEY_QUERY_METADATA].type() != Json::objectValue)
-    {
-      throw OrthancException(ErrorCode_BadRequest, 
-                             "Field \"" + std::string(KEY_QUERY_METADATA) + "\" must be an JSON object");
+                             "Field \"" + std::string(KEY_METADATA_QUERY) + "\" must be an JSON object");
     }
     else if (request.isMember(KEY_PARENT_PATIENT) &&
              request[KEY_PARENT_PATIENT].type() != Json::stringValue)
@@ -3204,60 +3197,68 @@
       throw OrthancException(ErrorCode_BadRequest, 
                              "Field \"" + std::string(KEY_PARENT_SERIES) + "\" must be a string");
     }
+    else if (requestType == FindType_Find && request.isMember(KEY_LIMIT) && 
+             request[KEY_LIMIT].type() != Json::intValue)
+    {
+      throw OrthancException(ErrorCode_BadRequest, 
+                             "Field \"" + std::string(KEY_LIMIT) + "\" must be an integer");
+    }
+    else if (requestType == FindType_Find && request.isMember(KEY_SINCE) &&
+             request[KEY_SINCE].type() != Json::intValue)
+    {
+      throw OrthancException(ErrorCode_BadRequest, 
+                             "Field \"" + std::string(KEY_SINCE) + "\" must be an integer");
+    }
+    else if (requestType == FindType_Find && request.isMember(KEY_REQUESTED_TAGS) &&
+             request[KEY_REQUESTED_TAGS].type() != Json::arrayValue)
+    {
+      throw OrthancException(ErrorCode_BadRequest, 
+                             "Field \"" + std::string(KEY_REQUESTED_TAGS) + "\" must be an array");
+    }
+    else if (requestType == FindType_Find && request.isMember(KEY_RESPONSE_CONTENT) &&
+             request[KEY_RESPONSE_CONTENT].type() != Json::arrayValue)
+    {
+      throw OrthancException(ErrorCode_BadRequest, 
+                             "Field \"" + std::string(KEY_RESPONSE_CONTENT) + "\" must be an array");
+    }
+    else if (requestType == FindType_Find && request.isMember(KEY_ORDER_BY) &&
+             request[KEY_ORDER_BY].type() != Json::arrayValue)
+    {
+      throw OrthancException(ErrorCode_BadRequest, 
+                             "Field \"" + std::string(KEY_ORDER_BY) + "\" must be an array");
+    }
     else if (true)
     {
       ResponseContentFlags responseContent = ResponseContentFlags_ID;
       
-      if (request.isMember(KEY_RESPONSE_CONTENT))
+      if (requestType == FindType_Find)
       {
-        responseContent = ResponseContentFlags_Default;
-
-        for (Json::ArrayIndex i = 0; i < request[KEY_RESPONSE_CONTENT].size(); ++i)
+        if (request.isMember(KEY_RESPONSE_CONTENT))
         {
-          responseContent = static_cast<ResponseContentFlags>(static_cast<uint32_t>(responseContent) | StringToResponseContent(request[KEY_RESPONSE_CONTENT][i].asString()));
+          responseContent = ResponseContentFlags_Default;
+
+          for (Json::ArrayIndex i = 0; i < request[KEY_RESPONSE_CONTENT].size(); ++i)
+          {
+            responseContent = static_cast<ResponseContentFlags>(static_cast<uint32_t>(responseContent) | StringToResponseContent(request[KEY_RESPONSE_CONTENT][i].asString()));
+          }
+        }
+        else if (request.isMember(KEY_EXPAND) && request[KEY_EXPAND].asBool())
+        {
+          responseContent = ResponseContentFlags_ExpandTrue;
         }
       }
-      else if (request.isMember(KEY_EXPAND) && request[KEY_EXPAND].asBool())
+      else if (requestType == FindType_Count)
       {
-        responseContent = ResponseContentFlags_ExpandTrue;
+        responseContent = ResponseContentFlags_INTERNAL_CountResources;
       }
 
       const ResourceType level = StringToResourceType(request[KEY_LEVEL].asCString());
 
       ResourceFinder finder(level, responseContent);
-      finder.SetDatabaseLimits(context.GetDatabaseLimits(level));
-
-      const DicomToJsonFormat format = OrthancRestApi::GetDicomFormat(request, DicomToJsonFormat_Human);
-
-      if (request.isMember(KEY_LIMIT))
-      {
-        int64_t tmp = request[KEY_LIMIT].asInt64();
-        if (tmp < 0)
-        {
-          throw OrthancException(ErrorCode_ParameterOutOfRange,
-                                 "Field \"" + std::string(KEY_LIMIT) + "\" must be a positive integer");
-        }
-        else if (tmp != 0)  // This is for compatibility with Orthanc 1.12.4
-        {
-          finder.SetLimitsCount(static_cast<uint64_t>(tmp));
-        }
-      }
-
-      if (request.isMember(KEY_SINCE))
-      {
-        int64_t tmp = request[KEY_SINCE].asInt64();
-        if (tmp < 0)
-        {
-          throw OrthancException(ErrorCode_ParameterOutOfRange,
-                                 "Field \"" + std::string(KEY_SINCE) + "\" must be a positive integer");
-        }
-        else
-        {
-          finder.SetLimitsSince(static_cast<uint64_t>(tmp));
-        }
-      }
-
-      {
+
+      DatabaseLookup dicomTagLookup;
+
+      { // common query code
         bool caseSensitive = false;
         if (request.isMember(KEY_CASE_SENSITIVE))
         {
@@ -3265,7 +3266,6 @@
         }
 
         { // DICOM Tag query
-          DatabaseLookup dicomTagLookup;
 
           Json::Value::Members members = request[KEY_QUERY].getMemberNames();
           for (size_t i = 0; i < members.size(); i++)
@@ -3288,21 +3288,27 @@
             }
           }
 
+          if (requestType == FindType_Count && !dicomTagLookup.HasOnlyMainDicomTags())
+          {
+              throw OrthancException(ErrorCode_BadRequest,
+                                    "Unable to count resources when querying tags that are not stored as MainDicomTags in the Database");
+          }
+
           finder.SetDatabaseLookup(dicomTagLookup);
         }
 
         { // Metadata query
-          Json::Value::Members members = request[KEY_QUERY_METADATA].getMemberNames();
+          Json::Value::Members members = request[KEY_METADATA_QUERY].getMemberNames();
           for (size_t i = 0; i < members.size(); i++)
           {
-            if (request[KEY_QUERY_METADATA][members[i]].type() != Json::stringValue)
+            if (request[KEY_METADATA_QUERY][members[i]].type() != Json::stringValue)
             {
               throw OrthancException(ErrorCode_BadRequest,
                                     "Tag \"" + members[i] + "\" must be associated with a string");
             }
             MetadataType metadata = StringToMetadata(members[i]);
 
-            const std::string value = request[KEY_QUERY_METADATA][members[i]].asString();
+            const std::string value = request[KEY_METADATA_QUERY][members[i]].asString();
 
             if (!value.empty())
             {
@@ -3324,132 +3330,186 @@
             }
           }
         }
-      }
-
-      if (request.isMember(KEY_REQUESTED_TAGS))
-      {
-        std::set<DicomTag> requestedTags;
-        FromDcmtkBridge::ParseListOfTags(requestedTags, request[KEY_REQUESTED_TAGS]);
-        finder.AddRequestedTags(requestedTags);
+
+        { // labels query
+          if (request.isMember(KEY_LABELS))  // New in Orthanc 1.12.0
+          {
+            for (Json::Value::ArrayIndex i = 0; i < request[KEY_LABELS].size(); i++)
+            {
+              if (request[KEY_LABELS][i].type() != Json::stringValue)
+              {
+                throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_LABELS) + "\" must contain strings");
+              }
+              else
+              {
+                finder.AddLabel(request[KEY_LABELS][i].asString());
+              }
+            }
+          }
+
+          finder.SetLabelsConstraint(LabelsConstraint_All);
+
+          if (request.isMember(KEY_LABELS_CONSTRAINT))
+          {
+            const std::string& s = request[KEY_LABELS_CONSTRAINT].asString();
+            if (s == "All")
+            {
+              finder.SetLabelsConstraint(LabelsConstraint_All);
+            }
+            else if (s == "Any")
+            {
+              finder.SetLabelsConstraint(LabelsConstraint_Any);
+            }
+            else if (s == "None")
+            {
+              finder.SetLabelsConstraint(LabelsConstraint_None);
+            }
+            else
+            {
+              throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_LABELS_CONSTRAINT) + "\" must be \"All\", \"Any\", or \"None\"");
+            }
+          }
+        }
+
+        // parents query
+        if (request.isMember(KEY_PARENT_PATIENT)) // New in Orthanc 1.12.5
+        {
+          finder.SetOrthancId(ResourceType_Patient, request[KEY_PARENT_PATIENT].asString());
+        }
+        else if (request.isMember(KEY_PARENT_STUDY))
+        {
+          finder.SetOrthancId(ResourceType_Study, request[KEY_PARENT_STUDY].asString());
+        }
+        else if (request.isMember(KEY_PARENT_SERIES))
+        {
+          finder.SetOrthancId(ResourceType_Series, request[KEY_PARENT_SERIES].asString());
+        }
       }
 
-      if (request.isMember(KEY_LABELS))  // New in Orthanc 1.12.0
+      // response
+      if (requestType == FindType_Find)
       {
-        for (Json::Value::ArrayIndex i = 0; i < request[KEY_LABELS].size(); i++)
+        const DicomToJsonFormat format = OrthancRestApi::GetDicomFormat(request, DicomToJsonFormat_Human);
+
+        finder.SetDatabaseLimits(context.GetDatabaseLimits(level));
+
+        if ((request.isMember(KEY_LIMIT) || request.isMember(KEY_SINCE)) &&
+          !dicomTagLookup.HasOnlyMainDicomTags())
+        {
+            throw OrthancException(ErrorCode_BadRequest,
+                                  "Unable to use " + std::string(KEY_LIMIT) + " or " + std::string(KEY_SINCE) + " in tools/find when querying tags that are not stored as MainDicomTags in the Database");
+        }
+
+        if (request.isMember(KEY_LIMIT))
         {
-          if (request[KEY_LABELS][i].type() != Json::stringValue)
+          int64_t tmp = request[KEY_LIMIT].asInt64();
+          if (tmp < 0)
+          {
+            throw OrthancException(ErrorCode_ParameterOutOfRange,
+                                  "Field \"" + std::string(KEY_LIMIT) + "\" must be a positive integer");
+          }
+          else if (tmp != 0)  // This is for compatibility with Orthanc 1.12.4
           {
-            throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_LABELS) + "\" must contain strings");
+            finder.SetLimitsCount(static_cast<uint64_t>(tmp));
+          }
+        }
+
+        if (request.isMember(KEY_SINCE))
+        {
+          int64_t tmp = request[KEY_SINCE].asInt64();
+          if (tmp < 0)
+          {
+            throw OrthancException(ErrorCode_ParameterOutOfRange,
+                                  "Field \"" + std::string(KEY_SINCE) + "\" must be a positive integer");
           }
           else
           {
-            finder.AddLabel(request[KEY_LABELS][i].asString());
+            finder.SetLimitsSince(static_cast<uint64_t>(tmp));
           }
         }
-      }
-
-      finder.SetLabelsConstraint(LabelsConstraint_All);
-
-      if (request.isMember(KEY_LABELS_CONSTRAINT))
-      {
-        const std::string& s = request[KEY_LABELS_CONSTRAINT].asString();
-        if (s == "All")
+
+        if (request.isMember(KEY_REQUESTED_TAGS))
         {
-          finder.SetLabelsConstraint(LabelsConstraint_All);
-        }
-        else if (s == "Any")
-        {
-          finder.SetLabelsConstraint(LabelsConstraint_Any);
-        }
-        else if (s == "None")
-        {
-          finder.SetLabelsConstraint(LabelsConstraint_None);
-        }
-        else
-        {
-          throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_LABELS_CONSTRAINT) + "\" must be \"All\", \"Any\", or \"None\"");
+          std::set<DicomTag> requestedTags;
+          FromDcmtkBridge::ParseListOfTags(requestedTags, request[KEY_REQUESTED_TAGS]);
+          finder.AddRequestedTags(requestedTags);
         }
-      }
-
-      if (request.isMember(KEY_PARENT_PATIENT)) // New in Orthanc 1.12.5
-      {
-        finder.SetOrthancId(ResourceType_Patient, request[KEY_PARENT_PATIENT].asString());
-      }
-      else if (request.isMember(KEY_PARENT_STUDY))
-      {
-        finder.SetOrthancId(ResourceType_Study, request[KEY_PARENT_STUDY].asString());
-      }
-      else if (request.isMember(KEY_PARENT_SERIES))
-      {
-        finder.SetOrthancId(ResourceType_Series, request[KEY_PARENT_SERIES].asString());
-     }
-
-      if (request.isMember(KEY_ORDER_BY))  // New in Orthanc 1.12.5
-      {
-        for (Json::Value::ArrayIndex i = 0; i < request[KEY_ORDER_BY].size(); i++)
+
+        if (request.isMember(KEY_ORDER_BY))  // New in Orthanc 1.12.5
         {
-          if (request[KEY_ORDER_BY][i].type() != Json::objectValue)
+          for (Json::Value::ArrayIndex i = 0; i < request[KEY_ORDER_BY].size(); i++)
           {
-            throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_ORDER_BY) + "\" must contain objects");
-          }
-          else
-          {
-            const Json::Value& order = request[KEY_ORDER_BY][i];
-            FindRequest::OrderingDirection direction;
-            std::string directionString;
-            std::string typeString;
-
-            if (!order.isMember(KEY_ORDER_BY_KEY) || order[KEY_ORDER_BY_KEY].type() != Json::stringValue)
+            if (request[KEY_ORDER_BY][i].type() != Json::objectValue)
             {
-              throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_ORDER_BY_KEY) + "\" must be a string");
-            }
-
-            if (!order.isMember(KEY_ORDER_BY_DIRECTION) || order[KEY_ORDER_BY_DIRECTION].type() != Json::stringValue)
-            {
-              throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_ORDER_BY_DIRECTION) + "\" must be \"ASC\" or \"DESC\"");
-            }
-
-            Toolbox::ToLowerCase(directionString,  order[KEY_ORDER_BY_DIRECTION].asString());
-            if (directionString == "asc")
-            {
-              direction = FindRequest::OrderingDirection_Ascending;
-            }
-            else if (directionString == "desc")
-            {
-              direction = FindRequest::OrderingDirection_Descending;
+              throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_ORDER_BY) + "\" must contain objects");
             }
             else
             {
-              throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_ORDER_BY_DIRECTION) + "\" must be \"ASC\" or \"DESC\"");
-            }
-
-            if (!order.isMember(KEY_ORDER_BY_TYPE) || order[KEY_ORDER_BY_TYPE].type() != Json::stringValue)
-            {
-              throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_ORDER_BY_TYPE) + "\" must be \"DicomTag\" or \"Metadata\"");
-            }
-
-            Toolbox::ToLowerCase(typeString, order[KEY_ORDER_BY_TYPE].asString());
-            if (typeString == "dicomtag")
-            {
-              DicomTag tag = FromDcmtkBridge::ParseTag(order[KEY_ORDER_BY_KEY].asString());
-              finder.AddOrdering(tag, direction);
-            }
-            else if (typeString == "metadata")
-            {
-              MetadataType metadata = StringToMetadata(order[KEY_ORDER_BY_KEY].asString());
-              finder.AddOrdering(metadata, direction);
-            }
-            else
-            {
-              throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_ORDER_BY_TYPE) + "\" must be \"DicomTag\" or \"Metadata\"");
+              const Json::Value& order = request[KEY_ORDER_BY][i];
+              FindRequest::OrderingDirection direction;
+              std::string directionString;
+              std::string typeString;
+
+              if (!order.isMember(KEY_ORDER_BY_KEY) || order[KEY_ORDER_BY_KEY].type() != Json::stringValue)
+              {
+                throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_ORDER_BY_KEY) + "\" must be a string");
+              }
+
+              if (!order.isMember(KEY_ORDER_BY_DIRECTION) || order[KEY_ORDER_BY_DIRECTION].type() != Json::stringValue)
+              {
+                throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_ORDER_BY_DIRECTION) + "\" must be \"ASC\" or \"DESC\"");
+              }
+
+              Toolbox::ToLowerCase(directionString,  order[KEY_ORDER_BY_DIRECTION].asString());
+              if (directionString == "asc")
+              {
+                direction = FindRequest::OrderingDirection_Ascending;
+              }
+              else if (directionString == "desc")
+              {
+                direction = FindRequest::OrderingDirection_Descending;
+              }
+              else
+              {
+                throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_ORDER_BY_DIRECTION) + "\" must be \"ASC\" or \"DESC\"");
+              }
+
+              if (!order.isMember(KEY_ORDER_BY_TYPE) || order[KEY_ORDER_BY_TYPE].type() != Json::stringValue)
+              {
+                throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_ORDER_BY_TYPE) + "\" must be \"DicomTag\" or \"Metadata\"");
+              }
+
+              Toolbox::ToLowerCase(typeString, order[KEY_ORDER_BY_TYPE].asString());
+              if (typeString == "dicomtag")
+              {
+                DicomTag tag = FromDcmtkBridge::ParseTag(order[KEY_ORDER_BY_KEY].asString());
+                finder.AddOrdering(tag, direction);
+              }
+              else if (typeString == "metadata")
+              {
+                MetadataType metadata = StringToMetadata(order[KEY_ORDER_BY_KEY].asString());
+                finder.AddOrdering(metadata, direction);
+              }
+              else
+              {
+                throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_ORDER_BY_TYPE) + "\" must be \"DicomTag\" or \"Metadata\"");
+              }
             }
           }
         }
+
+        Json::Value answer;
+        finder.Execute(answer, context, format, false /* no "Metadata" field */);
+        call.GetOutput().AnswerJson(answer);
       }
-
-      Json::Value answer;
-      finder.Execute(answer, context, format, false /* no "Metadata" field */);
-      call.GetOutput().AnswerJson(answer);
+      else if (requestType == FindType_Count)
+      {
+        uint64_t count = finder.Count(context);
+        Json::Value answer;
+        answer["Count"] = Json::Value::UInt64(count);
+        call.GetOutput().AnswerJson(answer);
+      }
+
     }
   }
 
@@ -4237,7 +4297,11 @@
     }
 
     Register("/tools/lookup", Lookup);
-    Register("/tools/find", Find);
+    Register("/tools/find", Find<FindType_Find>);
+    if (context_.GetIndex().HasFindSupport())
+    {
+      Register("/tools/count-resources", Find<FindType_Count>);
+    }
 
     Register("/patients/{id}/studies", GetChildResources<ResourceType_Patient, ResourceType_Study>);
     Register("/patients/{id}/series", GetChildResources<ResourceType_Patient, ResourceType_Series>);
--- a/OrthancServer/Sources/ResourceFinder.cpp	Wed Oct 09 11:01:11 2024 +0200
+++ b/OrthancServer/Sources/ResourceFinder.cpp	Tue Oct 29 13:11:40 2024 +0000
@@ -472,51 +472,73 @@
   }
 
 
-  void ResourceFinder::UpdateRequestLimits()
+  void ResourceFinder::UpdateRequestLimits(ServerContext& context)
   {
-    // By default, use manual paging
-    pagingMode_ = PagingMode_FullManual;
+    if (context.GetIndex().HasFindSupport())  // in this case, limits are fully implemented in DB
+    {
+      pagingMode_ = PagingMode_FullDatabase;
 
-    if (databaseLimits_ != 0)
-    {
-      request_.SetLimits(0, databaseLimits_ + 1);
+      if (hasLimitsSince_ || hasLimitsCount_)
+      {
+        pagingMode_ = PagingMode_FullDatabase;
+        if (databaseLimits_ != 0 && limitsCount_ > databaseLimits_)
+        {
+          LOG(WARNING) << "ResourceFinder: 'Limit' is larger than LimitFindResults/LimitFindInstances configurations, using limit fron the configuration file";
+          limitsCount_ = databaseLimits_;
+        }
+
+        request_.SetLimits(limitsSince_, limitsCount_);
+      }
+      else if (databaseLimits_ != 0)
+      {
+        request_.SetLimits(0, databaseLimits_);
+      }
+
     }
     else
     {
-      request_.ClearLimits();
-    }
-
-    if (lookup_.get() == NULL &&
-        (hasLimitsSince_ || hasLimitsCount_))
-    {
-      pagingMode_ = PagingMode_FullDatabase;
-      request_.SetLimits(limitsSince_, limitsCount_);
-    }
+      // By default, use manual paging
+      pagingMode_ = PagingMode_FullManual;
 
-    if (lookup_.get() != NULL &&
-        isSimpleLookup_ &&
-        (hasLimitsSince_ || hasLimitsCount_))
-    {
-      /**
-       * TODO-FIND: "IDatabaseWrapper::ApplyLookupResources()" only
-       * accept the "limit" argument.  The "since" must be implemented
-       * manually.
-       **/
-
-      if (hasLimitsSince_ &&
-          limitsSince_ != 0)
+      if (databaseLimits_ != 0)
       {
-        pagingMode_ = PagingMode_ManualSkip;
-        request_.SetLimits(0, limitsCount_ + limitsSince_);
+        request_.SetLimits(0, databaseLimits_ + 1);
       }
       else
       {
+        request_.ClearLimits();
+      }
+
+      if (lookup_.get() == NULL &&
+          (hasLimitsSince_ || hasLimitsCount_))
+      {
         pagingMode_ = PagingMode_FullDatabase;
-        request_.SetLimits(0, limitsCount_);
+        request_.SetLimits(limitsSince_, limitsCount_);
+      }
+
+      if (lookup_.get() != NULL &&
+          isSimpleLookup_ &&
+          (hasLimitsSince_ || hasLimitsCount_))
+      {
+        /**
+         * TODO-FIND: "IDatabaseWrapper::ApplyLookupResources()" only
+         * accept the "limit" argument.  The "since" must be implemented
+         * manually.
+         **/
+
+        if (hasLimitsSince_ &&
+            limitsSince_ != 0)
+        {
+          pagingMode_ = PagingMode_ManualSkip;
+          request_.SetLimits(0, limitsCount_ + limitsSince_);
+        }
+        else
+        {
+          pagingMode_ = PagingMode_FullDatabase;
+          request_.SetLimits(0, limitsCount_);
+        }
       }
     }
-
-    // TODO-FIND: More cases could be added, depending on "GetDatabaseCapabilities()"
   }
 
 
@@ -543,8 +565,6 @@
       isWarning005Enabled_ = lock.GetConfiguration().IsWarningEnabled(Warnings_005_RequestingTagFromLowerResourceLevel);
     }
 
-    UpdateRequestLimits();
-
     request_.SetRetrieveMainDicomTags(responseContent_ & ResponseContentFlags_MainDicomTags);
     request_.SetRetrieveMetadata((responseContent_ & ResponseContentFlags_Metadata) || (responseContent_ & ResponseContentFlags_MetadataLegacy));
     request_.SetRetrieveLabels(responseContent_ & ResponseContentFlags_Labels);
@@ -587,7 +607,6 @@
   void ResourceFinder::SetDatabaseLimits(uint64_t limits)
   {
     databaseLimits_ = limits;
-    UpdateRequestLimits();
   }
 
 
@@ -601,7 +620,6 @@
     {
       hasLimitsSince_ = true;
       limitsSince_ = since;
-      UpdateRequestLimits();
     }
   }
 
@@ -616,7 +634,6 @@
     {
       hasLimitsCount_ = true;
       limitsCount_ = count;
-      UpdateRequestLimits();
     }
   }
 
@@ -674,8 +691,6 @@
         throw OrthancException(ErrorCode_InternalError);
       }
     }
-
-    UpdateRequestLimits();
   }
 
 
@@ -991,10 +1006,19 @@
     }
   }
 
+  uint64_t ResourceFinder::Count(ServerContext& context) const
+  {
+    uint64_t count = 0;
+    context.GetIndex().ExecuteCount(count, request_);
+    return count;
+  }
+
 
   void ResourceFinder::Execute(IVisitor& visitor,
-                               ServerContext& context) const
+                               ServerContext& context)
   {
+    UpdateRequestLimits(context);
+
     bool isWarning002Enabled = false;
     bool isWarning004Enabled = false;
 
@@ -1146,7 +1170,7 @@
   void ResourceFinder::Execute(Json::Value& target,
                                ServerContext& context,
                                DicomToJsonFormat format,
-                               bool includeAllMetadata) const
+                               bool includeAllMetadata)
   {
     class Visitor : public IVisitor
     {
@@ -1202,6 +1226,8 @@
       }
     };
 
+    UpdateRequestLimits(context);
+
     target = Json::arrayValue;
 
     Visitor visitor(*this, context.GetIndex(), target, format, HasRequestedTags(), includeAllMetadata);
@@ -1212,7 +1238,7 @@
   bool ResourceFinder::ExecuteOneResource(Json::Value& target,
                                           ServerContext& context,
                                           DicomToJsonFormat format,
-                                          bool includeAllMetadata) const
+                                          bool includeAllMetadata)
   {
     Json::Value answer;
     Execute(answer, context, format, includeAllMetadata);
--- a/OrthancServer/Sources/ResourceFinder.h	Wed Oct 09 11:01:11 2024 +0200
+++ b/OrthancServer/Sources/ResourceFinder.h	Tue Oct 29 13:11:40 2024 +0000
@@ -94,7 +94,7 @@
     void InjectComputedTags(DicomMap& requestedTags,
                             const FindResponse::Resource& resource) const;
 
-    void UpdateRequestLimits();
+    void UpdateRequestLimits(ServerContext& context);
 
     bool HasRequestedTags() const
     {
@@ -187,16 +187,18 @@
                 DicomToJsonFormat format) const;
 
     void Execute(IVisitor& visitor,
-                 ServerContext& context) const;
+                 ServerContext& context);
 
     void Execute(Json::Value& target,
                  ServerContext& context,
                  DicomToJsonFormat format,
-                 bool includeAllMetadata) const;
+                 bool includeAllMetadata);
 
     bool ExecuteOneResource(Json::Value& target,
                             ServerContext& context,
                             DicomToJsonFormat format,
-                            bool includeAllMetadata) const;
+                            bool includeAllMetadata);
+
+    uint64_t Count(ServerContext& context) const;
   };
 }
--- a/OrthancServer/Sources/ServerEnumerations.h	Wed Oct 09 11:01:11 2024 +0200
+++ b/OrthancServer/Sources/ServerEnumerations.h	Tue Oct 29 13:11:40 2024 +0000
@@ -137,6 +137,8 @@
     ResponseContentFlags_Labels               = (1 << 11),
     ResponseContentFlags_IsStable             = (1 << 12),
 
+    ResponseContentFlags_INTERNAL_CountResources = (1 << 31),
+    
     // Some predefined combinations
     ResponseContentFlags_ExpandTrue  = (ResponseContentFlags_ID |
                                         ResponseContentFlags_Type |