changeset 5833:58c549b881ae find-refactoring-clean

merged find-refactoring -> find-refactoring-clean
author Alain Mazy <am@orthanc.team>
date Wed, 09 Oct 2024 11:01:11 +0200
parents f75596b224e0 (current diff) dd2af8692cbc (diff)
children
files OrthancServer/Sources/Database/FindResponse.cpp OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp OrthancServer/Sources/Database/StatelessDatabaseOperations.h OrthancServer/Sources/OrthancFindRequestHandler.cpp OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp OrthancServer/Sources/OrthancWebDav.cpp OrthancServer/Sources/ServerContext.cpp OrthancServer/Sources/ServerContext.h
diffstat 80 files changed, 3742 insertions(+), 1462 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Wed Sep 04 10:54:00 2024 +0200
+++ b/NEWS	Wed Oct 09 11:01:11 2024 +0200
@@ -5,11 +5,43 @@
   - /studies?expand and sibbling routes now also return "Metadata" (if the DB implements 'extended-api-v1')
   - /studies?since=x&limit=0 and sibbling routes: limit=0 now means "no limit" instead of "no results"
 
+General
+-------
+
+* Database:
+  - Introduced database optimizations "ExtendedFind" to replace many small SQL queries
+    by a small number of large SQL queries to greatly reduce the cost of DB latency.
+    Furthermore, this "ExtendedFind" brings new sorting and filtering features to the 
+    Rest API (TODO).
+  - Introduced database optimizations "ExtendedChanges" to allow filtering of /changes.
+  - Reduced the number of SQL queries when ingesting DICOM files.
+* Introduced a new configuration "ReadOnly" to forbid an Orthanc instance to perform 
+  any modifications in the Index DB or in the storage.
+  
+
 REST API
 --------
 
+* API version upgraded to 25
 * Improved parsing of multiple numerical values in DICOM tags.
   https://discourse.orthanc-server.org/t/qido-includefield-with-sequences/4746/6
+* in /system, added a new field "Capabilities" with new values:
+  - "HasExtendedChanges" for DB backend that provides this feature (the default SQLite DB
+    or PostgreSQL vX.X, MySQL vX.X, ODBC vX.X).
+  - "HasExtendedFind" for DB backend that provides this feature (the default SQLite DB
+    or PostgreSQL vX.X, MySQL vX.X, ODBC vX.X).
+* With DB backend with "HasExtendedChanges" support, /changes now supports 2 more options: 
+  - 'type' to filter the changes returned by the query 
+  - 'to' to potentially cycle through changes in reverse order.
+  example: /changes?type=StableStudy&to=7584&limit=100
+* With DB backend with "HasExtendedFind" support, /tools/find now supports new options:
+  - '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.
+  - 'ResponseContent' to define what shall be included in the response for each returned
+    resource (e.g: Metadata, Children, ...)
+
 
 Maintenance
 -----------
@@ -24,12 +56,29 @@
   in very specific use-cases.
 * Fix extremely rare error when 2 threads are trying to create the same folder in the File Storage 
   at the same time.
+* Metrics:
+  - fix a few metrics that were not published
+  - added 2 metrics: orthanc_storage_cache_miss_count & orthanc_storage_cache_hit_count 
 * Upgraded dependencies for static builds:
   - curl 8.9.0
+  - SQLite 3.46
 * Added a new fallback when trying to decode a frame: transcode the file using the plugin
   before decoding the frame.  This solves some issues with JP2K Lossy compression:
   https://discourse.orthanc-server.org/t/decoding-displaying-jpeg2000-lossy-images/5117
-* Added a new warning that can be disabled in the configuration: W003_DecoderFailure
+* Added new warnings that can be disabled in the configuration: 
+  - W003_DecoderFailure
+  - W004_NoMainDicomTagsSignature
+  - W005_RequestingTagFromLowerResourceLevel
+* New default MainDicomTags are now stored in DB.  Note that, in order to store these values
+  for resources that were ingested in Orthanc before this release, you would have to run
+  the Housekeeper plugin or call /reconstruct on every resources
+  - At Study Level:
+    - TimezoneOffsetFromUTC (used in QIDO-RS default queries)
+  - At Series Level:
+    - TimezoneOffsetFromUTC (used in QIDO-RS default queries)
+    - PerformedProcedureStepStartDate (used in QIDO-RS default queries)
+    - PerformedProcedureStepStartTime (used in QIDO-RS default queries)
+    - RequestAttributesSequence (used in QIDO-RS default queries)
 
 
 
--- a/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake	Wed Oct 09 11:01:11 2024 +0200
@@ -39,7 +39,7 @@
 # Version of the Orthanc API, can be retrieved from "/system" URI in
 # order to check whether new URI endpoints are available even if using
 # the mainline version of Orthanc
-set(ORTHANC_API_VERSION "24")
+set(ORTHANC_API_VERSION "25")
 
 
 #####################################################################
--- a/OrthancFramework/Resources/CMake/SQLiteConfiguration.cmake	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancFramework/Resources/CMake/SQLiteConfiguration.cmake	Wed Oct 09 11:01:11 2024 +0200
@@ -37,11 +37,11 @@
 
 
 if (SQLITE_STATIC)
-  SET(SQLITE_SOURCES_DIR ${CMAKE_BINARY_DIR}/sqlite-amalgamation-3270100)
-  SET(SQLITE_MD5 "16717b26358ba81f0bfdac07addc77da")
-  SET(SQLITE_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/sqlite-amalgamation-3270100.zip")
+  SET(SQLITE_SOURCES_DIR ${CMAKE_BINARY_DIR}/sqlite-amalgamation-3460100)
+  SET(SQLITE_MD5 "1fb0f7ebbee45752098cf453b6dffff3")
+  SET(SQLITE_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/sqlite-amalgamation-3460100.zip")
 
-  set(ORTHANC_SQLITE_VERSION 3027001)
+  set(ORTHANC_SQLITE_VERSION 3046001)
 
   DownloadPackage(${SQLITE_MD5} ${SQLITE_URL} "${SQLITE_SOURCES_DIR}")
 
--- a/OrthancFramework/Resources/CodeGeneration/ErrorCodes.json	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancFramework/Resources/CodeGeneration/ErrorCodes.json	Wed Oct 09 11:01:11 2024 +0200
@@ -262,6 +262,11 @@
     "Name": "DuplicateResource", 
     "Description": "Duplicate resource"
   }, 
+  {
+    "Code": 47,
+    "Name": "IncompatibleConfigurations", 
+    "Description": "Your configuration file contains configuration that are mutually incompatible"
+  },
 
 
 
@@ -337,7 +342,7 @@
   {
     "Code": 1011, 
     "Name": "SQLiteBindOutOfRange", 
-    "Description": "SQLite: Bing a value while out of range (serious error)",
+    "Description": "SQLite: Bind a value while out of range (serious error)",
     "SQLite": true
   },
   {
--- a/OrthancFramework/Resources/RetrieveCACertificates.py	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancFramework/Resources/RetrieveCACertificates.py	Wed Oct 09 11:01:11 2024 +0200
@@ -32,7 +32,7 @@
     print('Download a set of CA certificates, convert them to PEM, then format them as a C macro')
     print('Usage: %s [Macro] [Certificate1] <Certificate2>...' % sys.argv[0])
     print('')
-    print('Example: %s BITBUCKET_CERTIFICATES https://cacerts.digicert.com/DigiCertSHA2HighAssuranceServerCA.crt' % sys.argv[0])
+    print('Example: %s GITHUB_CERTIFICATES https://cacerts.digicert.com/DigiCertSHA2HighAssuranceServerCA.crt' % sys.argv[0])
     print('')
     sys.exit(-1)
 
--- a/OrthancFramework/Sources/DicomFormat/DicomMap.cpp	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomMap.cpp	Wed Oct 09 11:01:11 2024 +0200
@@ -55,9 +55,9 @@
   // These lists have a specific signature.  When a resource does not have
   // the metadata "MainDicomTagsSignature", we'll assume that they were stored
   // with an Orthanc prior to 1.11.  It is therefore very important that you never
-  // change these lists !
+  // change these lists !  Update ResetDefaultMainDicomTags instead.
 
-  static const DicomTag DEFAULT_PATIENT_MAIN_DICOM_TAGS[] =
+  static const DicomTag DEFAULT_1_11_PATIENT_MAIN_DICOM_TAGS[] =
   {
     // { DicomTag(0x0010, 0x1010), "PatientAge" },
     // { DicomTag(0x0010, 0x1040), "PatientAddress" },
@@ -66,9 +66,11 @@
     DICOM_TAG_PATIENT_SEX,
     DICOM_TAG_OTHER_PATIENT_IDS,
     DICOM_TAG_PATIENT_ID
+
+    // don't add tags here, check ResetDefaultMainDicomTags instead
   };
   
-  static const DicomTag DEFAULT_STUDY_MAIN_DICOM_TAGS[] =
+  static const DicomTag DEFAULT_1_11_STUDY_MAIN_DICOM_TAGS[] =
   {
     // { DicomTag(0x0010, 0x1020), "PatientSize" },
     // { DicomTag(0x0010, 0x1030), "PatientWeight" },
@@ -84,9 +86,11 @@
     DICOM_TAG_INSTITUTION_NAME,
     DICOM_TAG_REQUESTING_PHYSICIAN,
     DICOM_TAG_REFERRING_PHYSICIAN_NAME
+
+    // don't add tags here, check ResetDefaultMainDicomTags instead
   };
 
-  static const DicomTag DEFAULT_SERIES_MAIN_DICOM_TAGS[] =
+  static const DicomTag DEFAULT_1_11_SERIES_MAIN_DICOM_TAGS[] =
   {
     // { DicomTag(0x0010, 0x1080), "MilitaryRank" },
     DICOM_TAG_SERIES_DATE,
@@ -113,9 +117,11 @@
     DICOM_TAG_PERFORMED_PROCEDURE_STEP_DESCRIPTION,
     DICOM_TAG_ACQUISITION_DEVICE_PROCESSING_DESCRIPTION,
     DICOM_TAG_CONTRAST_BOLUS_AGENT
+
+    // don't add tags here, check ResetDefaultMainDicomTags instead
   };
 
-  static const DicomTag DEFAULT_INSTANCE_MAIN_DICOM_TAGS[] =
+  static const DicomTag DEFAULT_1_11_INSTANCE_MAIN_DICOM_TAGS[] =
   {
     DICOM_TAG_INSTANCE_CREATION_DATE,
     DICOM_TAG_INSTANCE_CREATION_TIME,
@@ -138,6 +144,8 @@
      * indexed in the database by an older version of Orthanc.
      **/
     DICOM_TAG_IMAGE_ORIENTATION_PATIENT  // New in Orthanc 1.4.2
+
+    // don't add tags here, check ResetDefaultMainDicomTags instead
   };
 
   class DicomMap::MainDicomTagsConfiguration : public boost::noncopyable
@@ -187,23 +195,23 @@
       switch (level)
       {
         case ResourceType_Patient:
-          tags = DEFAULT_PATIENT_MAIN_DICOM_TAGS;
-          size = sizeof(DEFAULT_PATIENT_MAIN_DICOM_TAGS) / sizeof(DicomTag);
+          tags = DEFAULT_1_11_PATIENT_MAIN_DICOM_TAGS;
+          size = sizeof(DEFAULT_1_11_PATIENT_MAIN_DICOM_TAGS) / sizeof(DicomTag);
           break;
 
         case ResourceType_Study:
-          tags = DEFAULT_STUDY_MAIN_DICOM_TAGS;
-          size = sizeof(DEFAULT_STUDY_MAIN_DICOM_TAGS) / sizeof(DicomTag);
+          tags = DEFAULT_1_11_STUDY_MAIN_DICOM_TAGS;
+          size = sizeof(DEFAULT_1_11_STUDY_MAIN_DICOM_TAGS) / sizeof(DicomTag);
           break;
 
         case ResourceType_Series:
-          tags = DEFAULT_SERIES_MAIN_DICOM_TAGS;
-          size = sizeof(DEFAULT_SERIES_MAIN_DICOM_TAGS) / sizeof(DicomTag);
+          tags = DEFAULT_1_11_SERIES_MAIN_DICOM_TAGS;
+          size = sizeof(DEFAULT_1_11_SERIES_MAIN_DICOM_TAGS) / sizeof(DicomTag);
           break;
 
         case ResourceType_Instance:
-          tags = DEFAULT_INSTANCE_MAIN_DICOM_TAGS;
-          size = sizeof(DEFAULT_INSTANCE_MAIN_DICOM_TAGS) / sizeof(DicomTag);
+          tags = DEFAULT_1_11_INSTANCE_MAIN_DICOM_TAGS;
+          size = sizeof(DEFAULT_1_11_INSTANCE_MAIN_DICOM_TAGS) / sizeof(DicomTag);
           break;
 
         default:
@@ -247,7 +255,7 @@
       
       if (existingLevelTags.find(tag) != existingLevelTags.end())
       {
-        throw OrthancException(ErrorCode_MainDicomTagsMultiplyDefined, tag.Format() + " is already defined");
+        throw OrthancException(ErrorCode_MainDicomTagsMultiplyDefined, tag.Format() + " is already defined", false);
       }
 
       existingLevelTags.insert(tag);
@@ -287,6 +295,17 @@
       defaultSignatures_[ResourceType_Study] = signatures_[ResourceType_Study];
       defaultSignatures_[ResourceType_Series] = signatures_[ResourceType_Series];
       defaultSignatures_[ResourceType_Instance] = signatures_[ResourceType_Instance];
+
+      // only add new tags here !
+      // introduced in v 1.12.5
+      AddMainDicomTagInternal(DICOM_TAG_TIMEZONE_OFFSET_FROM_UTC, ResourceType_Study);  // used in default QIDO-RS queries
+      
+      AddMainDicomTagInternal(DICOM_TAG_TIMEZONE_OFFSET_FROM_UTC, ResourceType_Series);  // used in default QIDO-RS queries
+      AddMainDicomTagInternal(DICOM_TAG_PERFORMED_PROCEDURE_STEP_START_DATE, ResourceType_Series);  // used in default QIDO-RS queries
+      AddMainDicomTagInternal(DICOM_TAG_PERFORMED_PROCEDURE_STEP_START_TIME, ResourceType_Series);  // used in default QIDO-RS queries
+      AddMainDicomTagInternal(DICOM_TAG_REQUEST_ATTRIBUTES_SEQUENCE, ResourceType_Series);  // used in default QIDO-RS queries
+
+      // TODO-FIND: remove it from metadata when adding it ! AddMainDicomTagInternal(DICOM_TAG_SOP_CLASS_UID, ResourceType_Instance);  // previously saved in a metadata; makes more sense to store it in a DICOM tag
     }
 
     void AddMainDicomTag(const DicomTag& tag,
@@ -328,7 +347,7 @@
       return signatures_[level];
     }
 
-    std::string GetDefaultMainDicomTagsSignature(ResourceType level)
+    std::string GetDefaultMainDicomTagsSignatureFrom1_11(ResourceType level)
     {
 #if !defined(__EMSCRIPTEN__)
       ReaderLock lock(mutex_);
@@ -667,13 +686,16 @@
   }
 
 
-  void DicomMap::CopyTagIfExists(const DicomMap& source,
+  bool DicomMap::CopyTagIfExists(const DicomMap& source,
                                  const DicomTag& tag)
   {
     if (source.HasTag(tag))
     {
       SetValue(tag, source.GetValue(tag));
+      return true;
     }
+
+    return false;
   }
 
 
@@ -805,9 +827,9 @@
     return DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTagsSignature(level);
   }
 
-  std::string DicomMap::GetDefaultMainDicomTagsSignature(ResourceType level)
+  std::string DicomMap::GetDefaultMainDicomTagsSignatureFrom1_11(ResourceType level)
   {
-    return DicomMap::MainDicomTagsConfiguration::GetInstance().GetDefaultMainDicomTagsSignature(level);
+    return DicomMap::MainDicomTagsConfiguration::GetInstance().GetDefaultMainDicomTagsSignatureFrom1_11(level);
   }
 
   void DicomMap::GetTags(std::set<DicomTag>& tags) const
--- a/OrthancFramework/Sources/DicomFormat/DicomMap.h	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomMap.h	Wed Oct 09 11:01:11 2024 +0200
@@ -129,7 +129,7 @@
 
     static void SetupFindInstanceTemplate(DicomMap& result);
 
-    void CopyTagIfExists(const DicomMap& source,
+    bool CopyTagIfExists(const DicomMap& source,
                          const DicomTag& tag);
 
     static bool IsMainDicomTag(const DicomTag& tag, ResourceType level);
@@ -152,7 +152,7 @@
     // returns a string uniquely identifying the list of main dicom tags for a level
     static std::string GetMainDicomTagsSignature(ResourceType level);
 
-    static std::string GetDefaultMainDicomTagsSignature(ResourceType level);
+    static std::string GetDefaultMainDicomTagsSignatureFrom1_11(ResourceType level);
 
     static void GetAllMainDicomTags(std::set<DicomTag>& target);
 
--- a/OrthancFramework/Sources/DicomFormat/DicomTag.h	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomTag.h	Wed Oct 09 11:01:11 2024 +0200
@@ -157,7 +157,10 @@
   static const DicomTag DICOM_TAG_REQUESTING_PHYSICIAN(0x0032, 0x1032);
   static const DicomTag DICOM_TAG_REFERRING_PHYSICIAN_NAME(0x0008, 0x0090);
   static const DicomTag DICOM_TAG_OPERATOR_NAME(0x0008, 0x1070);
+  static const DicomTag DICOM_TAG_PERFORMED_PROCEDURE_STEP_START_DATE(0x0040, 0x0244);
+  static const DicomTag DICOM_TAG_PERFORMED_PROCEDURE_STEP_START_TIME(0x0040, 0x0245);
   static const DicomTag DICOM_TAG_PERFORMED_PROCEDURE_STEP_DESCRIPTION(0x0040, 0x0254);
+  static const DicomTag DICOM_TAG_REQUEST_ATTRIBUTES_SEQUENCE(0x0040, 0x0275);
   static const DicomTag DICOM_TAG_IMAGE_COMMENTS(0x0020, 0x4000);
   static const DicomTag DICOM_TAG_ACQUISITION_DEVICE_PROCESSING_DESCRIPTION(0x0018, 0x1400);
   static const DicomTag DICOM_TAG_ACQUISITION_DEVICE_PROCESSING_CODE(0x0018, 0x1401);
--- a/OrthancFramework/Sources/Enumerations.cpp	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancFramework/Sources/Enumerations.cpp	Wed Oct 09 11:01:11 2024 +0200
@@ -186,6 +186,9 @@
       case ErrorCode_DuplicateResource:
         return "Duplicate resource";
 
+      case ErrorCode_IncompatibleConfigurations:
+        return "Your configuration file contains configuration that are mutually incompatible";
+
       case ErrorCode_SQLiteNotOpened:
         return "SQLite: The database is not opened";
 
@@ -220,7 +223,7 @@
         return "SQLite: Cannot step over a cached statement";
 
       case ErrorCode_SQLiteBindOutOfRange:
-        return "SQLite: Bing a value while out of range (serious error)";
+        return "SQLite: Bind a value while out of range (serious error)";
 
       case ErrorCode_SQLitePrepareStatement:
         return "SQLite: Cannot prepare a cached statement";
--- a/OrthancFramework/Sources/Enumerations.h	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancFramework/Sources/Enumerations.h	Wed Oct 09 11:01:11 2024 +0200
@@ -172,6 +172,7 @@
     ErrorCode_MainDicomTagsMultiplyDefined = 44    /*!< A main DICOM Tag has been defined multiple times for the same resource level */,
     ErrorCode_ForbiddenAccess = 45    /*!< Access to a resource is forbidden */,
     ErrorCode_DuplicateResource = 46    /*!< Duplicate resource */,
+    ErrorCode_IncompatibleConfigurations = 47    /*!< Your configuration file contains configuration that are mutually incompatible */,
     ErrorCode_SQLiteNotOpened = 1000    /*!< SQLite: The database is not opened */,
     ErrorCode_SQLiteAlreadyOpened = 1001    /*!< SQLite: Connection is already open */,
     ErrorCode_SQLiteCannotOpen = 1002    /*!< SQLite: Unable to open the database */,
@@ -183,7 +184,7 @@
     ErrorCode_SQLiteFlush = 1008    /*!< SQLite: Unable to flush the database */,
     ErrorCode_SQLiteCannotRun = 1009    /*!< SQLite: Cannot run a cached statement */,
     ErrorCode_SQLiteCannotStep = 1010    /*!< SQLite: Cannot step over a cached statement */,
-    ErrorCode_SQLiteBindOutOfRange = 1011    /*!< SQLite: Bing a value while out of range (serious error) */,
+    ErrorCode_SQLiteBindOutOfRange = 1011    /*!< SQLite: Bind a value while out of range (serious error) */,
     ErrorCode_SQLitePrepareStatement = 1012    /*!< SQLite: Cannot prepare a cached statement */,
     ErrorCode_SQLiteTransactionAlreadyStarted = 1013    /*!< SQLite: Beginning the same transaction twice */,
     ErrorCode_SQLiteTransactionCommit = 1014    /*!< SQLite: Failure when committing the transaction */,
--- a/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp	Wed Oct 09 11:01:11 2024 +0200
@@ -44,6 +44,8 @@
 static const std::string METRICS_REMOVE_DURATION = "orthanc_storage_remove_duration_ms";
 static const std::string METRICS_READ_BYTES = "orthanc_storage_read_bytes";
 static const std::string METRICS_WRITTEN_BYTES = "orthanc_storage_written_bytes";
+static const std::string METRICS_CACHE_HIT_COUNT = "orthanc_storage_cache_hit_count";
+static const std::string METRICS_CACHE_MISS_COUNT = "orthanc_storage_cache_miss_count";
 
 
 namespace Orthanc
@@ -208,10 +210,19 @@
 
       if (!cacheAccessor.Fetch(content, info.GetUuid(), info.GetContentType()))
       {
+        if (metrics_ != NULL)
+        {
+          metrics_->IncrementIntegerValue(METRICS_CACHE_MISS_COUNT, 1);
+        }
+
         ReadWholeInternal(content, info);
 
         // always store the uncompressed data in cache
         cacheAccessor.Add(info.GetUuid(), info.GetContentType(), content);
+      } 
+      else if (metrics_ != NULL)
+      {
+        metrics_->IncrementIntegerValue(METRICS_CACHE_HIT_COUNT, 1);
       }
     }
   }
@@ -284,10 +295,19 @@
 
       if (!cacheAccessor.Fetch(content, info.GetUuid(), info.GetContentType()))
       {
+        if (metrics_ != NULL)
+        {
+          metrics_->IncrementIntegerValue(METRICS_CACHE_MISS_COUNT, 1);
+        }
+
         ReadRawInternal(content, info);
 
         cacheAccessor.Add(info.GetUuid(), info.GetContentType(), content);
       }
+      else if (metrics_ != NULL)
+      {
+        metrics_->IncrementIntegerValue(METRICS_CACHE_HIT_COUNT, 1);
+      }
     }
   }
 
@@ -353,24 +373,38 @@
       StorageCache::Accessor accessorStartRange(*cache_);
       if (!accessorStartRange.FetchStartRange(target, info.GetUuid(), info.GetContentType(), end))
       {
-        ReadStartRangeInternal(target, info, end);
-        accessorStartRange.AddStartRange(info.GetUuid(), info.GetContentType(), target);
-      }
-      else
-      {
+        // the start range is not in cache, let's check if the whole file is
         StorageCache::Accessor accessorWhole(*cache_);
         if (!accessorWhole.Fetch(target, info.GetUuid(), info.GetContentType()))
         {
-          ReadWholeInternal(target, info);
-          accessorWhole.Add(info.GetUuid(), info.GetContentType(), target);
-        }
+          if (metrics_ != NULL)
+          {
+            metrics_->IncrementIntegerValue(METRICS_CACHE_MISS_COUNT, 1);
+          }
 
-        if (target.size() < end)
+          // if nothing is in the cache, let's read and cache only the start
+          ReadStartRangeInternal(target, info, end);
+          accessorStartRange.AddStartRange(info.GetUuid(), info.GetContentType(), target);
+        }
+        else
         {
-          throw OrthancException(ErrorCode_CorruptedFile);
+          if (metrics_ != NULL)
+          {
+            metrics_->IncrementIntegerValue(METRICS_CACHE_HIT_COUNT, 1);
+          }
+
+          // we have read the whole file, check size and resize if needed
+          if (target.size() < end)
+          {
+            throw OrthancException(ErrorCode_CorruptedFile);
+          }
+
+          target.resize(end);
         }
-
-        target.resize(end);
+      }
+      else if (metrics_ != NULL)
+      {
+        metrics_->IncrementIntegerValue(METRICS_CACHE_HIT_COUNT, 1);
       }
     }
   }
--- a/OrthancFramework/Sources/MetricsRegistry.cpp	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancFramework/Sources/MetricsRegistry.cpp	Wed Oct 09 11:01:11 2024 +0200
@@ -139,6 +139,8 @@
 
       void Increment(const T& delta)
       {
+        time_ = GetNow();
+
         if (hasValue_)
         {
           value_ += delta;
@@ -146,6 +148,7 @@
         else
         {
           value_ = delta;
+          hasValue_ = true;
         }
       }
 
--- a/OrthancFramework/Sources/SQLite/OrthancSQLiteException.h	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancFramework/Sources/SQLite/OrthancSQLiteException.h	Wed Oct 09 11:01:11 2024 +0200
@@ -129,7 +129,7 @@
             return "SQLite: Cannot step over a cached statement";
 
           case ErrorCode_SQLiteBindOutOfRange:
-            return "SQLite: Bing a value while out of range (serious error)";
+            return "SQLite: Bind a value while out of range (serious error)";
 
           case ErrorCode_SQLitePrepareStatement:
             return "SQLite: Cannot prepare a cached statement";
--- a/OrthancFramework/UnitTestsSources/BitbucketCACertificates.h	Wed Sep 04 10:54:00 2024 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,30 +0,0 @@
-#define BITBUCKET_CERTIFICATES  \
-"-----BEGIN CERTIFICATE-----\n"  \
-"MIIEtjCCA56gAwIBAgIQDHmpRLCMEZUgkmFf4msdgzANBgkqhkiG9w0BAQsFADBs\n"  \
-"MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\n"  \
-"d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j\n"  \
-"ZSBFViBSb290IENBMB4XDTEzMTAyMjEyMDAwMFoXDTI4MTAyMjEyMDAwMFowdTEL\n"  \
-"MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3\n"  \
-"LmRpZ2ljZXJ0LmNvbTE0MDIGA1UEAxMrRGlnaUNlcnQgU0hBMiBFeHRlbmRlZCBW\n"  \
-"YWxpZGF0aW9uIFNlcnZlciBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC\n"  \
-"ggEBANdTpARR+JmmFkhLZyeqk0nQOe0MsLAAh/FnKIaFjI5j2ryxQDji0/XspQUY\n"  \
-"uD0+xZkXMuwYjPrxDKZkIYXLBxA0sFKIKx9om9KxjxKws9LniB8f7zh3VFNfgHk/\n"  \
-"LhqqqB5LKw2rt2O5Nbd9FLxZS99RStKh4gzikIKHaq7q12TWmFXo/a8aUGxUvBHy\n"  \
-"/Urynbt/DvTVvo4WiRJV2MBxNO723C3sxIclho3YIeSwTQyJ3DkmF93215SF2AQh\n"  \
-"cJ1vb/9cuhnhRctWVyh+HA1BV6q3uCe7seT6Ku8hI3UarS2bhjWMnHe1c63YlC3k\n"  \
-"8wyd7sFOYn4XwHGeLN7x+RAoGTMCAwEAAaOCAUkwggFFMBIGA1UdEwEB/wQIMAYB\n"  \
-"Af8CAQAwDgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEF\n"  \
-"BQcDAjA0BggrBgEFBQcBAQQoMCYwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRp\n"  \
-"Z2ljZXJ0LmNvbTBLBgNVHR8ERDBCMECgPqA8hjpodHRwOi8vY3JsNC5kaWdpY2Vy\n"  \
-"dC5jb20vRGlnaUNlcnRIaWdoQXNzdXJhbmNlRVZSb290Q0EuY3JsMD0GA1UdIAQ2\n"  \
-"MDQwMgYEVR0gADAqMCgGCCsGAQUFBwIBFhxodHRwczovL3d3dy5kaWdpY2VydC5j\n"  \
-"b20vQ1BTMB0GA1UdDgQWBBQ901Cl1qCt7vNKYApl0yHU+PjWDzAfBgNVHSMEGDAW\n"  \
-"gBSxPsNpA/i/RwHUmCYaCALvY2QrwzANBgkqhkiG9w0BAQsFAAOCAQEAnbbQkIbh\n"  \
-"hgLtxaDwNBx0wY12zIYKqPBKikLWP8ipTa18CK3mtlC4ohpNiAexKSHc59rGPCHg\n"  \
-"4xFJcKx6HQGkyhE6V6t9VypAdP3THYUYUN9XR3WhfVUgLkc3UHKMf4Ib0mKPLQNa\n"  \
-"2sPIoc4sUqIAY+tzunHISScjl2SFnjgOrWNoPLpSgVh5oywM395t6zHyuqB8bPEs\n"  \
-"1OG9d4Q3A84ytciagRpKkk47RpqF/oOi+Z6Mo8wNXrM9zwR4jxQUezKcxwCmXMS1\n"  \
-"oVWNWlZopCJwqjyBcdmdqEU79OX2olHdx3ti6G8MdOu42vi/hw15UJGQmxg7kVkn\n"  \
-"8TUoE6smftX3eg==\n"  \
-"-----END CERTIFICATE-----\n"  \
-"\n" 
--- a/OrthancFramework/UnitTestsSources/DicomMapTests.cpp	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancFramework/UnitTestsSources/DicomMapTests.cpp	Wed Oct 09 11:01:11 2024 +0200
@@ -148,10 +148,10 @@
 
   TEST_F(DicomMapMainTagsTests, Signatures)
   {
-    std::string defaultPatientSignature = DicomMap::GetDefaultMainDicomTagsSignature(ResourceType_Patient);
-    std::string defaultStudySignature = DicomMap::GetDefaultMainDicomTagsSignature(ResourceType_Study);
-    std::string defaultSeriesSignature = DicomMap::GetDefaultMainDicomTagsSignature(ResourceType_Series);
-    std::string defaultInstanceSignature = DicomMap::GetDefaultMainDicomTagsSignature(ResourceType_Instance);
+    std::string defaultPatientSignature = DicomMap::GetDefaultMainDicomTagsSignatureFrom1_11(ResourceType_Patient);
+    std::string defaultStudySignature = DicomMap::GetDefaultMainDicomTagsSignatureFrom1_11(ResourceType_Study);
+    std::string defaultSeriesSignature = DicomMap::GetDefaultMainDicomTagsSignatureFrom1_11(ResourceType_Series);
+    std::string defaultInstanceSignature = DicomMap::GetDefaultMainDicomTagsSignatureFrom1_11(ResourceType_Instance);
 
     ASSERT_NE(defaultInstanceSignature, defaultPatientSignature);
     ASSERT_NE(defaultSeriesSignature, defaultStudySignature);
@@ -162,11 +162,11 @@
     std::string seriesSignature = DicomMap::GetMainDicomTagsSignature(ResourceType_Series);
     std::string instanceSignature = DicomMap::GetMainDicomTagsSignature(ResourceType_Instance);
 
-    // at start, default and current signature should be equal
-    ASSERT_EQ(defaultPatientSignature, patientSignature);
-    ASSERT_EQ(defaultStudySignature, studySignature);
-    ASSERT_EQ(defaultSeriesSignature, seriesSignature);
-    ASSERT_EQ(defaultInstanceSignature, instanceSignature);
+    // // at start, default and current signature should be equal  !! This is not true anymore since we have added new MainDicomTags in 1.12.5
+    // ASSERT_EQ(defaultPatientSignature, patientSignature);
+    // ASSERT_EQ(defaultStudySignature, studySignature);
+    // ASSERT_EQ(defaultSeriesSignature, seriesSignature);
+    // ASSERT_EQ(defaultInstanceSignature, instanceSignature);
 
     DicomMap::AddMainDicomTag(DICOM_TAG_BITS_ALLOCATED, ResourceType_Instance);
     instanceSignature = DicomMap::GetMainDicomTagsSignature(ResourceType_Instance);
@@ -266,6 +266,7 @@
     if (level == ResourceType_Study &&
         (*it == DicomTag(0x0008, 0x0080) ||  /* InstitutionName, from Visit identification module, related to Visit */
          *it == DicomTag(0x0032, 0x1032) ||  /* RequestingPhysician, from Imaging Service Request module, related to Study */
+         *it == DicomTag(0x0008, 0x0201) ||  /* TimezoneOffsetFromUTC */
          *it == DicomTag(0x0032, 0x1060)))   /* RequestedProcedureDescription, from Requested Procedure module, related to Study */
     {
       ok = true;
@@ -284,6 +285,7 @@
          *it == DicomTag(0x0054, 0x0101) ||  /* NumberOfTimeSlices, from PET Series module */
          *it == DicomTag(0x0054, 0x1000) ||  /* SeriesType, from PET Series module */
          *it == DicomTag(0x0018, 0x1400) ||  /* AcquisitionDeviceProcessingDescription, from CR/X-Ray/DX/WholeSlideMicro Image (SIMPLIFICATION => Series) */
+         *it == DicomTag(0x0008, 0x0201) ||  /* TimezoneOffsetFromUTC */
          *it == DicomTag(0x0018, 0x0010)))   /* ContrastBolusAgent, from Contrast/Bolus module (SIMPLIFICATION => Series) */
     {
       ok = true;
@@ -1085,7 +1087,7 @@
     std::set<DicomTag> tags;
     m.GetTags(tags);
 
-    // This corresponds to the values of DEFAULT_PATIENT_MAIN_DICOM_TAGS
+    // This corresponds to the values of DEFAULT_1_11_PATIENT_MAIN_DICOM_TAGS
     ASSERT_EQ(5u, tags.size());
     ASSERT_EQ("", m.GetStringValue(DICOM_TAG_PATIENT_ID, "nope", false));
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/UnitTestsSources/GithubCACertificates.h	Wed Oct 09 11:01:11 2024 +0200
@@ -0,0 +1,30 @@
+#define GITHUB_CERTIFICATES  \
+"-----BEGIN CERTIFICATE-----\n"  \
+"MIIEyDCCA7CgAwIBAgIQDPW9BitWAvR6uFAsI8zwZjANBgkqhkiG9w0BAQsFADBh\n"  \
+"MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\n"  \
+"d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH\n"  \
+"MjAeFw0yMTAzMzAwMDAwMDBaFw0zMTAzMjkyMzU5NTlaMFkxCzAJBgNVBAYTAlVT\n"  \
+"MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxMzAxBgNVBAMTKkRpZ2lDZXJ0IEdsb2Jh\n"  \
+"bCBHMiBUTFMgUlNBIFNIQTI1NiAyMDIwIENBMTCCASIwDQYJKoZIhvcNAQEBBQAD\n"  \
+"ggEPADCCAQoCggEBAMz3EGJPprtjb+2QUlbFbSd7ehJWivH0+dbn4Y+9lavyYEEV\n"  \
+"cNsSAPonCrVXOFt9slGTcZUOakGUWzUb+nv6u8W+JDD+Vu/E832X4xT1FE3LpxDy\n"  \
+"FuqrIvAxIhFhaZAmunjZlx/jfWardUSVc8is/+9dCopZQ+GssjoP80j812s3wWPc\n"  \
+"3kbW20X+fSP9kOhRBx5Ro1/tSUZUfyyIxfQTnJcVPAPooTncaQwywa8WV0yUR0J8\n"  \
+"osicfebUTVSvQpmowQTCd5zWSOTOEeAqgJnwQ3DPP3Zr0UxJqyRewg2C/Uaoq2yT\n"  \
+"zGJSQnWS+Jr6Xl6ysGHlHx+5fwmY6D36g39HaaECAwEAAaOCAYIwggF+MBIGA1Ud\n"  \
+"EwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFHSFgMBmx9833s+9KTeqAx2+7c0XMB8G\n"  \
+"A1UdIwQYMBaAFE4iVCAYlebjbuYP+vq5Eu0GF485MA4GA1UdDwEB/wQEAwIBhjAd\n"  \
+"BgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwdgYIKwYBBQUHAQEEajBoMCQG\n"  \
+"CCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQAYIKwYBBQUHMAKG\n"  \
+"NGh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbFJvb3RH\n"  \
+"Mi5jcnQwQgYDVR0fBDswOTA3oDWgM4YxaHR0cDovL2NybDMuZGlnaWNlcnQuY29t\n"  \
+"L0RpZ2lDZXJ0R2xvYmFsUm9vdEcyLmNybDA9BgNVHSAENjA0MAsGCWCGSAGG/WwC\n"  \
+"ATAHBgVngQwBATAIBgZngQwBAgEwCAYGZ4EMAQICMAgGBmeBDAECAzANBgkqhkiG\n"  \
+"9w0BAQsFAAOCAQEAkPFwyyiXaZd8dP3A+iZ7U6utzWX9upwGnIrXWkOH7U1MVl+t\n"  \
+"wcW1BSAuWdH/SvWgKtiwla3JLko716f2b4gp/DA/JIS7w7d7kwcsr4drdjPtAFVS\n"  \
+"slme5LnQ89/nD/7d+MS5EHKBCQRfz5eeLjJ1js+aWNJXMX43AYGyZm0pGrFmCW3R\n"  \
+"bpD0ufovARTFXFZkAdl9h6g4U5+LXUZtXMYnhIHUfoyMo5tS58aI7Dd8KvvwVVo4\n"  \
+"chDYABPPTHPbqjc1qCmBaZx2vN4Ye5DUys/vZwP9BFohFrH/6j/f3IL16/RZkiMN\n"  \
+"JCqVJUzKoZHm1Lesh3Sz8W2jmdv51b2EQJ8HmA==\n"  \
+"-----END CERTIFICATE-----\n"  \
+"\n" 
--- a/OrthancFramework/UnitTestsSources/RestApiTests.cpp	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancFramework/UnitTestsSources/RestApiTests.cpp	Wed Oct 09 11:01:11 2024 +0200
@@ -98,20 +98,20 @@
    
    (1) We retrieve the URI of the root CA of BitBucket:
 
-   # echo | openssl s_client -servername bitbucket.org -connect bitbucket.org:443 2>/dev/null | openssl x509 -text | grep "CA Issuers"
+   # echo | openssl s_client -servername raw.githubusercontent.com -connect raw.githubusercontent.com:443 2>/dev/null | openssl x509 -text | grep "CA Issuers"
 
    (2) Once we get the URL to the CA certificate, we convert it to a C
    macro that can be used by libcurl:
 
    # cd UnitTestsSources
-   # python2 ../Resources/RetrieveCACertificates.py BITBUCKET_CERTIFICATES http://cacerts.digicert.com/DigiCertSHA2ExtendedValidationServerCA.crt > BitbucketCACertificates.h
+   # python2 ../Resources/RetrieveCACertificates.py GITHUB_CERTIFICATES http://cacerts.digicert.com/DigiCertGlobalG2TLSRSASHA2562020CA1-1.crt > GithubCACertificates.h
 **/
 
-#include "BitbucketCACertificates.h"
+#include "GithubCACertificates.h"
 
 TEST(HttpClient, Ssl)
 {
-  SystemToolbox::WriteFile(BITBUCKET_CERTIFICATES, "UnitTestsResults/bitbucket.cert");
+  SystemToolbox::WriteFile(GITHUB_CERTIFICATES, "UnitTestsResults/github.cert");
 
   /*{
     std::string s;
@@ -122,12 +122,12 @@
   HttpClient c;
   //c.SetVerbose(true);
   c.SetHttpsVerifyPeers(true);
-  c.SetHttpsCACertificates("UnitTestsResults/bitbucket.cert");
+  c.SetHttpsCACertificates("UnitTestsResults/github.cert");
 
   // Test file modified on 2020-04-20, in order to use a git
   // repository on BitBucket instead of a Mercurial repository
   // (because Mercurial support disappears on 2020-05-31)
-  c.SetUrl("https://bitbucket.org/osimis/orthanc-setup-samples/raw/master/docker/serve-folders/orthanc/serve-folders.json");
+  c.SetUrl("https://raw.githubusercontent.com/orthanc-server/orthanc-setup-samples/refs/heads/master/docker/serve-folders/orthanc/serve-folders.json");
 
   Json::Value v;
   c.Apply(v);
@@ -138,7 +138,7 @@
 {
   HttpClient c;
   c.SetHttpsVerifyPeers(false);
-  c.SetUrl("https://bitbucket.org/osimis/orthanc-setup-samples/raw/master/docker/serve-folders/orthanc/serve-folders.json");
+  c.SetUrl("https://raw.githubusercontent.com/orthanc-server/orthanc-setup-samples/refs/heads/master/docker/serve-folders/orthanc/serve-folders.json");
 
   Json::Value v;
   c.Apply(v);
--- a/OrthancServer/CMakeLists.txt	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/CMakeLists.txt	Wed Oct 09 11:01:11 2024 +0200
@@ -125,7 +125,9 @@
   ${CMAKE_SOURCE_DIR}/Sources/OrthancWebDav.cpp
   ${CMAKE_SOURCE_DIR}/Sources/QueryRetrieveHandler.cpp
   ${CMAKE_SOURCE_DIR}/Sources/ResourceFinder.cpp
-  ${CMAKE_SOURCE_DIR}/Sources/Search/DatabaseConstraint.cpp
+  ${CMAKE_SOURCE_DIR}/Sources/Search/DatabaseDicomTagConstraint.cpp
+  ${CMAKE_SOURCE_DIR}/Sources/Search/DatabaseDicomTagConstraints.cpp
+  ${CMAKE_SOURCE_DIR}/Sources/Search/DatabaseMetadataConstraint.cpp
   ${CMAKE_SOURCE_DIR}/Sources/Search/DatabaseLookup.cpp
   ${CMAKE_SOURCE_DIR}/Sources/Search/DicomTagConstraint.cpp
   ${CMAKE_SOURCE_DIR}/Sources/Search/HierarchicalMatcher.cpp
@@ -332,7 +334,6 @@
 
 add_definitions(
   -DORTHANC_BUILD_UNIT_TESTS=1
-  -DORTHANC_BUILDING_SERVER_LIBRARY=1
 
   # Macros for the plugins
   -DHAS_ORTHANC_EXCEPTION=0
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp	Wed Oct 09 11:01:11 2024 +0200
@@ -564,7 +564,7 @@
     
     virtual void ApplyLookupResources(std::list<std::string>& resourcesId,
                                       std::list<std::string>* instancesId,
-                                      const DatabaseConstraints& lookup,
+                                      const DatabaseDicomTagConstraints& lookup,
                                       ResourceType queryLevel,
                                       const std::set<std::string>& labels,
                                       LabelsConstraint labelsConstraint,
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp	Wed Oct 09 11:01:11 2024 +0200
@@ -798,7 +798,7 @@
     
     virtual void ApplyLookupResources(std::list<std::string>& resourcesId,
                                       std::list<std::string>* instancesId, // Can be NULL if not needed
-                                      const DatabaseConstraints& lookup,
+                                      const DatabaseDicomTagConstraints& lookup,
                                       ResourceType queryLevel,
                                       const std::set<std::string>& labels,
                                       LabelsConstraint labelsConstraint,
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp	Wed Oct 09 11:01:11 2024 +0200
@@ -36,6 +36,7 @@
 #include "../../Sources/Database/VoidDatabaseListener.h"
 #include "../../Sources/ServerToolbox.h"
 #include "PluginsEnumerations.h"
+#include "../../Sources/Database/MainDicomTagsRegistry.h"
 
 #include "OrthancDatabasePlugin.pb.h"  // Auto-generated file
 
@@ -136,7 +137,7 @@
 
 
   static void Convert(DatabasePluginMessages::DatabaseConstraint& target,
-                      const DatabaseConstraint& source)
+                      const DatabaseDicomTagConstraint& source)
   {
     target.set_level(Convert(source.GetLevel()));
     target.set_tag_group(source.GetTag().GetGroup());
@@ -179,6 +180,94 @@
   }
 
 
+  static void Convert(DatabasePluginMessages::DatabaseMetadataConstraint& target,
+                      const DatabaseMetadataConstraint& source)
+  {
+    target.set_metadata(source.GetMetadata());
+    target.set_is_case_sensitive(source.IsCaseSensitive());
+    target.set_is_mandatory(source.IsMandatory());
+
+    target.mutable_values()->Reserve(source.GetValuesCount());
+    for (size_t j = 0; j < source.GetValuesCount(); j++)
+    {
+      target.add_values(source.GetValue(j));
+    }
+
+    switch (source.GetConstraintType())
+    {
+      case ConstraintType_Equal:
+        target.set_type(DatabasePluginMessages::CONSTRAINT_EQUAL);
+        break;
+
+      case ConstraintType_SmallerOrEqual:
+        target.set_type(DatabasePluginMessages::CONSTRAINT_SMALLER_OR_EQUAL);
+        break;
+
+      case ConstraintType_GreaterOrEqual:
+        target.set_type(DatabasePluginMessages::CONSTRAINT_GREATER_OR_EQUAL);
+        break;
+
+      case ConstraintType_Wildcard:
+        target.set_type(DatabasePluginMessages::CONSTRAINT_WILDCARD);
+        break;
+
+      case ConstraintType_List:
+        target.set_type(DatabasePluginMessages::CONSTRAINT_LIST);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  static void Convert(DatabasePluginMessages::Find_Request_Ordering& target,
+                      const FindRequest::Ordering& source)
+  {
+    switch (source.GetKeyType())
+    {
+      case FindRequest::KeyType_DicomTag:
+      {
+        ResourceType tagLevel;
+        DicomTagType tagType;
+        MainDicomTagsRegistry registry;
+
+        registry.LookupTag(tagLevel, tagType, source.GetDicomTag());
+
+        target.set_key_type(DatabasePluginMessages::ORDERING_KEY_TYPE_DICOM_TAG);
+        target.set_tag_group(source.GetDicomTag().GetGroup());
+        target.set_tag_element(source.GetDicomTag().GetElement());
+        target.set_is_identifier_tag(tagType == DicomTagType_Identifier);
+        target.set_tag_level(Convert(tagLevel));
+
+      }; break;
+
+      case FindRequest::KeyType_Metadata:
+        target.set_key_type(DatabasePluginMessages::ORDERING_KEY_TYPE_METADATA);
+        target.set_metadata(source.GetMetadataType());
+
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    switch (source.GetDirection())
+    {
+      case FindRequest::OrderingDirection_Ascending:
+        target.set_direction(DatabasePluginMessages::ORDERING_DIRECTION_ASC);
+        break;
+
+      case FindRequest::OrderingDirection_Descending:
+        target.set_direction(DatabasePluginMessages::ORDERING_DIRECTION_DESC);
+
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
   static DatabasePluginMessages::LabelsConstraintType Convert(LabelsConstraint constraint)
   {
     switch (constraint)
@@ -626,6 +715,37 @@
       }
     }
 
+    virtual void GetChangesExtended(std::list<ServerIndexChange>& target /*out*/,
+                                    bool& done /*out*/,
+                                    int64_t since,
+                                    int64_t to,
+                                    uint32_t limit,
+                                    const std::set<ChangeType>& changeTypes) ORTHANC_OVERRIDE
+    {
+      assert(database_.GetDatabaseCapabilities().HasExtendedChanges());
+
+      DatabasePluginMessages::TransactionRequest request;
+      DatabasePluginMessages::TransactionResponse response;
+
+      request.mutable_get_changes_extended()->set_since(since);
+      request.mutable_get_changes_extended()->set_limit(limit);
+      request.mutable_get_changes_extended()->set_to(to);
+      for (std::set<ChangeType>::const_iterator it = changeTypes.begin(); it != changeTypes.end(); ++it)
+      {
+        request.mutable_get_changes_extended()->add_change_type(*it);
+      }
+      
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_CHANGES_EXTENDED, request);
+
+      done = response.get_changes_extended().done();
+
+      target.clear();
+      for (int i = 0; i < response.get_changes_extended().changes().size(); i++)
+      {
+        target.push_back(Convert(response.get_changes_extended().changes(i)));
+      }
+    }
+
     
     virtual void GetChildrenInternalId(std::list<int64_t>& target,
                                        int64_t id) ORTHANC_OVERRIDE
@@ -1106,7 +1226,7 @@
 
     virtual void ApplyLookupResources(std::list<std::string>& resourcesId,
                                       std::list<std::string>* instancesId, // Can be NULL if not needed
-                                      const DatabaseConstraints& lookup,
+                                      const DatabaseDicomTagConstraints& lookup,
                                       ResourceType queryLevel,
                                       const std::set<std::string>& labels,
                                       LabelsConstraint labelsConstraint,
@@ -1389,6 +1509,16 @@
           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::deque<FindRequest::Ordering*>::const_iterator it = request.GetOrdering().begin(); it != request.GetOrdering().end(); ++it)
+        {
+          Convert(*dbRequest.mutable_find()->add_ordering(), *(*it)); 
+        }
+
         if (request.HasLimits())
         {
           dbRequest.mutable_find()->mutable_limits()->set_since(request.GetLimitsSince());
@@ -1402,15 +1532,20 @@
 
         dbRequest.mutable_find()->set_labels_constraint(Convert(request.GetLabelsConstraint()));
 
-        // TODO-FIND: ordering_
-        // TODO-FIND: metadataConstraints__
-
         dbRequest.mutable_find()->set_retrieve_main_dicom_tags(request.IsRetrieveMainDicomTags());
         dbRequest.mutable_find()->set_retrieve_metadata(request.IsRetrieveMetadata());
         dbRequest.mutable_find()->set_retrieve_labels(request.IsRetrieveLabels());
         dbRequest.mutable_find()->set_retrieve_attachments(request.IsRetrieveAttachments());
         dbRequest.mutable_find()->set_retrieve_parent_identifier(request.IsRetrieveParentIdentifier());
-        dbRequest.mutable_find()->set_retrieve_at_least_one_instance(request.IsRetrieveOneInstanceIdentifier());
+
+        if (request.GetLevel() == ResourceType_Instance)
+        {
+          dbRequest.mutable_find()->set_retrieve_one_instance_metadata_and_attachments(false);
+        }
+        else
+        {
+          dbRequest.mutable_find()->set_retrieve_one_instance_metadata_and_attachments(request.IsRetrieveOneInstanceMetadataAndAttachments());
+        }
 
         if (request.GetLevel() == ResourceType_Study ||
             request.GetLevel() == ResourceType_Series ||
@@ -1498,26 +1633,59 @@
 
           if (request.GetLevel() == ResourceType_Patient)
           {
-            Convert(*target, ResourceType_Patient, source.children_studies_content());
+            Convert(*target, ResourceType_Study, source.children_studies_content());
           }
 
           if (request.GetLevel() == ResourceType_Patient ||
               request.GetLevel() == ResourceType_Study)
           {
-            Convert(*target, ResourceType_Study, source.children_series_content());
+            Convert(*target, ResourceType_Series, source.children_series_content());
           }
 
           if (request.GetLevel() == ResourceType_Patient ||
               request.GetLevel() == ResourceType_Study ||
               request.GetLevel() == ResourceType_Series)
           {
-            Convert(*target, ResourceType_Series, source.children_instances_content());
+            Convert(*target, ResourceType_Instance, source.children_instances_content());
+          }
+
+          if (request.GetLevel() != ResourceType_Instance &&
+              request.IsRetrieveOneInstanceMetadataAndAttachments())
+          {
+            std::map<MetadataType, std::string> metadata;
+            for (int i = 0; i < source.one_instance_metadata().size(); i++)
+            {
+              MetadataType key = static_cast<MetadataType>(source.one_instance_metadata(i).key());
+              if (metadata.find(key) == metadata.end())
+              {
+                metadata[key] = source.one_instance_metadata(i).value();
+              }
+              else
+              {
+                throw OrthancException(ErrorCode_DatabasePlugin);
+              }
+            }
+
+            std::map<FileContentType, FileInfo> attachments;
+
+            for (int i = 0; i < source.one_instance_attachments().size(); i++)
+            {
+              FileInfo info(Convert(source.one_instance_attachments(i)));
+              if (attachments.find(info.GetContentType()) == attachments.end())
+              {
+                attachments[info.GetContentType()] = info;
+              }
+              else
+              {
+                throw OrthancException(ErrorCode_DatabasePlugin);
+              }
+            }
+
+            target->SetOneInstanceMetadataAndAttachments(source.one_instance_public_id(), metadata, attachments);
           }
 
           response.Add(target.release());
         }
-
-        throw OrthancException(ErrorCode_NotImplemented);
       }
       else
       {
@@ -1647,9 +1815,8 @@
       dbCapabilities_.SetAtomicIncrementGlobalProperty(systemInfo.supports_increment_global_property());
       dbCapabilities_.SetUpdateAndGetStatistics(systemInfo.has_update_and_get_statistics());
       dbCapabilities_.SetMeasureLatency(systemInfo.has_measure_latency());
+      dbCapabilities_.SetHasExtendedChanges(systemInfo.has_extended_changes());
       dbCapabilities_.SetHasFindSupport(systemInfo.supports_find());
-
-      printf(">>> %d\n", dbCapabilities_.HasFindSupport());
     }
 
     open_ = true;
--- a/OrthancServer/Plugins/Engine/PluginsEnumerations.cpp	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/Plugins/Engine/PluginsEnumerations.cpp	Wed Oct 09 11:01:11 2024 +0200
@@ -595,5 +595,74 @@
           throw OrthancException(ErrorCode_ParameterOutOfRange);
       }
     }
+
+
+    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);
+      }
+    }
+
+
+    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);
+      }
+    }
   }
 }
--- a/OrthancServer/Plugins/Engine/PluginsEnumerations.h	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/Plugins/Engine/PluginsEnumerations.h	Wed Oct 09 11:01:11 2024 +0200
@@ -25,15 +25,7 @@
 
 #if ORTHANC_ENABLE_PLUGINS == 1
 
-/**
- * NB: Conversions to/from "OrthancPluginConstraintType" and
- * "OrthancPluginResourceType" are located in file
- * "../../Sources/Search/DatabaseConstraint.h" to be shared with the
- * "orthanc-databases" project.
- **/
-
 #include "../../../OrthancFramework/Sources/MetricsRegistry.h"
-#include "../../Sources/Search/DatabaseConstraint.h"
 #include "../../Sources/ServerEnumerations.h"
 #include "../Include/orthanc/OrthancCPlugin.h"
 
@@ -75,6 +67,12 @@
     StorageCommitmentFailureReason Convert(OrthancPluginStorageCommitmentFailureReason reason);
 
     MetricsUpdatePolicy Convert(OrthancPluginMetricsType type);
+
+    OrthancPluginResourceType Convert(ResourceType type);
+
+    ResourceType Convert(OrthancPluginResourceType type);
+
+    OrthancPluginConstraintType Convert(ConstraintType constraint);
   }
 }
 
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Wed Oct 09 11:01:11 2024 +0200
@@ -262,6 +262,7 @@
     OrthancPluginErrorCode_MainDicomTagsMultiplyDefined = 44    /*!< A main DICOM Tag has been defined multiple times for the same resource level */,
     OrthancPluginErrorCode_ForbiddenAccess = 45    /*!< Access to a resource is forbidden */,
     OrthancPluginErrorCode_DuplicateResource = 46    /*!< Duplicate resource */,
+    OrthancPluginErrorCode_IncompatibleConfigurations = 47    /*!< Your configuration file contains configuration that are mutually incompatible */,
     OrthancPluginErrorCode_SQLiteNotOpened = 1000    /*!< SQLite: The database is not opened */,
     OrthancPluginErrorCode_SQLiteAlreadyOpened = 1001    /*!< SQLite: Connection is already open */,
     OrthancPluginErrorCode_SQLiteCannotOpen = 1002    /*!< SQLite: Unable to open the database */,
@@ -273,7 +274,7 @@
     OrthancPluginErrorCode_SQLiteFlush = 1008    /*!< SQLite: Unable to flush the database */,
     OrthancPluginErrorCode_SQLiteCannotRun = 1009    /*!< SQLite: Cannot run a cached statement */,
     OrthancPluginErrorCode_SQLiteCannotStep = 1010    /*!< SQLite: Cannot step over a cached statement */,
-    OrthancPluginErrorCode_SQLiteBindOutOfRange = 1011    /*!< SQLite: Bing a value while out of range (serious error) */,
+    OrthancPluginErrorCode_SQLiteBindOutOfRange = 1011    /*!< SQLite: Bind a value while out of range (serious error) */,
     OrthancPluginErrorCode_SQLitePrepareStatement = 1012    /*!< SQLite: Cannot prepare a cached statement */,
     OrthancPluginErrorCode_SQLiteTransactionAlreadyStarted = 1013    /*!< SQLite: Beginning the same transaction twice */,
     OrthancPluginErrorCode_SQLiteTransactionCommit = 1014    /*!< SQLite: Failure when committing the transaction */,
@@ -748,6 +749,7 @@
 
   /**
    * The supported types of changes that can be signaled to the change callback.
+   * Note: this enum is not used to store changes in the DB !
    * @ingroup Callbacks
    **/
   typedef enum
--- a/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto	Wed Oct 09 11:01:11 2024 +0200
@@ -78,6 +78,16 @@
   LABELS_CONSTRAINT_NONE = 2;
 }
 
+enum OrderingKeyType {
+  ORDERING_KEY_TYPE_DICOM_TAG = 0;
+  ORDERING_KEY_TYPE_METADATA = 1;
+}
+
+enum OrderingDirection {
+  ORDERING_DIRECTION_ASC = 0;
+  ORDERING_DIRECTION_DESC = 1;
+}
+
 message ServerIndexChange {
   int64         seq = 1;
   int32         change_type = 2;   // opaque "ChangeType" in Orthanc
@@ -109,6 +119,13 @@
   repeated string  values = 8;
 }
 
+message DatabaseMetadataConstraint {
+  int32            metadata = 1;
+  bool             is_case_sensitive = 2;
+  bool             is_mandatory = 3;
+  ConstraintType   type = 4;
+  repeated string  values = 5;
+}
 
 /**
  * Database-level operations.
@@ -142,6 +159,7 @@
     bool has_update_and_get_statistics = 6;
     bool has_measure_latency = 7;
     bool supports_find = 8;         // New in Orthanc 1.12.5
+    bool has_extended_changes = 9;  // New in Orthanc 1.12.5
   }
 }
 
@@ -295,6 +313,7 @@
   OPERATION_INCREMENT_GLOBAL_PROPERTY = 48;   // New in Orthanc 1.12.3
   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
 }
 
 message Rollback {
@@ -415,6 +434,19 @@
   }
 }
 
+message GetChangesExtended {
+  message Request {
+    int64 since = 1;
+    int64 to = 2;
+    repeated int32 change_type = 3;
+    uint32 limit = 4;
+  }
+  message Response {
+    repeated ServerIndexChange changes = 1;
+    bool done = 2;
+  }
+}
+
 message GetChildrenInternalId {
   message Request {
     int64 id = 1;
@@ -845,6 +877,15 @@
       repeated int32 retrieve_metadata = 2;
       repeated Tag retrieve_main_dicom_tags = 3;
     }
+    message Ordering {
+      OrderingKeyType key_type = 1;
+      OrderingDirection direction = 2;
+      uint32 tag_group = 3;
+      uint32 tag_element = 4;
+      bool is_identifier_tag = 5;
+      ResourceType tag_level = 6;
+      int32 metadata = 7;
+    }
 
     // Part 1 of the request: Constraints
     ResourceType level = 1;
@@ -856,9 +897,9 @@
     Limits limits = 7;               // optional
     repeated string labels = 8;
     LabelsConstraintType labels_constraint = 9;
+    repeated Ordering ordering = 10;
+    repeated DatabaseMetadataConstraint metadata_constraints = 11;
 
-    // TODO-FIND: ordering_
-    // TODO-FIND: metadataConstraints_
 
     // Part 2 of the request: What is to be retrieved
     bool retrieve_main_dicom_tags = 100;
@@ -866,7 +907,7 @@
     bool retrieve_labels = 102;
     bool retrieve_attachments = 103;
     bool retrieve_parent_identifier = 104;
-    bool retrieve_at_least_one_instance = 105;
+    bool retrieve_one_instance_metadata_and_attachments = 105;
     ParentSpecification parent_patient = 106;
     ParentSpecification parent_study = 107;
     ParentSpecification parent_series = 108;
@@ -916,6 +957,9 @@
     ChildrenContent children_studies_content = 10;
     ChildrenContent children_series_content = 11;
     ChildrenContent children_instances_content = 12;
+    string one_instance_public_id = 13;
+    repeated Metadata one_instance_metadata = 14;
+    repeated FileInfo one_instance_attachments = 15;
   }
 }
 
@@ -974,6 +1018,7 @@
   IncrementGlobalProperty.Request         increment_global_property = 148;
   UpdateAndGetStatistics.Request          update_and_get_statistics = 149;
   Find.Request                            find = 150;
+  GetChangesExtended.Request              get_changes_extended = 151;
 }
 
 message TransactionResponse {
@@ -1028,6 +1073,7 @@
   IncrementGlobalProperty.Response         increment_global_property = 148;
   UpdateAndGetStatistics.Response          update_and_get_statistics = 149;
   repeated Find.Response                   find = 150;   // One message per found resources
+  GetChangesExtended.Response              get_changes_extended = 151;
 }
 
 enum RequestType {
--- a/OrthancServer/Plugins/Samples/DelayedDeletion/CMakeLists.txt	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/Plugins/Samples/DelayedDeletion/CMakeLists.txt	Wed Oct 09 11:01:11 2024 +0200
@@ -60,7 +60,6 @@
   -DORTHANC_PLUGIN_VERSION="${PLUGIN_VERSION}"
   -DORTHANC_ENABLE_LOGGING=1
   -DORTHANC_ENABLE_PLUGINS=1
-  -DORTHANC_BUILDING_SERVER_LIBRARY=0
   )
 
 include_directories(
--- a/OrthancServer/Resources/Configuration.json	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/Resources/Configuration.json	Wed Oct 09 11:01:11 2024 +0200
@@ -985,12 +985,33 @@
     // saved with another "ExtraMainDicomTags" configuration which means that
     // your response might be incomplete/inconsistent.
     // You should call patients|studies|series|instances/../reconstruct to rebuild
-    // the DB.  You may also check for the "Housekeeper" plugin
+    // the DB.  You may also check for the "Housekeeper" plugin.
     "W002_InconsistentDicomTagsInDb": true,
 
     // Display a warning message when Orthanc and its plugins are unable
     // to decode a frame (new in Orthanc 1.12.5).
-    "W003_DecoderFailure": true
-  }
+    "W003_DecoderFailure": true,
+
+    // Display a warning when the MainDicomTagsSignature metadata has not been
+    // found which means that the resource has been saved with a version prior
+    // to 1.11.0.
+    // You should call patients|studies|series|instances/../reconstruct to rebuild
+    // the DB.  You may also check for the "Housekeeper" plugin.
+    // (new in Orthanc 1.12.5)
+    "W004_NoMainDicomTagsSignature": true,
+
+    // Display a warning when a user performs a find request and requests a tag
+    // from a lower resource level; e.g. when requesting "StudyDescription" at
+    // Patient level.
+    // (new in Orthanc 1.12.5)
+    "W005_RequestingTagFromLowerResourceLevel": true
+  },
+
+  // Configure Orthanc in read only mode.
+  // In this mode, many Orthanc features that requires a write access to the 
+  // Index DB or the disk storage won't be available at all.
+  // (new in Orthanc 1.12.5)
+  "ReadOnly" : false
+
 
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/ImplementationNotes/DatabasesClassHierarchy.txt	Wed Oct 09 11:01:11 2024 +0200
@@ -0,0 +1,30 @@
+The main object to access the DB is the ServerIndex class that is accessible from the ServerContext.
+
+ServerIndex inherits from StatelessDatabaseOperations.
+
+StatelessDatabaseOperations owns an IDatabaseWrapper member (db).
+StatelessDatabaseOperations has 2 internal Transaction classes (ReadOnlyTransactions and ReadWriteTransactions) that implements the DB
+operations by calling the methods from IDatabaseWrapper:ITransaction.
+
+IDatabaseWrapper has 2 direct derived classes:
+- BaseDatabaseWrapper which simply provides a "not implemented" implementation of new methods to its derived classes:
+  - OrthancPluginDatabase    that is a legacy plugin interface
+  - OrthancPluginDatabaseV3  that is a legacy plugin interface
+  - SQLiteDatabaseWrapper    that is used by the default SQLite DB in Orthanc
+- OrthancPluginDatabaseV4 that is the latest plugin interface and uses protobuf
+
+When you add a new method in the DB (e.g: UpdateAndGetStatistics with a new signature), you must:
+- define it as a member of StatelessDatabaseOperations
+- define it as a member of StatelessDatabaseOperations::ReadWriteTransactions or StatelessDatabaseOperations::ReadOnlyTransactions
+- define it as a member of IDatabaseWrapper:ITransaction
+- define it in OrthancDatabasePlugin.proto (new request + new response + new message)
+- define it in OrthancPluginDatabaseV4
+- define a NotImplemented default implementation in BaseDatabaseWrapper
+- optionally define it in SQLiteDatabaseWrapper if it can be implemented in SQLite
+- very likely define it as a DbCapabilities in IDatabaseWrapper::DbCapabilities (e.g: Has/SetUpdateAndGetStatistics()) such that the Orthanc
+  core knows if it can use it or not.
+
+Then, in the orthanc-databases repo, you should:
+- define it as a virtual member of IDatabaseBackend
+- define it as a member of IndexBackend
+- add a handler for the new protobuf message in DatabaseBackendAdapterV4
--- a/OrthancServer/Resources/RunCppCheck.sh	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/Resources/RunCppCheck.sh	Wed Oct 09 11:01:11 2024 +0200
@@ -55,7 +55,6 @@
             -DJSONCPP_VERSION_MAJOR=1 \
             -DJSONCPP_VERSION_MINOR=0 \
             -DORTHANC_BUILDING_FRAMEWORK_LIBRARY=0 \
-            -DORTHANC_BUILDING_SERVER_LIBRARY=1 \
             -DORTHANC_BUILD_UNIT_TESTS=1 \
             -DORTHANC_ENABLE_BASE64=1 \
             -DORTHANC_ENABLE_CIVETWEB=1 \
--- a/OrthancServer/Sources/Database/BaseDatabaseWrapper.cpp	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/Sources/Database/BaseDatabaseWrapper.cpp	Wed Oct 09 11:01:11 2024 +0200
@@ -46,6 +46,17 @@
     throw OrthancException(ErrorCode_NotImplemented);  // Not supported
   }
 
+  void BaseDatabaseWrapper::BaseTransaction::GetChangesExtended(std::list<ServerIndexChange>& target /*out*/,
+                                                                bool& done /*out*/,
+                                                                int64_t since,
+                                                                int64_t to,
+                                                                uint32_t limit,
+                                                                const std::set<ChangeType>& filterType)
+  {
+    throw OrthancException(ErrorCode_NotImplemented);  // Not supported
+  }
+
+
 
   void BaseDatabaseWrapper::BaseTransaction::ExecuteFind(FindResponse& response,
                                                          const FindRequest& request,
--- a/OrthancServer/Sources/Database/BaseDatabaseWrapper.h	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/Sources/Database/BaseDatabaseWrapper.h	Wed Oct 09 11:01:11 2024 +0200
@@ -60,6 +60,13 @@
                                  const Capabilities& capabilities,
                                  const FindRequest& request,
                                  const std::string& identifier) ORTHANC_OVERRIDE;
+
+      virtual void GetChangesExtended(std::list<ServerIndexChange>& target /*out*/,
+                                      bool& done /*out*/,
+                                      int64_t since,
+                                      int64_t to,
+                                      uint32_t limit,
+                                      const std::set<ChangeType>& filterType) ORTHANC_OVERRIDE;
     };
 
     virtual uint64_t MeasureLatency() ORTHANC_OVERRIDE;
--- a/OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp	Wed Oct 09 11:01:11 2024 +0200
@@ -74,7 +74,7 @@
           }
         }
         
-        void Add(const DatabaseConstraint& constraint)
+        void Add(const DatabaseDicomTagConstraint& constraint)
         {
           constraints_.push_back(new DicomTagConstraint(constraint));
         }          
@@ -84,7 +84,7 @@
     
     static void ApplyIdentifierConstraint(SetOfResources& candidates,
                                           ILookupResources& compatibility,
-                                          const DatabaseConstraint& constraint,
+                                          const DatabaseDicomTagConstraint& constraint,
                                           ResourceType level)
     {
       std::list<int64_t> matches;
@@ -134,8 +134,8 @@
     
     static void ApplyIdentifierRange(SetOfResources& candidates,
                                      ILookupResources& compatibility,
-                                     const DatabaseConstraint& smaller,
-                                     const DatabaseConstraint& greater,
+                                     const DatabaseDicomTagConstraint& smaller,
+                                     const DatabaseDicomTagConstraint& greater,
                                      ResourceType level)
     {
       assert(smaller.GetConstraintType() == ConstraintType_SmallerOrEqual &&
@@ -153,10 +153,10 @@
     static void ApplyLevel(SetOfResources& candidates,
                            IDatabaseWrapper::ITransaction& transaction,
                            ILookupResources& compatibility,
-                           const DatabaseConstraints& lookup,
+                           const DatabaseDicomTagConstraints& lookup,
                            ResourceType level)
     {
-      typedef std::set<const DatabaseConstraint*>  SetOfConstraints;
+      typedef std::set<const DatabaseDicomTagConstraint*>  SetOfConstraints;
       typedef std::map<DicomTag, SetOfConstraints> Identifiers;
 
       // (1) Select which constraints apply to this level, and split
@@ -168,7 +168,7 @@
       
       for (size_t i = 0; i < lookup.GetSize(); i++)
       {
-        const DatabaseConstraint& constraint = lookup.GetConstraint(i);
+        const DatabaseDicomTagConstraint& constraint = lookup.GetConstraint(i);
 
         if (constraint.GetLevel() == level)
         {
@@ -191,8 +191,8 @@
       {
         // Check whether some range constraint over identifiers is
         // present at this level
-        const DatabaseConstraint* smaller = NULL;
-        const DatabaseConstraint* greater = NULL;
+        const DatabaseDicomTagConstraint* smaller = NULL;
+        const DatabaseDicomTagConstraint* greater = NULL;
         
         for (SetOfConstraints::const_iterator it2 = it->second.begin();
              it2 != it->second.end(); ++it2)
@@ -308,7 +308,7 @@
 
     void DatabaseLookup::ApplyLookupResources(std::list<std::string>& resourcesId,
                                               std::list<std::string>* instancesId,
-                                              const DatabaseConstraints& lookup,
+                                              const DatabaseDicomTagConstraints& lookup,
                                               ResourceType queryLevel,
                                               size_t limit)
     {
--- a/OrthancServer/Sources/Database/Compatibility/DatabaseLookup.h	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/Sources/Database/Compatibility/DatabaseLookup.h	Wed Oct 09 11:01:11 2024 +0200
@@ -46,7 +46,7 @@
 
       void ApplyLookupResources(std::list<std::string>& resourcesId,
                                 std::list<std::string>* instancesId,
-                                const DatabaseConstraints& lookup,
+                                const DatabaseDicomTagConstraints& lookup,
                                 ResourceType queryLevel,
                                 size_t limit);
     };
--- a/OrthancServer/Sources/Database/Compatibility/GenericFind.cpp	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/Sources/Database/Compatibility/GenericFind.cpp	Wed Oct 09 11:01:11 2024 +0200
@@ -129,13 +129,30 @@
           !request.GetOrthancIdentifiers().HasSeriesId() &&
           !request.GetOrthancIdentifiers().HasInstanceId())
       {
-        if (request.HasLimits())
+        if (!request.HasLimits())
+        {
+          transaction_.GetAllPublicIds(identifiers, request.GetLevel());
+        }
+        else if (request.GetLimitsCount() != 0)
         {
           transaction_.GetAllPublicIds(identifiers, request.GetLevel(), request.GetLimitsSince(), request.GetLimitsCount());
         }
         else
         {
-          transaction_.GetAllPublicIds(identifiers, request.GetLevel());
+          // Starting with Orthanc 1.12.5, "limit=0" means "no limit"
+          std::list<std::string> tmp;
+          transaction_.GetAllPublicIds(tmp, request.GetLevel());
+
+          size_t count = 0;
+          for (std::list<std::string>::const_iterator it = tmp.begin(); it != tmp.end(); ++it)
+          {
+            if (count >= request.GetLimitsSince())
+            {
+              identifiers.push_back(*it);
+            }
+
+            count++;
+          }
         }
       }
       else if (IsRequestWithoutContraint(request) &&
@@ -401,7 +418,7 @@
 
       if (level != request.GetLevel())
       {
-        throw OrthancException(ErrorCode_DatabasePlugin);
+        throw OrthancException(ErrorCode_UnknownResource, "Wrong resource level for this ID");  // this might happen e.g if you call /instances/... with a series instance id
       }
 
       std::unique_ptr<FindResponse::Resource> resource(new FindResponse::Resource(request.GetLevel(), internalId, identifier));
@@ -566,8 +583,8 @@
         }
       }
 
-      if (request.IsRetrieveOneInstanceIdentifier() &&
-          !request.GetChildrenSpecification(ResourceType_Instance).IsRetrieveIdentifiers())
+      if (request.GetLevel() != ResourceType_Instance &&
+          request.IsRetrieveOneInstanceMetadataAndAttachments())
       {
         int64_t currentId = internalId;
         ResourceType currentLevel = level;
@@ -587,7 +604,28 @@
           }
         }
 
-        resource->AddChildIdentifier(ResourceType_Instance, transaction_.GetPublicId(currentId));
+        std::map<MetadataType, std::string> metadata;
+        transaction_.GetAllMetadata(metadata, currentId);
+
+        std::set<FileContentType> attachmentsType;
+        transaction_.ListAvailableAttachments(attachmentsType, currentId);
+
+        std::map<FileContentType, FileInfo> attachments;
+        for (std::set<FileContentType>::const_iterator it = attachmentsType.begin(); it != attachmentsType.end(); ++it)
+        {
+          FileInfo info;
+          int64_t revision;  // Unused in this case
+          if (transaction_.LookupAttachment(info, revision, currentId, *it))
+          {
+            attachments[*it] = info;
+          }
+          else
+          {
+            throw OrthancException(ErrorCode_DatabasePlugin);
+          }
+        }
+
+        resource->SetOneInstanceMetadataAndAttachments(transaction_.GetPublicId(currentId), metadata, attachments);
       }
 
       response.Add(resource.release());
--- a/OrthancServer/Sources/Database/Compatibility/ILookupResources.cpp	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/Sources/Database/Compatibility/ILookupResources.cpp	Wed Oct 09 11:01:11 2024 +0200
@@ -35,7 +35,7 @@
       ILookupResources& compatibility,
       std::list<std::string>& resourcesId,
       std::list<std::string>* instancesId,
-      const DatabaseConstraints& lookup,
+      const DatabaseDicomTagConstraints& lookup,
       ResourceType queryLevel,
       size_t limit)
     {
--- a/OrthancServer/Sources/Database/Compatibility/ILookupResources.h	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/Sources/Database/Compatibility/ILookupResources.h	Wed Oct 09 11:01:11 2024 +0200
@@ -60,7 +60,7 @@
                         ILookupResources& compatibility,
                         std::list<std::string>& resourcesId,
                         std::list<std::string>* instancesId,
-                        const DatabaseConstraints& lookup,
+                        const DatabaseDicomTagConstraints& lookup,
                         ResourceType queryLevel,
                         size_t limit);
     };
--- a/OrthancServer/Sources/Database/FindRequest.cpp	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/Sources/Database/FindRequest.cpp	Wed Oct 09 11:01:11 2024 +0200
@@ -93,15 +93,20 @@
     retrieveLabels_(false),
     retrieveAttachments_(false),
     retrieveParentIdentifier_(false),
-    retrieveOneInstanceIdentifier_(false)
+    retrieveOneInstanceMetadataAndAttachments_(false)
   {
   }
 
 
   FindRequest::~FindRequest()
   {
+    for (std::deque<Ordering*>::iterator it = ordering_.begin(); it != ordering_.end(); ++it)
+    {
+      assert(*it != NULL);
+      delete *it;
+    }
 
-    for (std::deque<Ordering*>::iterator it = ordering_.begin(); it != ordering_.end(); ++it)
+    for (std::deque<DatabaseMetadataConstraint*>::iterator it = metadataConstraints_.begin(); it != metadataConstraints_.end(); ++it)
     {
       assert(*it != NULL);
       delete *it;
@@ -233,6 +238,12 @@
   }
 
 
+  void FindRequest::AddMetadataConstraint(DatabaseMetadataConstraint* constraint)
+  {
+    metadataConstraints_.push_back(constraint);
+  }
+
+
   void FindRequest::SetRetrieveParentIdentifier(bool retrieve)
   {
     if (level_ == ResourceType_Patient)
@@ -246,7 +257,7 @@
   }
 
 
-  void FindRequest::SetRetrieveOneInstanceIdentifier(bool retrieve)
+  void FindRequest::SetRetrieveOneInstanceMetadataAndAttachments(bool retrieve)
   {
     if (level_ == ResourceType_Instance)
     {
@@ -254,7 +265,20 @@
     }
     else
     {
-      retrieveOneInstanceIdentifier_ = retrieve;
+      retrieveOneInstanceMetadataAndAttachments_ = retrieve;
+    }
+  }
+
+
+  bool FindRequest::IsRetrieveOneInstanceMetadataAndAttachments() const
+  {
+    if (level_ == ResourceType_Instance)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return retrieveOneInstanceMetadataAndAttachments_;
     }
   }
 }
--- a/OrthancServer/Sources/Database/FindRequest.h	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/Sources/Database/FindRequest.h	Wed Oct 09 11:01:11 2024 +0200
@@ -24,7 +24,8 @@
 #pragma once
 
 #include "../../../OrthancFramework/Sources/DicomFormat/DicomTag.h"
-#include "../Search/DatabaseConstraint.h"
+#include "../Search/DatabaseDicomTagConstraints.h"
+#include "../Search/DatabaseMetadataConstraint.h"
 #include "../Search/DicomTagConstraint.h"
 #include "../Search/ISqlLookupFormatter.h"
 #include "../ServerEnumerations.h"
@@ -232,16 +233,15 @@
     // filter & ordering fields
     ResourceType                         level_;                // The level of the response (the filtering on tags, labels and metadata also happens at this level)
     OrthancIdentifiers                   orthancIdentifiers_;   // The response must belong to this Orthanc resources hierarchy
-    DatabaseConstraints                  dicomTagConstraints_;  // All tags filters (note: the order is not important)
+    DatabaseDicomTagConstraints          dicomTagConstraints_;  // All tags filters (note: the order is not important)
     bool                                 hasLimits_;
     uint64_t                             limitsSince_;
     uint64_t                             limitsCount_;
     std::set<std::string>                labels_;
     LabelsConstraint                     labelsConstraint_;
 
-    // TODO-FIND
     std::deque<Ordering*>                ordering_;             // The ordering criteria (note: the order is important !)
-    std::deque<void*>   /* TODO-FIND */       metadataConstraints_;  // All metadata filters (note: the order is not important)
+    std::deque<DatabaseMetadataConstraint*>  metadataConstraints_;  // All metadata filters (note: the order is not important)
 
     bool                                 retrieveMainDicomTags_;
     bool                                 retrieveMetadata_;
@@ -254,7 +254,7 @@
     ChildrenSpecification                retrieveChildrenStudies_;
     ChildrenSpecification                retrieveChildrenSeries_;
     ChildrenSpecification                retrieveChildrenInstances_;
-    bool                                 retrieveOneInstanceIdentifier_;
+    bool                                 retrieveOneInstanceMetadataAndAttachments_;
 
     std::unique_ptr<MainDicomTagsRegistry>  mainDicomTagsRegistry_;
 
@@ -284,12 +284,12 @@
       return orthancIdentifiers_;
     }
 
-    DatabaseConstraints& GetDicomTagConstraints()
+    DatabaseDicomTagConstraints& GetDicomTagConstraints()
     {
       return dicomTagConstraints_;
     }
 
-    const DatabaseConstraints& GetDicomTagConstraints() const
+    const DatabaseDicomTagConstraints& GetDicomTagConstraints() const
     {
       return dicomTagConstraints_;
     }
@@ -327,6 +327,13 @@
       return ordering_;
     }
 
+    void AddMetadataConstraint(DatabaseMetadataConstraint* constraint);
+
+    const std::deque<DatabaseMetadataConstraint*>& GetMetadataConstraint() const
+    {
+      return metadataConstraints_;
+    }
+
     void SetLabels(const std::set<std::string>& labels)
     {
       labels_ = labels;
@@ -413,13 +420,8 @@
       return const_cast<FindRequest&>(*this).GetChildrenSpecification(level);
     }
 
-    void SetRetrieveOneInstanceIdentifier(bool retrieve);
+    void SetRetrieveOneInstanceMetadataAndAttachments(bool retrieve);
 
-    bool IsRetrieveOneInstanceIdentifier() const
-    {
-      return (retrieveOneInstanceIdentifier_ ||
-              (level_ != ResourceType_Instance &&
-               GetChildrenSpecification(ResourceType_Instance).IsRetrieveIdentifiers()));
-    }
+    bool IsRetrieveOneInstanceMetadataAndAttachments() const;
   };
 }
--- a/OrthancServer/Sources/Database/FindResponse.cpp	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/Sources/Database/FindResponse.cpp	Wed Oct 09 11:01:11 2024 +0200
@@ -483,17 +483,111 @@
   }
 
 
-  const std::string& FindResponse::Resource::GetOneInstanceIdentifier() const
+  void FindResponse::Resource::SetOneInstanceMetadataAndAttachments(const std::string& instancePublicId,
+                                                                    const std::map<MetadataType, std::string>& metadata,
+                                                                    const std::map<FileContentType, FileInfo>& attachments)
   {
-    const std::set<std::string>& instances = GetChildrenInformation(ResourceType_Instance).GetIdentifiers();
+    if (hasOneInstanceMetadataAndAttachments_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else if (instancePublicId.empty())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      hasOneInstanceMetadataAndAttachments_ = true;
+      oneInstancePublicId_ = instancePublicId;
+      oneInstanceMetadata_ = metadata;
+      oneInstanceAttachments_ = attachments;
+    }
+  }
 
-    if (instances.size() == 0)
+
+  void FindResponse::Resource::SetOneInstancePublicId(const std::string& instancePublicId)
+  {
+    SetOneInstanceMetadataAndAttachments(instancePublicId, std::map<MetadataType, std::string>(),
+                                         std::map<FileContentType, FileInfo>());
+  }
+
+
+  void FindResponse::Resource::AddOneInstanceMetadata(MetadataType metadata,
+                                                      const std::string& value)
+  {
+    if (hasOneInstanceMetadataAndAttachments_)
     {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);  // HasOneInstanceIdentifier() should have been called
+      if (oneInstanceMetadata_.find(metadata) == oneInstanceMetadata_.end())
+      {
+        oneInstanceMetadata_[metadata] = value;
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls, "Metadata already exists");
+      }
     }
     else
     {
-      return *instances.begin();
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  void FindResponse::Resource::AddOneInstanceAttachment(const FileInfo& attachment)
+  {
+    if (hasOneInstanceMetadataAndAttachments_)
+    {
+      if (oneInstanceAttachments_.find(attachment.GetContentType()) == oneInstanceAttachments_.end())
+      {
+        oneInstanceAttachments_[attachment.GetContentType()] = attachment;
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls, "Attachment already exists");
+      }
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  const std::string& FindResponse::Resource::GetOneInstancePublicId() const
+  {
+    if (hasOneInstanceMetadataAndAttachments_)
+    {
+      return oneInstancePublicId_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  const std::map<MetadataType, std::string>& FindResponse::Resource::GetOneInstanceMetadata() const
+  {
+    if (hasOneInstanceMetadataAndAttachments_)
+    {
+      return oneInstanceMetadata_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  const std::map<FileContentType, FileInfo>& FindResponse::Resource::GetOneInstanceAttachments() const
+  {
+    if (hasOneInstanceMetadataAndAttachments_)
+    {
+      return oneInstanceAttachments_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
     }
   }
 
@@ -542,6 +636,25 @@
   }
 
 
+  static void DebugAttachments(Json::Value& target,
+                               const std::map<FileContentType, FileInfo>& attachments)
+  {
+    target = Json::objectValue;
+    for (std::map<FileContentType, FileInfo>::const_iterator it = attachments.begin();
+         it != attachments.end(); ++it)
+    {
+      if (it->first != it->second.GetContentType())
+      {
+        throw OrthancException(ErrorCode_DatabasePlugin);
+      }
+      else
+      {
+        DebugAddAttachment(target, it->second);
+      }
+    }
+  }
+
+
   static void DebugSetOfStrings(Json::Value& target,
                                 const std::set<std::string>& values)
   {
@@ -633,25 +746,14 @@
 
     if (request.IsRetrieveAttachments())
     {
-      Json::Value v = Json::objectValue;
-      for (std::map<FileContentType, FileInfo>::const_iterator it = attachments_.begin();
-           it != attachments_.end(); ++it)
-      {
-        if (it->first != it->second.GetContentType())
-        {
-          throw OrthancException(ErrorCode_DatabasePlugin);
-        }
-        else
-        {
-          DebugAddAttachment(v, it->second);
-        }
-      }
-      target["Attachments"] = v;
+      DebugAttachments(target["Attachments"], attachments_);
     }
 
-    if (request.IsRetrieveOneInstanceIdentifier())
+    if (request.GetLevel() != ResourceType_Instance &&
+        request.IsRetrieveOneInstanceMetadataAndAttachments())
     {
-      target["OneInstance"] = GetOneInstanceIdentifier();
+      DebugMetadata(target["OneInstance"]["Metadata"], GetOneInstanceMetadata());
+      DebugAttachments(target["OneInstance"]["Attachments"], GetOneInstanceAttachments());
     }
   }
 
--- a/OrthancServer/Sources/Database/FindResponse.h	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/Sources/Database/FindResponse.h	Wed Oct 09 11:01:11 2024 +0200
@@ -123,6 +123,10 @@
       ChildrenInformation                   childrenInstancesInformation_;
       std::set<std::string>                 labels_;
       std::map<FileContentType, FileInfo>   attachments_;
+      bool                                  hasOneInstanceMetadataAndAttachments_;
+      std::string                           oneInstancePublicId_;
+      std::map<MetadataType, std::string>   oneInstanceMetadata_;
+      std::map<FileContentType, FileInfo>   oneInstanceAttachments_;
 
       MainDicomTagsAtLevel& GetMainDicomTagsAtLevel(ResourceType level);
 
@@ -144,7 +148,8 @@
                const std::string& identifier) :
         level_(level),
         internalId_(internalId),
-        identifier_(identifier)
+        identifier_(identifier),
+        hasOneInstanceMetadataAndAttachments_(false)
       {
       }
 
@@ -268,13 +273,28 @@
         return attachments_;
       }
 
-      const std::string& GetOneInstanceIdentifier() const;
+      void SetOneInstanceMetadataAndAttachments(const std::string& instancePublicId,
+                                                const std::map<MetadataType, std::string>& metadata,
+                                                const std::map<FileContentType, FileInfo>& attachments);
+
+      void SetOneInstancePublicId(const std::string& instancePublicId);
+
+      void AddOneInstanceMetadata(MetadataType metadata,
+                                  const std::string& value);
+
+      void AddOneInstanceAttachment(const FileInfo& attachment);
 
-      bool HasOneInstanceIdentifier() const
+      bool HasOneInstanceMetadataAndAttachments() const
       {
-        return !GetChildrenIdentifiers(ResourceType_Instance).empty();
+        return hasOneInstanceMetadataAndAttachments_;
       }
 
+      const std::string& GetOneInstancePublicId() const;
+
+      const std::map<MetadataType, std::string>& GetOneInstanceMetadata() const;
+
+      const std::map<FileContentType, FileInfo>& GetOneInstanceAttachments() const;
+
       void DebugExport(Json::Value& target,
                        const FindRequest& request) const;
     };
--- a/OrthancServer/Sources/Database/IDatabaseWrapper.h	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/Sources/Database/IDatabaseWrapper.h	Wed Oct 09 11:01:11 2024 +0200
@@ -39,7 +39,7 @@
 
 namespace Orthanc
 {
-  class DatabaseConstraints;
+  class DatabaseDicomTagConstraints;
   class ResourcesContent;
 
   class IDatabaseWrapper : public boost::noncopyable
@@ -55,6 +55,7 @@
       bool hasUpdateAndGetStatistics_;
       bool hasMeasureLatency_;
       bool hasFindSupport_;
+      bool hasExtendedChanges_;
 
     public:
       Capabilities() :
@@ -64,7 +65,8 @@
         hasAtomicIncrementGlobalProperty_(false),
         hasUpdateAndGetStatistics_(false),
         hasMeasureLatency_(false),
-        hasFindSupport_(false)
+        hasFindSupport_(false),
+        hasExtendedChanges_(false)
       {
       }
 
@@ -98,6 +100,16 @@
         return hasLabelsSupport_;
       }
 
+      void SetHasExtendedChanges(bool value)
+      {
+        hasExtendedChanges_ = value;
+      }
+
+      bool HasExtendedChanges() const
+      {
+        return hasExtendedChanges_;
+      }
+
       void SetAtomicIncrementGlobalProperty(bool value)
       {
         hasAtomicIncrementGlobalProperty_ = value;
@@ -296,7 +308,7 @@
     
       virtual void ApplyLookupResources(std::list<std::string>& resourcesId,
                                         std::list<std::string>* instancesId, // Can be NULL if not needed
-                                        const DatabaseConstraints& lookup,
+                                        const DatabaseDicomTagConstraints& lookup,
                                         ResourceType queryLevel,
                                         const std::set<std::string>& labels,
                                         LabelsConstraint labelsConstraint,
@@ -359,6 +371,7 @@
                                               int64_t increment,
                                               bool shared) = 0;
 
+      // New in Orthanc 1.12.3
       virtual void UpdateAndGetStatistics(int64_t& patientsCount,
                                           int64_t& studiesCount,
                                           int64_t& seriesCount,
@@ -393,6 +406,14 @@
                                  const Capabilities& capabilities,
                                  const FindRequest& request,
                                  const std::string& identifier) = 0;
+
+      // New in Orthanc 1.12.5
+      virtual void GetChangesExtended(std::list<ServerIndexChange>& target /*out*/,
+                                      bool& done /*out*/,
+                                      int64_t since,
+                                      int64_t to,
+                                      uint32_t limit,
+                                      const std::set<ChangeType>& filterType) = 0;
     };
 
 
--- a/OrthancServer/Sources/Database/MainDicomTagsRegistry.cpp	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/Sources/Database/MainDicomTagsRegistry.cpp	Wed Oct 09 11:01:11 2024 +0200
@@ -98,7 +98,7 @@
   }
 
 
-  bool MainDicomTagsRegistry::NormalizeLookup(DatabaseConstraints& target,
+  bool MainDicomTagsRegistry::NormalizeLookup(DatabaseDicomTagConstraints& target,
                                               const DatabaseLookup& source,
                                               ResourceType queryLevel) const
   {
--- a/OrthancServer/Sources/Database/MainDicomTagsRegistry.h	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/Sources/Database/MainDicomTagsRegistry.h	Wed Oct 09 11:01:11 2024 +0200
@@ -24,6 +24,7 @@
 #pragma once
 
 #include "../Search/DatabaseLookup.h"
+#include "../Search/DatabaseDicomTagConstraints.h"
 
 #include <boost/noncopyable.hpp>
 
@@ -81,7 +82,7 @@
      * constraints are less strict than the original DatabaseLookup,
      * so more resources will match them.
      **/
-    bool NormalizeLookup(DatabaseConstraints& target,
+    bool NormalizeLookup(DatabaseDicomTagConstraints& target,
                          const DatabaseLookup& source,
                          ResourceType queryLevel) const;
   };
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp	Wed Oct 09 11:01:11 2024 +0200
@@ -43,6 +43,53 @@
 
 namespace Orthanc
 {  
+  static std::string JoinRequestedMetadata(const FindRequest::ChildrenSpecification& childrenSpec)
+  {
+    std::set<std::string> metadataTypes;
+    for (std::set<MetadataType>::const_iterator it = childrenSpec.GetMetadata().begin(); it != childrenSpec.GetMetadata().end(); ++it)
+    {
+      metadataTypes.insert(boost::lexical_cast<std::string>(*it));
+    }
+    std::string joinedMetadataTypes;
+    Orthanc::Toolbox::JoinStrings(joinedMetadataTypes, metadataTypes, ", ");
+
+    return joinedMetadataTypes;
+  }
+
+  static std::string JoinRequestedTags(const FindRequest::ChildrenSpecification& childrenSpec)
+  {
+    // note: SQLite does not seem to support (tagGroup, tagElement) in ((x, y), (z, w)) in complex subqueries.
+    // Therefore, since we expect the requested tag list to be short, we write it as 
+    // ((tagGroup = x AND tagElement = y ) OR (tagGroup = z AND tagElement = w))
+
+    std::string sql = " (";
+    std::set<std::string> tags;
+    for (std::set<DicomTag>::const_iterator it = childrenSpec.GetMainDicomTags().begin(); it != childrenSpec.GetMainDicomTags().end(); ++it)
+    {
+      tags.insert("(tagGroup = " + boost::lexical_cast<std::string>(it->GetGroup()) 
+                  + " AND tagElement = " + boost::lexical_cast<std::string>(it->GetElement()) + ")");
+    }
+    std::string joinedTags;
+    Orthanc::Toolbox::JoinStrings(joinedTags, tags, " OR ");
+
+    sql += joinedTags + ") ";
+    return sql;
+  }
+
+  static std::string JoinChanges(const std::set<ChangeType>& changeTypes)
+  {
+    std::set<std::string> changeTypesString;
+    for (std::set<ChangeType>::const_iterator it = changeTypes.begin(); it != changeTypes.end(); ++it)
+    {
+      changeTypesString.insert(boost::lexical_cast<std::string>(static_cast<uint32_t>(*it)));
+    }
+
+    std::string joinedChangesTypes;
+    Orthanc::Toolbox::JoinStrings(joinedChangesTypes, changeTypesString, ", ");
+
+    return joinedChangesTypes;
+  }
+
   class SQLiteDatabaseWrapper::LookupFormatter : public ISqlLookupFormatter
   {
   private:
@@ -65,6 +112,28 @@
       return "ESCAPE '\\'";
     }
 
+    virtual std::string FormatLimits(uint64_t since, uint64_t count) ORTHANC_OVERRIDE
+    {
+      std::string sql;
+
+      if (count > 0)
+      {
+        sql += " LIMIT " + boost::lexical_cast<std::string>(count);
+      }
+
+      if (since > 0)
+      {
+        if (count == 0)
+        {
+          sql += " LIMIT -1";  // In SQLite, "OFFSET" cannot appear without "LIMIT"
+        }
+
+        sql += " OFFSET " + boost::lexical_cast<std::string>(since);
+      }
+      
+      return sql;
+    }
+
     virtual bool IsEscapeBrackets() const ORTHANC_OVERRIDE
     {
       return false;
@@ -235,11 +304,12 @@
     void GetChangesInternal(std::list<ServerIndexChange>& target,
                             bool& done,
                             SQLite::Statement& s,
-                            uint32_t limit)
+                            uint32_t limit,
+                            bool returnFirstResults) // the statement usually returns limit+1 results while we only need the limit results -> we need to know which ones to return, the firsts or the lasts
     {
       target.clear();
 
-      while (target.size() < limit && s.Step())
+      while (s.Step())
       {
         int64_t seq = s.ColumnInt64(0);
         ChangeType changeType = static_cast<ChangeType>(s.ColumnInt(1));
@@ -252,7 +322,22 @@
         target.push_back(ServerIndexChange(seq, changeType, resourceType, publicId, date));
       }
 
-      done = !(target.size() == limit && s.Step());
+      done = target.size() <= limit;  // 'done' means "there are no more other changes of this type in that direction (depending on since/to)"
+      
+      // if we have retrieved more changes than requested -> cleanup
+      if (target.size() > limit)
+      {
+        assert(target.size() == limit+1); // the statement should only request 1 element more
+
+        if (returnFirstResults)
+        {
+          target.pop_back();
+        }
+        else
+        {
+          target.pop_front();
+        }
+      }
     }
 
 
@@ -341,7 +426,7 @@
 
     virtual void ApplyLookupResources(std::list<std::string>& resourcesId,
                                       std::list<std::string>* instancesId,
-                                      const DatabaseConstraints& lookup,
+                                      const DatabaseDicomTagConstraints& lookup,
                                       ResourceType queryLevel,
                                       const std::set<std::string>& labels,
                                       LabelsConstraint labelsConstraint,
@@ -352,7 +437,7 @@
       std::string sql;
       LookupFormatter::Apply(sql, formatter, lookup, queryLevel, labels, labelsConstraint, limit);
 
-      sql = "CREATE TEMPORARY TABLE Lookup AS " + sql;
+      sql = "CREATE TEMPORARY TABLE Lookup AS " + sql;   // TODO-FIND: use a CTE (or is this method obsolete ?)
     
       {
         SQLite::Statement s(db_, SQLITE_FROM_HERE, "DROP TABLE IF EXISTS Lookup");
@@ -382,325 +467,648 @@
       }
     }
 
+#define C0_QUERY_ID 0
+#define C1_INTERNAL_ID 1
+#define C2_ROW_NUMBER 2
+#define C3_STRING_1 3
+#define C4_STRING_2 4
+#define C5_STRING_3 5
+#define C6_INT_1 6
+#define C7_INT_2 7
+#define C8_BIG_INT_1 8
+#define C9_BIG_INT_2 9
+
+#define QUERY_LOOKUP 1
+#define QUERY_MAIN_DICOM_TAGS 2
+#define QUERY_ATTACHMENTS 3
+#define QUERY_METADATA 4
+#define QUERY_LABELS 5
+#define QUERY_PARENT_MAIN_DICOM_TAGS 10
+#define QUERY_PARENT_IDENTIFIER 11
+#define QUERY_PARENT_METADATA 12
+#define QUERY_GRAND_PARENT_MAIN_DICOM_TAGS 15
+#define QUERY_GRAND_PARENT_METADATA 16
+#define QUERY_CHILDREN_IDENTIFIERS 20
+#define QUERY_CHILDREN_MAIN_DICOM_TAGS 21
+#define QUERY_CHILDREN_METADATA 22
+#define QUERY_GRAND_CHILDREN_IDENTIFIERS 30
+#define QUERY_GRAND_CHILDREN_MAIN_DICOM_TAGS 31
+#define QUERY_GRAND_CHILDREN_METADATA 32
+#define QUERY_GRAND_GRAND_CHILDREN_IDENTIFIERS 40
+#define QUERY_ONE_INSTANCE_IDENTIFIER 50
+#define QUERY_ONE_INSTANCE_METADATA 51
+#define QUERY_ONE_INSTANCE_ATTACHMENTS 52
+
+#define STRINGIFY(x) #x
+#define TOSTRING(x) STRINGIFY(x)
+
+
     virtual void ExecuteFind(FindResponse& response,
                              const FindRequest& request,
                              const Capabilities& capabilities) ORTHANC_OVERRIDE
     {
-      const ResourceType requestLevel = request.GetLevel();
+      LookupFormatter formatter;
       std::string sql;
+      const ResourceType requestLevel = request.GetLevel();
 
+      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 + ") ";
+
+      // in SQLite, all CTEs must be created at the beginning of the query, you can not define local CTE inside subqueries
+      // need one instance info ? (part 1: create the CTE)
+      if (request.GetLevel() != ResourceType_Instance &&
+          request.IsRetrieveOneInstanceMetadataAndAttachments())
       {
-        // clean previous lookup table
-        SQLite::Statement s(db_, SQLITE_FROM_HERE, "DROP TABLE IF EXISTS Lookup");
-        s.Run();
+        // Here, we create a nested CTE 'OneInstance' with one instance ID to join with metadata and main
+        sql += ", OneInstance AS";
+
+        switch (requestLevel)
+        {
+          case ResourceType_Series:
+          {
+            sql+= "  (SELECT Lookup.internalId AS parentInternalId, childLevel.publicId AS instancePublicId, childLevel.internalId AS instanceInternalId"
+                  "   FROM Resources AS childLevel "
+                  "   INNER JOIN Lookup ON childLevel.parentId = Lookup.internalId GROUP BY Lookup.internalId) ";
+            break;
+          }
+
+          case ResourceType_Study:
+          {
+            sql+= "  (SELECT Lookup.internalId AS parentInternalId, grandChildLevel.publicId AS instancePublicId, grandChildLevel.internalId AS instanceInternalId"
+                  "   FROM Resources AS grandChildLevel "
+                  "   INNER JOIN Resources childLevel ON grandChildLevel.parentId = childLevel.internalId "
+                  "   INNER JOIN Lookup ON childLevel.parentId = Lookup.internalId GROUP BY Lookup.internalId) ";
+            break;
+          }
+
+          case ResourceType_Patient:
+          {
+            sql+= "  (SELECT Lookup.internalId AS parentInternalId, grandGrandChildLevel.publicId AS instancePublicId, grandGrandChildLevel.internalId AS instanceInternalId"
+                  "   FROM Resources AS grandGrandChildLevel "
+                  "   INNER JOIN Resources grandChildLevel ON grandGrandChildLevel.parentId = grandChildLevel.internalId "
+                  "   INNER JOIN Resources childLevel ON grandChildLevel.parentId = childLevel.internalId "
+                  "   INNER JOIN Lookup ON childLevel.parentId = Lookup.internalId GROUP BY Lookup.internalId) ";
+            break;
+          }
+
+          default:
+            throw OrthancException(ErrorCode_InternalError);
+        }
       }
 
-      {
-        // extract the resource id of interest by executing the lookup
-        LookupFormatter formatter;
-        LookupFormatter::Apply(sql, formatter, request);
-
-        sql = "CREATE TEMPORARY TABLE Lookup AS " + sql;
+      sql += "SELECT "
+             "  " TOSTRING(QUERY_LOOKUP) " AS c0_queryId, "
+             "  Lookup.internalId AS c1_internalId, "
+             "  Lookup.rowNumber AS c2_rowNumber, "
+             "  Lookup.publicId AS c3_string1, "
+             "  NULL AS c4_string2, "
+             "  NULL AS c5_string3, "
+             "  NULL AS c6_int1, "
+             "  NULL AS c7_int2, "
+             "  NULL AS c8_big_int1, "
+             "  NULL AS c9_big_int2 "
+             "  FROM Lookup ";
 
-        SQLite::Statement statement(db_, sql);
-        formatter.Bind(statement);
-        statement.Run();
+      // need one instance info ? (part 2: execute the queries)
+      if (request.GetLevel() != ResourceType_Instance &&
+          request.IsRetrieveOneInstanceMetadataAndAttachments())
+      {
+        sql += "   UNION SELECT"
+               "    " TOSTRING(QUERY_ONE_INSTANCE_IDENTIFIER) " AS c0_queryId, "
+               "    parentInternalId AS c1_internalId, "
+               "    NULL AS c2_rowNumber, "
+               "    instancePublicId AS c3_string1, "
+               "    NULL AS c4_string2, "
+               "    NULL AS c5_string3, "
+               "    NULL AS c6_int1, "
+               "    NULL AS c7_int2, "
+               "    instanceInternalId AS c8_big_int1, "
+               "    NULL AS c9_big_int2 "
+               "   FROM OneInstance ";
 
-        SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId, internalId FROM Lookup");
-        while (s.Step())
-        {
-          response.Add(new FindResponse::Resource(requestLevel, s.ColumnInt64(1), s.ColumnString(0)));
-        }
+        sql += "   UNION SELECT"
+               "    " TOSTRING(QUERY_ONE_INSTANCE_METADATA) " AS c0_queryId, "
+               "    parentInternalId AS c1_internalId, "
+               "    NULL AS c2_rowNumber, "
+               "    Metadata.value AS c3_string1, "
+               "    NULL AS c4_string2, "
+               "    NULL AS c5_string3, "
+               "    Metadata.type AS c6_int1, "
+               "    NULL AS c7_int2, "
+               "    NULL AS c8_big_int1, "
+               "    NULL AS c9_big_int2 "
+               "   FROM OneInstance "
+               "   INNER JOIN Metadata ON Metadata.id = OneInstance.instanceInternalId ";
+              
+        sql += "   UNION SELECT"
+               "    " TOSTRING(QUERY_ONE_INSTANCE_ATTACHMENTS) " AS c0_queryId, "
+               "    parentInternalId AS c1_internalId, "
+               "    NULL AS c2_rowNumber, "
+               "    uuid AS c3_string1, "
+               "    uncompressedMD5 AS c4_string2, "
+               "    compressedMD5 AS c5_string3, "
+               "    fileType AS c6_int1, "
+               "    compressionType AS c7_int2, "
+               "    compressedSize AS c8_big_int1, "
+               "    uncompressedSize AS c9_big_int2 "
+               "   FROM OneInstance "
+               "   INNER JOIN AttachedFiles ON AttachedFiles.id = OneInstance.instanceInternalId ";
+
       }
 
       // need MainDicomTags from resource ?
       if (request.IsRetrieveMainDicomTags())
       {
-        sql = "SELECT id, tagGroup, tagElement, value "
-              "FROM MainDicomTags "
-              "INNER JOIN Lookup ON MainDicomTags.id = Lookup.internalId";
+        sql += "UNION SELECT "
+               "  " TOSTRING(QUERY_MAIN_DICOM_TAGS) " AS c0_queryId, "
+               "  Lookup.internalId AS c1_internalId, "
+               "  NULL AS c2_rowNumber, "
+               "  value AS c3_string1, "
+               "  NULL AS c4_string2, "
+               "  NULL AS c5_string3, "
+               "  tagGroup AS c6_int1, "
+               "  tagElement AS c7_int2, "
+               "  NULL AS c8_big_int1, "
+               "  NULL AS c9_big_int2 "
+               "FROM Lookup "
+               "INNER JOIN MainDicomTags ON MainDicomTags.id = Lookup.internalId ";
+      }
+
+      // need resource metadata ?
+      if (request.IsRetrieveMetadata())
+      {
+        sql += "UNION SELECT "
+               "  " TOSTRING(QUERY_METADATA) " AS c0_queryId, "
+               "  Lookup.internalId AS c1_internalId, "
+               "  NULL AS c2_rowNumber, "
+               "  value AS c3_string1, "
+               "  NULL AS c4_string2, "
+               "  NULL AS c5_string3, "
+               "  type AS c6_int1, "
+               "  NULL AS c7_int2, "
+               "  NULL AS c8_big_int1, "
+               "  NULL AS c9_big_int2 "
+               "FROM Lookup "
+               "INNER JOIN Metadata ON Metadata.id = Lookup.internalId ";
+      }
 
-        SQLite::Statement s(db_, SQLITE_FROM_HERE, sql);
-        while (s.Step())
-        {
-          FindResponse::Resource& res = response.GetResourceByInternalId(s.ColumnInt64(0));
-          res.AddStringDicomTag(requestLevel, 
-                                static_cast<uint16_t>(s.ColumnInt(1)),
-                                static_cast<uint16_t>(s.ColumnInt(2)),
-                                s.ColumnString(3));
-        }
+      // need resource attachments ?
+      if (request.IsRetrieveAttachments())
+      {
+        sql += "UNION SELECT "
+               "  " TOSTRING(QUERY_ATTACHMENTS) " AS c0_queryId, "
+               "  Lookup.internalId AS c1_internalId, "
+               "  NULL AS c2_rowNumber, "
+               "  uuid AS c3_string1, "
+               "  uncompressedMD5 AS c4_string2, "
+               "  compressedMD5 AS c5_string3, "
+               "  fileType AS c6_int1, "
+               "  compressionType AS c7_int2, "
+               "  compressedSize AS c8_big_int1, "
+               "  uncompressedSize AS c9_big_int2 "
+               "FROM Lookup "
+               "INNER JOIN AttachedFiles ON AttachedFiles.id = Lookup.internalId ";
+      }
+
+
+      // need resource labels ?
+      if (request.IsRetrieveLabels())
+      {
+        sql += "UNION SELECT "
+               "  " TOSTRING(QUERY_LABELS) " AS c0_queryId, "
+               "  Lookup.internalId AS c1_internalId, "
+               "  NULL AS c2_rowNumber, "
+               "  label AS c3_string1, "
+               "  NULL AS c4_string2, "
+               "  NULL AS c5_string3, "
+               "  NULL AS c6_int1, "
+               "  NULL AS c7_int2, "
+               "  NULL AS c8_big_int1, "
+               "  NULL AS c9_big_int2 "
+               "FROM Lookup "
+               "INNER JOIN Labels ON Labels.id = Lookup.internalId ";
       }
 
-      // need MainDicomTags from parent ?
-      if (requestLevel > ResourceType_Patient && request.GetParentSpecification(static_cast<ResourceType>(requestLevel - 1)).IsRetrieveMainDicomTags())
+      if (requestLevel > ResourceType_Patient)
       {
-        sql = "SELECT currentLevel.internalId, tagGroup, tagElement, value "
-              "FROM MainDicomTags "
-              "INNER JOIN Resources currentLevel ON Lookup.internalId = currentLevel.internalId "
-              "INNER JOIN Lookup ON MainDicomTags.id = currentLevel.parentId";
+        // need MainDicomTags from parent ?
+        if (request.GetParentSpecification(static_cast<ResourceType>(requestLevel - 1)).IsRetrieveMainDicomTags())
+        {
+          sql += "UNION SELECT "
+                 "  " TOSTRING(QUERY_PARENT_MAIN_DICOM_TAGS) " AS c0_queryId, "
+                 "  Lookup.internalId AS c1_internalId, "
+                 "  NULL AS c2_rowNumber, "
+                 "  value AS c3_string1, "
+                 "  NULL AS c4_string2, "
+                 "  NULL AS c5_string3, "
+                 "  tagGroup AS c6_int1, "
+                 "  tagElement AS c7_int2, "
+                 "  NULL AS c8_big_int1, "
+                 "  NULL AS c9_big_int2 "
+                 "FROM Lookup "
+                 "INNER JOIN Resources currentLevel ON Lookup.internalId = currentLevel.internalId "
+                 "INNER JOIN MainDicomTags ON MainDicomTags.id = currentLevel.parentId ";
+        }
 
-        SQLite::Statement s(db_, SQLITE_FROM_HERE, sql);
-        while (s.Step())
+        // need metadata from parent ?
+        if (request.GetParentSpecification(static_cast<ResourceType>(requestLevel - 1)).IsRetrieveMetadata())
         {
-          FindResponse::Resource& res = response.GetResourceByInternalId(s.ColumnInt64(0));
-          res.AddStringDicomTag(static_cast<ResourceType>(requestLevel - 1), 
-                                static_cast<uint16_t>(s.ColumnInt(1)),
-                                static_cast<uint16_t>(s.ColumnInt(2)),
-                                s.ColumnString(3));
+          sql += "UNION SELECT "
+                 "  " TOSTRING(QUERY_PARENT_METADATA) " AS c0_queryId, "
+                 "  Lookup.internalId AS c1_internalId, "
+                 "  NULL AS c2_rowNumber, "
+                 "  value AS c3_string1, "
+                 "  NULL AS c4_string2, "
+                 "  NULL AS c5_string3, "
+                 "  type AS c6_int1, "
+                 "  NULL AS c7_int2, "
+                 "  NULL AS c8_big_int1, "
+                 "  NULL AS c9_big_int2 "
+                 "FROM Lookup "
+                 "INNER JOIN Resources currentLevel ON Lookup.internalId = currentLevel.internalId "
+                 "INNER JOIN Metadata ON Metadata.id = currentLevel.parentId ";        
         }
-      }
 
-      // need MainDicomTags from grandparent ?
-      if (requestLevel > ResourceType_Study && request.GetParentSpecification(static_cast<ResourceType>(requestLevel - 2)).IsRetrieveMainDicomTags())
-      {
-        sql = "SELECT currentLevel.internalId, tagGroup, tagElement, value "
-              "FROM MainDicomTags "
-              "INNER JOIN Resources currentLevel ON Lookup.internalId = currentLevel.internalId "
-              "INNER JOIN Resources parentLevel ON currentLevel.parentId = parentLevel.internalId "
-              "INNER JOIN Lookup ON MainDicomTags.id = parentLevel.parentId";
+        if (requestLevel > ResourceType_Study)
+        {
+          // need MainDicomTags from grandparent ?
+          if (request.GetParentSpecification(static_cast<ResourceType>(requestLevel - 2)).IsRetrieveMainDicomTags())
+          {
+            sql += "UNION SELECT "
+                  "  " TOSTRING(QUERY_GRAND_PARENT_MAIN_DICOM_TAGS) " AS c0_queryId, "
+                  "  Lookup.internalId AS c1_internalId, "
+                  "  NULL AS c2_rowNumber, "
+                  "  value AS c3_string1, "
+                  "  NULL AS c4_string2, "
+                  "  NULL AS c5_string3, "
+                  "  tagGroup AS c6_int1, "
+                  "  tagElement AS c7_int2, "
+                  "  NULL AS c8_big_int1, "
+                  "  NULL AS c9_big_int2 "
+                  "FROM Lookup "
+                  "INNER JOIN Resources currentLevel ON Lookup.internalId = currentLevel.internalId "
+                  "INNER JOIN Resources parentLevel ON currentLevel.parentId = parentLevel.internalId "
+                  "INNER JOIN MainDicomTags ON MainDicomTags.id = parentLevel.parentId ";
+          }
 
-        SQLite::Statement s(db_, SQLITE_FROM_HERE, sql);
-        while (s.Step())
-        {
-          FindResponse::Resource& res = response.GetResourceByInternalId(s.ColumnInt64(0));
-          res.AddStringDicomTag(static_cast<ResourceType>(requestLevel - 2), 
-                                static_cast<uint16_t>(s.ColumnInt(1)),
-                                static_cast<uint16_t>(s.ColumnInt(2)),
-                                s.ColumnString(3));
+          // need metadata from grandparent ?
+          if (request.GetParentSpecification(static_cast<ResourceType>(requestLevel - 2)).IsRetrieveMetadata())
+          {
+            sql += "UNION SELECT "
+                  "  " TOSTRING(QUERY_GRAND_PARENT_METADATA) " AS c0_queryId, "
+                  "  Lookup.internalId AS c1_internalId, "
+                  "  NULL AS c2_rowNumber, "
+                  "  value AS c3_string1, "
+                  "  NULL AS c4_string2, "
+                  "  NULL AS c5_string3, "
+                  "  type AS c6_int1, "
+                  "  NULL AS c7_int2, "
+                  "  NULL AS c8_big_int1, "
+                  "  NULL AS c9_big_int2 "
+                  "FROM Lookup "
+                  "INNER JOIN Resources currentLevel ON Lookup.internalId = currentLevel.internalId "
+                  "INNER JOIN Resources parentLevel ON currentLevel.parentId = parentLevel.internalId "
+                  "INNER JOIN Metadata ON Metadata.id = parentLevel.parentId ";
+          }
         }
       }
 
       // need MainDicomTags from children ?
       if (requestLevel <= ResourceType_Series && request.GetChildrenSpecification(static_cast<ResourceType>(requestLevel + 1)).GetMainDicomTags().size() > 0)
       {
-        sql = "SELECT Lookup.internalId, tagGroup, tagElement, value "
-              "FROM MainDicomTags "
-              "INNER JOIN Resources childLevel ON childLevel.parentId = Lookup.internalId "
-              "INNER JOIN Lookup ON MainDicomTags.id = childLevel.internalId ";
-
-        SQLite::Statement s(db_, SQLITE_FROM_HERE, sql);
-        while (s.Step())
-        {
-          FindResponse::Resource& res = response.GetResourceByInternalId(s.ColumnInt64(0));
-          res.AddChildrenMainDicomTagValue(static_cast<ResourceType>(requestLevel + 1), 
-                                DicomTag(static_cast<uint16_t>(s.ColumnInt(1)),
-                                         static_cast<uint16_t>(s.ColumnInt(2))),
-                                s.ColumnString(3));
-        }
+        sql += "UNION SELECT "
+               "  " TOSTRING(QUERY_CHILDREN_MAIN_DICOM_TAGS) " AS c0_queryId, "
+               "  Lookup.internalId AS c1_internalId, "
+               "  NULL AS c2_rowNumber, "
+               "  value AS c3_string1, "
+               "  NULL AS c4_string2, "
+               "  NULL AS c5_string3, "
+               "  tagGroup AS c6_int1, "
+               "  tagElement AS c7_int2, "
+               "  NULL AS c8_big_int1, "
+               "  NULL AS c9_big_int2 "
+               "FROM Lookup "
+               "  INNER JOIN Resources childLevel ON childLevel.parentId = Lookup.internalId "
+               "  INNER JOIN MainDicomTags ON MainDicomTags.id = childLevel.internalId AND " + JoinRequestedTags(request.GetChildrenSpecification(static_cast<ResourceType>(requestLevel + 1))); 
       }
 
       // need MainDicomTags from grandchildren ?
       if (requestLevel <= ResourceType_Study && request.GetChildrenSpecification(static_cast<ResourceType>(requestLevel + 2)).GetMainDicomTags().size() > 0)
       {
-        sql = "SELECT Lookup.internalId, tagGroup, tagElement, value "
-              "FROM MainDicomTags "
-              "INNER JOIN Resources childLevel ON childLevel.parentId = Lookup.internalId "
-              "INNER JOIN Resources grandChildLevel ON childLevel.parentId = Lookup.internalId "
-              "INNER JOIN Lookup ON MainDicomTags.id = grandChildLevel.internalId ";
-
-        SQLite::Statement s(db_, SQLITE_FROM_HERE, sql);
-        while (s.Step())
-        {
-          FindResponse::Resource& res = response.GetResourceByInternalId(s.ColumnInt64(0));
-          res.AddChildrenMainDicomTagValue(static_cast<ResourceType>(requestLevel + 2), 
-                                DicomTag(static_cast<uint16_t>(s.ColumnInt(1)),
-                                         static_cast<uint16_t>(s.ColumnInt(2))),
-                                s.ColumnString(3));
-        }
+        sql += "UNION SELECT "
+                "  " TOSTRING(QUERY_GRAND_CHILDREN_MAIN_DICOM_TAGS) " AS c0_queryId, "
+                "  Lookup.internalId AS c1_internalId, "
+                "  NULL AS c2_rowNumber, "
+                "  value AS c3_string1, "
+                "  NULL AS c4_string2, "
+                "  NULL AS c5_string3, "
+                "  tagGroup AS c6_int1, "
+                "  tagElement AS c7_int2, "
+                "  NULL AS c8_big_int1, "
+                "  NULL AS c9_big_int2 "
+                "FROM Lookup "
+                "  INNER JOIN Resources childLevel ON childLevel.parentId = Lookup.internalId "
+                "  INNER JOIN Resources grandChildLevel ON grandChildLevel.parentId = childLevel.internalId "
+                "  INNER JOIN MainDicomTags ON MainDicomTags.id = grandChildLevel.internalId AND " + JoinRequestedTags(request.GetChildrenSpecification(static_cast<ResourceType>(requestLevel + 2))); 
       }
 
       // need parent identifier ?
       if (request.IsRetrieveParentIdentifier())
       {
-        sql = "SELECT currentLevel.internalId, parentLevel.publicId "
-              "FROM Resources AS currentLevel "
-              "INNER JOIN Lookup ON currentLevel.internalId = Lookup.internalId "
-              "INNER JOIN Resources parentLevel ON currentLevel.parentId = parentLevel.internalId ";
-
-        SQLite::Statement s(db_, SQLITE_FROM_HERE, sql);
-        while (s.Step())
-        {
-          FindResponse::Resource& res = response.GetResourceByInternalId(s.ColumnInt64(0));
-          res.SetParentIdentifier(s.ColumnString(1));
-        }
-      }
-
-      // need resource metadata ?
-      if (request.IsRetrieveMetadata())
-      {
-        sql = "SELECT id, type, value "
-              "FROM Metadata "
-              "INNER JOIN Lookup ON Metadata.id = Lookup.internalId";
-
-        SQLite::Statement s(db_, SQLITE_FROM_HERE, sql);
-        while (s.Step())
-        {
-          FindResponse::Resource& res = response.GetResourceByInternalId(s.ColumnInt64(0));
-          res.AddMetadata(requestLevel,
-                          static_cast<MetadataType>(s.ColumnInt(1)),
-                          s.ColumnString(2));
-        }
-      }
-
-      // need resource labels ?
-      if (request.IsRetrieveLabels())
-      {
-        sql = "SELECT Lookup.internalId, label "
-              "FROM Labels "
-              "INNER JOIN Lookup ON Labels.id = Lookup.internalId";
-
-        SQLite::Statement s(db_, SQLITE_FROM_HERE, sql);
-        while (s.Step())
-        {
-          FindResponse::Resource& res = response.GetResourceByInternalId(s.ColumnInt64(0));
-          res.AddLabel(s.ColumnString(1));
-        }
-      }
-
-      // need one instance identifier ?  TODO: it might be actually more interesting to retrieve directly the attachment ids ....
-      if (request.IsRetrieveOneInstanceIdentifier())
-      {
-        if (requestLevel == ResourceType_Series)
-        {
-          sql = "SELECT Lookup.internalId, childLevel.publicId "
-                "FROM Resources AS childLevel "
-                "INNER JOIN Lookup ON childLevel.parentId = Lookup.internalId ";
-
-          SQLite::Statement s(db_, SQLITE_FROM_HERE, sql);
-          while (s.Step())
-          {
-            FindResponse::Resource& res = response.GetResourceByInternalId(s.ColumnInt64(0));
-            res.AddChildIdentifier(ResourceType_Instance, s.ColumnString(1));
-          }
-        }
-        else if (requestLevel == ResourceType_Study)
-        {
-          sql = "SELECT Lookup.internalId, grandChildLevel.publicId "
-                "FROM Resources AS grandChildLevel "
-                "INNER JOIN Resources childLevel ON grandChildLevel.parentId = childLevel.internalId "
-                "INNER JOIN Lookup ON childLevel.parentId = Lookup.internalId ";
-
-          SQLite::Statement s(db_, SQLITE_FROM_HERE, sql);
-          while (s.Step())
-          {
-            FindResponse::Resource& res = response.GetResourceByInternalId(s.ColumnInt64(0));
-            res.AddChildIdentifier(ResourceType_Instance, s.ColumnString(1));
-          }
-        }
-        else if (requestLevel == ResourceType_Patient)
-        {
-          sql = "SELECT Lookup.internalId, grandGrandChildLevel.publicId "
-                "FROM Resources AS grandGrandChildLevel "
-                "INNER JOIN Resources grandChildLevel ON grandGrandChildLevel.parentId = grandChildLevel.internalId "
-                "INNER JOIN Resources childLevel ON grandChildLevel.parentId = childLevel.internalId "
-                "INNER JOIN Lookup ON childLevel.parentId = Lookup.internalId ";
-
-          SQLite::Statement s(db_, SQLITE_FROM_HERE, sql);
-          while (s.Step())
-          {
-            FindResponse::Resource& res = response.GetResourceByInternalId(s.ColumnInt64(0));
-            res.AddChildIdentifier(ResourceType_Instance, s.ColumnString(1));
-          }
-        }
-        else
-        {
-          throw OrthancException(ErrorCode_InternalError);
-        }
+        sql += "UNION SELECT "
+               "  " TOSTRING(QUERY_PARENT_IDENTIFIER) " AS c0_queryId, "
+               "  Lookup.internalId AS c1_internalId, "
+               "  NULL AS c2_rowNumber, "
+               "  parentLevel.publicId AS c3_string1, "
+               "  NULL AS c4_string2, "
+               "  NULL AS c5_string3, "
+               "  NULL AS c6_int1, "
+               "  NULL AS c7_int2, "
+               "  NULL AS c8_big_int1, "
+               "  NULL AS c9_big_int2 "
+               "FROM Lookup "
+               "  INNER JOIN Resources currentLevel ON currentLevel.internalId = Lookup.internalId "
+               "  INNER JOIN Resources parentLevel ON currentLevel.parentId = parentLevel.internalId ";
       }
 
       // need children metadata ?
       if (requestLevel <= ResourceType_Series && request.GetChildrenSpecification(static_cast<ResourceType>(requestLevel + 1)).GetMetadata().size() > 0)
       {
-        sql = "SELECT Lookup.internalId, type, value "
-              "FROM Metadata "
-              "INNER JOIN Lookup ON Lookup.internalId = childLevel.parentId "
-              "INNER JOIN Resources childLevel ON childLevel.internalId = Metadata.id";
-
-        SQLite::Statement s(db_, SQLITE_FROM_HERE, sql);
-        while (s.Step())
-        {
-          FindResponse::Resource& res = response.GetResourceByInternalId(s.ColumnInt64(0));
-          res.AddChildrenMetadataValue(static_cast<ResourceType>(requestLevel + 1), 
-                                       static_cast<MetadataType>(s.ColumnInt(1)),
-                                       s.ColumnString(2));
-        }
+        sql += "UNION SELECT "
+                "  " TOSTRING(QUERY_CHILDREN_METADATA) " AS c0_queryId, "
+                "  Lookup.internalId AS c1_internalId, "
+                "  NULL AS c2_rowNumber, "
+                "  value AS c3_string1, "
+                "  NULL AS c4_string2, "
+                "  NULL AS c5_string3, "
+                "  type AS c6_int1, "
+                "  NULL AS c7_int2, "
+                "  NULL AS c8_big_int1, "
+                "  NULL AS c9_big_int2 "
+                "FROM Lookup "
+                "  INNER JOIN Resources childLevel ON childLevel.parentId = Lookup.internalId "
+                "  INNER JOIN Metadata ON Metadata.id = childLevel.internalId AND Metadata.type IN (" + JoinRequestedMetadata(request.GetChildrenSpecification(static_cast<ResourceType>(requestLevel + 1))) + ") ";
       }
 
       // need grandchildren metadata ?
       if (requestLevel <= ResourceType_Study && request.GetChildrenSpecification(static_cast<ResourceType>(requestLevel + 2)).GetMetadata().size() > 0)
       {
-        sql = "SELECT Lookup.internalId, type, value "
-              "FROM Metadata "
-              "INNER JOIN Lookup ON Lookup.internalId = childLevel.parentId "
-              "INNER JOIN Resources childLevel ON childLevel.internalId = grandChildLevel.parentId "
-              "INNER JOIN Resources grandChildLevel ON grandChildLevel.internalId = Metadata.id";
-
-        SQLite::Statement s(db_, SQLITE_FROM_HERE, sql);
-        while (s.Step())
-        {
-          FindResponse::Resource& res = response.GetResourceByInternalId(s.ColumnInt64(0));
-          res.AddChildrenMetadataValue(static_cast<ResourceType>(requestLevel + 2), 
-                                       static_cast<MetadataType>(s.ColumnInt(1)),
-                                       s.ColumnString(2));
-        }
+        sql += "UNION SELECT "
+                "  " TOSTRING(QUERY_GRAND_CHILDREN_METADATA) " AS c0_queryId, "
+                "  Lookup.internalId AS c1_internalId, "
+                "  NULL AS c2_rowNumber, "
+                "  value AS c3_string1, "
+                "  NULL AS c4_string2, "
+                "  NULL AS c5_string3, "
+                "  type AS c6_int1, "
+                "  NULL AS c7_int2, "
+                "  NULL AS c8_big_int1, "
+                "  NULL AS c9_big_int2 "
+                "FROM Lookup "
+                "  INNER JOIN Resources childLevel ON childLevel.parentId = Lookup.internalId "
+                "  INNER JOIN Resources grandChildLevel ON grandChildLevel.parentId = childLevel.internalId "
+                "  INNER JOIN Metadata ON Metadata.id = grandChildLevel.internalId AND Metadata.type IN (" + JoinRequestedMetadata(request.GetChildrenSpecification(static_cast<ResourceType>(requestLevel + 2))) + ") ";
       }
 
       // need children identifiers ?
-      if (requestLevel <= ResourceType_Series && request.GetChildrenSpecification(static_cast<ResourceType>(requestLevel + 1)).IsRetrieveIdentifiers())
+      if ((requestLevel == ResourceType_Patient && request.GetChildrenSpecification(ResourceType_Study).IsRetrieveIdentifiers()) ||
+          (requestLevel == ResourceType_Study && request.GetChildrenSpecification(ResourceType_Series).IsRetrieveIdentifiers()) ||
+          (requestLevel == ResourceType_Series && request.GetChildrenSpecification(ResourceType_Instance).IsRetrieveIdentifiers()))
       {
-        sql = "SELECT Lookup.internalId, childLevel.publicId "
-              "FROM Resources AS currentLevel "
-              "INNER JOIN Lookup ON currentLevel.internalId = Lookup.internalId "
-              "INNER JOIN Resources childLevel ON currentLevel.internalId = childLevel.parentId ";
-
-        SQLite::Statement s(db_, SQLITE_FROM_HERE, sql);
-        while (s.Step())
-        {
-          FindResponse::Resource& res = response.GetResourceByInternalId(s.ColumnInt64(0));
-          res.AddChildIdentifier(static_cast<ResourceType>(requestLevel + 1), s.ColumnString(1));
-        }
+        sql += "UNION SELECT "
+               "  " TOSTRING(QUERY_CHILDREN_IDENTIFIERS) " AS c0_queryId, "
+               "  Lookup.internalId AS c1_internalId, "
+               "  NULL AS c2_rowNumber, "
+               "  childLevel.publicId AS c3_string1, "
+               "  NULL AS c4_string2, "
+               "  NULL AS c5_string3, "
+               "  NULL AS c6_int1, "
+               "  NULL AS c7_int2, "
+               "  NULL AS c8_big_int1, "
+               "  NULL AS c9_big_int2 "
+               "FROM Lookup "
+               "  INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId ";
       }
 
       // need grandchildren identifiers ?
-      if (requestLevel <= ResourceType_Study && request.GetChildrenSpecification(static_cast<ResourceType>(requestLevel + 2)).IsRetrieveIdentifiers())
+      if ((requestLevel == ResourceType_Patient && request.GetChildrenSpecification(ResourceType_Series).IsRetrieveIdentifiers()) ||
+          (requestLevel == ResourceType_Study && request.GetChildrenSpecification(ResourceType_Instance).IsRetrieveIdentifiers()))
       {
-        sql = "SELECT Lookup.internalId, grandChildLevel.publicId "
-              "FROM Resources AS currentLevel "
-              "INNER JOIN Lookup ON currentLevel.internalId = Lookup.internalId "
-              "INNER JOIN Resources childLevel ON currentLevel.internalId = childLevel.parentId "
+        sql += "UNION SELECT "
+              "  " TOSTRING(QUERY_GRAND_CHILDREN_IDENTIFIERS) " AS c0_queryId, "
+              "  Lookup.internalId AS c1_internalId, "
+              "  NULL AS c2_rowNumber, "
+              "  grandChildLevel.publicId AS c3_string1, "
+              "  NULL AS c4_string2, "
+              "  NULL AS c5_string3, "
+              "  NULL AS c6_int1, "
+              "  NULL AS c7_int2, "
+              "  NULL AS c8_big_int1, "
+              "  NULL AS c9_big_int2 "
+              "FROM Lookup "
+              "INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId "
               "INNER JOIN Resources grandChildLevel ON childLevel.internalId = grandChildLevel.parentId ";
+      }
 
-        SQLite::Statement s(db_, SQLITE_FROM_HERE, sql);
-        while (s.Step())
-        {
-          FindResponse::Resource& res = response.GetResourceByInternalId(s.ColumnInt64(0));
-          res.AddChildIdentifier(static_cast<ResourceType>(requestLevel + 2), s.ColumnString(1));
-        }
+      // need grandgrandchildren identifiers ?
+      if (requestLevel == ResourceType_Patient && request.GetChildrenSpecification(ResourceType_Instance).IsRetrieveIdentifiers())
+      {
+        sql += "UNION SELECT "
+              "  " TOSTRING(QUERY_GRAND_GRAND_CHILDREN_IDENTIFIERS) " AS c0_queryId, "
+              "  Lookup.internalId AS c1_internalId, "
+              "  NULL AS c2_rowNumber, "
+              "  grandGrandChildLevel.publicId AS c3_string1, "
+              "  NULL AS c4_string2, "
+              "  NULL AS c5_string3, "
+              "  NULL AS c6_int1, "
+              "  NULL AS c7_int2, "
+              "  NULL AS c8_big_int1, "
+              "  NULL AS c9_big_int2 "
+              "FROM Lookup "
+              "INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId "
+              "INNER JOIN Resources grandChildLevel ON childLevel.internalId = grandChildLevel.parentId "
+              "INNER JOIN Resources grandGrandChildLevel ON grandChildLevel.internalId = grandGrandChildLevel.parentId ";
       }
 
-      // need resource attachments ?
-      if (request.IsRetrieveAttachments())
+
+      sql += " ORDER BY c0_queryId, c2_rowNumber";  // this is really important to make sure that the Lookup query is the first one to provide results since we use it to create the responses element !
+
+      SQLite::Statement s(db_, SQLITE_FROM_HERE_DYNAMIC(sql), sql);
+      formatter.Bind(s);
+
+      while (s.Step())
       {
-        sql = "SELECT Lookup.internalId, fileType, uuid, uncompressedSize, compressedSize, compressionType, uncompressedMD5, compressedMD5 "
-              "FROM AttachedFiles "
-              "INNER JOIN Lookup ON AttachedFiles.id = Lookup.internalId";
+        int queryId = s.ColumnInt(C0_QUERY_ID);
+        int64_t internalId = s.ColumnInt64(C1_INTERNAL_ID);
+
+        // LOG(INFO) << queryId << ": " << internalId;
+        // continue;
+
+        assert(queryId == QUERY_LOOKUP || response.HasResource(internalId)); // the QUERY_LOOKUP must be read first and must create the response before any other query tries to populate the fields
+
+        switch (queryId)
+        {
+          case QUERY_LOOKUP:
+            response.Add(new FindResponse::Resource(requestLevel, internalId, s.ColumnString(C3_STRING_1)));
+            break;
+
+          case QUERY_LABELS:
+          {
+            FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
+            res.AddLabel(s.ColumnString(C3_STRING_1));
+          }; break;
+
+          case QUERY_ATTACHMENTS:
+          {
+            FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
+            FileInfo file(s.ColumnString(C3_STRING_1), static_cast<FileContentType>(s.ColumnInt(C6_INT_1)), 
+                          s.ColumnInt64(C8_BIG_INT_1), s.ColumnString(C4_STRING_2),
+                          static_cast<CompressionType>(s.ColumnInt(C7_INT_2)),
+                          s.ColumnInt64(C9_BIG_INT_2), s.ColumnString(C5_STRING_3));
+            res.AddAttachment(file);
+          }; break;
+
+          case QUERY_MAIN_DICOM_TAGS:
+          {
+            FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
+            res.AddStringDicomTag(requestLevel, 
+                                  static_cast<uint16_t>(s.ColumnInt(C6_INT_1)),
+                                  static_cast<uint16_t>(s.ColumnInt(C7_INT_2)),
+                                  s.ColumnString(C3_STRING_1));
+          }; break;
+
+          case QUERY_PARENT_MAIN_DICOM_TAGS:
+          {
+            FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
+            res.AddStringDicomTag(static_cast<ResourceType>(requestLevel - 1), 
+                                  static_cast<uint16_t>(s.ColumnInt(C6_INT_1)),
+                                  static_cast<uint16_t>(s.ColumnInt(C7_INT_2)),
+                                  s.ColumnString(C3_STRING_1));
+          }; break;
+
+          case QUERY_GRAND_PARENT_MAIN_DICOM_TAGS:
+          {
+            FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
+            res.AddStringDicomTag(static_cast<ResourceType>(requestLevel - 2), 
+                                  static_cast<uint16_t>(s.ColumnInt(C6_INT_1)),
+                                  static_cast<uint16_t>(s.ColumnInt(C7_INT_2)),
+                                  s.ColumnString(C3_STRING_1));
+          }; break;
+
+          case QUERY_CHILDREN_MAIN_DICOM_TAGS:
+          {
+            FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
+            res.AddChildrenMainDicomTagValue(static_cast<ResourceType>(requestLevel + 1), 
+                                             DicomTag(static_cast<uint16_t>(s.ColumnInt(C6_INT_1)), static_cast<uint16_t>(s.ColumnInt(C7_INT_2))),
+                                             s.ColumnString(C3_STRING_1));
+          }; break;
+
+          case QUERY_GRAND_CHILDREN_MAIN_DICOM_TAGS:
+          {
+            FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
+            res.AddChildrenMainDicomTagValue(static_cast<ResourceType>(requestLevel + 2), 
+                                             DicomTag(static_cast<uint16_t>(s.ColumnInt(C6_INT_1)), static_cast<uint16_t>(s.ColumnInt(C7_INT_2))),
+                                             s.ColumnString(C3_STRING_1));
+          }; break;
 
-        SQLite::Statement s(db_, SQLITE_FROM_HERE, sql);
-        while (s.Step())
-        {
-          FindResponse::Resource& res = response.GetResourceByInternalId(s.ColumnInt64(0));
-          FileInfo file(s.ColumnString(2), static_cast<FileContentType>(s.ColumnInt(1)), 
-                        s.ColumnInt64(3), s.ColumnString(6),
-                        static_cast<CompressionType>(s.ColumnInt(5)),
-                        s.ColumnInt64(4), s.ColumnString(7));
-          res.AddAttachment(file);
+          case QUERY_METADATA:
+          {
+            FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
+            res.AddMetadata(static_cast<ResourceType>(requestLevel), 
+                            static_cast<MetadataType>(s.ColumnInt(C6_INT_1)),
+                            s.ColumnString(C3_STRING_1));
+          }; break;
+
+          case QUERY_PARENT_METADATA:
+          {
+            FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
+            res.AddMetadata(static_cast<ResourceType>(requestLevel - 1), 
+                            static_cast<MetadataType>(s.ColumnInt(C6_INT_1)),
+                            s.ColumnString(C3_STRING_1));
+          }; break;
+
+          case QUERY_GRAND_PARENT_METADATA:
+          {
+            FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
+            res.AddMetadata(static_cast<ResourceType>(requestLevel - 2), 
+                            static_cast<MetadataType>(s.ColumnInt(C6_INT_1)),
+                            s.ColumnString(C3_STRING_1));
+          }; break;
+
+          case QUERY_CHILDREN_METADATA:
+          {
+            FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
+            res.AddChildrenMetadataValue(static_cast<ResourceType>(requestLevel + 1), 
+                                         static_cast<MetadataType>(s.ColumnInt(C6_INT_1)),
+                                         s.ColumnString(C3_STRING_1));
+          }; break;
+
+          case QUERY_GRAND_CHILDREN_METADATA:
+          {
+            FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
+            res.AddChildrenMetadataValue(static_cast<ResourceType>(requestLevel + 2), 
+                                         static_cast<MetadataType>(s.ColumnInt(C6_INT_1)),
+                                         s.ColumnString(C3_STRING_1));
+          }; break;
+
+          case QUERY_PARENT_IDENTIFIER:
+          {
+            FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
+            res.SetParentIdentifier(s.ColumnString(C3_STRING_1));
+          }; break;
+
+          case QUERY_CHILDREN_IDENTIFIERS:
+          {
+            FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
+            res.AddChildIdentifier(static_cast<ResourceType>(requestLevel + 1),
+                                   s.ColumnString(C3_STRING_1));
+          }; break;
+
+          case QUERY_GRAND_CHILDREN_IDENTIFIERS:
+          {
+            FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
+            res.AddChildIdentifier(static_cast<ResourceType>(requestLevel + 2),
+                                   s.ColumnString(C3_STRING_1));
+          }; break;
+
+          case QUERY_GRAND_GRAND_CHILDREN_IDENTIFIERS:
+          {
+            FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
+            res.AddChildIdentifier(static_cast<ResourceType>(requestLevel + 3),
+                                   s.ColumnString(C3_STRING_1));
+          }; break;
+
+          case QUERY_ONE_INSTANCE_IDENTIFIER:
+          {
+            FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
+            res.SetOneInstancePublicId(s.ColumnString(C3_STRING_1));
+          }; break;
+
+          case QUERY_ONE_INSTANCE_METADATA:
+          {
+            FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
+            res.AddOneInstanceMetadata(static_cast<MetadataType>(s.ColumnInt(C6_INT_1)), s.ColumnString(C3_STRING_1));
+          }; break;
+
+          case QUERY_ONE_INSTANCE_ATTACHMENTS:
+          {
+            FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
+            FileInfo file(s.ColumnString(C3_STRING_1), static_cast<FileContentType>(s.ColumnInt(C6_INT_1)), 
+                          s.ColumnInt64(C8_BIG_INT_1), s.ColumnString(C4_STRING_2),
+                          static_cast<CompressionType>(s.ColumnInt(C7_INT_2)),
+                          s.ColumnInt64(C9_BIG_INT_2), s.ColumnString(C5_STRING_3));
+            res.AddOneInstanceAttachment(file);
+          }; break;
+
+          default:
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
         }
-
       }
     }
 
-
-
     // From the "ICreateInstance" interface
     virtual void AttachChild(int64_t parent,
                              int64_t child) ORTHANC_OVERRIDE
@@ -834,17 +1242,11 @@
                                  int64_t since,
                                  uint32_t limit) ORTHANC_OVERRIDE
     {
-      if (limit == 0)
-      {
-        target.clear();
-        return;
-      }
-
       SQLite::Statement s(db_, SQLITE_FROM_HERE,
                           "SELECT publicId FROM Resources WHERE "
                           "resourceType=? LIMIT ? OFFSET ?");
       s.BindInt(0, resourceType);
-      s.BindInt64(1, limit);
+      s.BindInt64(1, limit == 0 ? -1 : limit);  // In SQLite, setting "LIMIT" to "-1" means "no limit"
       s.BindInt64(2, since);
 
       target.clear();
@@ -860,10 +1262,72 @@
                             int64_t since,
                             uint32_t limit) ORTHANC_OVERRIDE
     {
-      SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes WHERE seq>? ORDER BY seq LIMIT ?");
-      s.BindInt64(0, since);
-      s.BindInt(1, limit + 1);
-      GetChangesInternal(target, done, s, limit);
+      std::set<ChangeType> filter;
+      GetChangesExtended(target, done, since, -1, limit, filter);
+    }
+
+    virtual void GetChangesExtended(std::list<ServerIndexChange>& target /*out*/,
+                                    bool& done /*out*/,
+                                    int64_t since,
+                                    int64_t to,
+                                    uint32_t limit,
+                                    const std::set<ChangeType>& filterType) ORTHANC_OVERRIDE
+    {
+      std::vector<std::string> filters;
+      bool hasSince = false;
+      bool hasTo = false;
+
+      if (since > 0)
+      {
+        hasSince = true;
+        filters.push_back("seq>?");
+      }
+      if (to != -1)
+      {
+        hasTo = true;
+        filters.push_back("seq<=?");
+      }
+      if (filterType.size() != 0)
+      {
+        filters.push_back("changeType IN ( " + JoinChanges(filterType) +  " )");
+      }
+
+      std::string filtersString;
+      if (filters.size() > 0)
+      {
+        Toolbox::JoinStrings(filtersString, filters, " AND ");
+        filtersString = "WHERE " + filtersString;
+      }
+
+      std::string sql;
+      bool returnFirstResults;
+      if (hasTo && !hasSince)
+      {
+        // in this case, we want the largest values in the LIMIT clause but we want them ordered in ascending order
+        sql = "SELECT * FROM (SELECT * FROM Changes " + filtersString + " ORDER BY seq DESC LIMIT ?) ORDER BY seq ASC";
+        returnFirstResults = false;
+      }
+      else
+      {
+        // default query: we want the smallest values ordered in ascending order
+        sql = "SELECT * FROM Changes " + filtersString + " ORDER BY seq ASC LIMIT ?";
+        returnFirstResults = true;
+      }
+       
+      SQLite::Statement s(db_, SQLITE_FROM_HERE_DYNAMIC(sql), sql);
+
+      int paramCounter = 0;
+      if (hasSince)
+      {
+        s.BindInt64(paramCounter++, since);
+      }
+      if (hasTo)
+      {
+        s.BindInt64(paramCounter++, to);
+      }
+
+      s.BindInt(paramCounter++, limit + 1); // we take limit+1 because we use the +1 to know if "Done" must be set to true
+      GetChangesInternal(target, done, s, limit, returnFirstResults);
     }
 
 
@@ -924,7 +1388,7 @@
     {
       bool done;  // Ignored
       SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes ORDER BY seq DESC LIMIT 1");
-      GetChangesInternal(target, done, s, 1);
+      GetChangesInternal(target, done, s, 1, true);
     }
 
 
@@ -1647,6 +2111,8 @@
     // TODO: implement revisions in SQLite
     dbCapabilities_.SetFlushToDisk(true);
     dbCapabilities_.SetLabelsSupport(true);
+    dbCapabilities_.SetHasExtendedChanges(true);
+    dbCapabilities_.SetHasFindSupport(true);
     db_.Open(path);
   }
 
@@ -1659,6 +2125,8 @@
     // TODO: implement revisions in SQLite
     dbCapabilities_.SetFlushToDisk(true);
     dbCapabilities_.SetLabelsSupport(true);
+    dbCapabilities_.SetHasExtendedChanges(true);
+    dbCapabilities_.SetHasFindSupport(true);
     db_.OpenInMemory();
   }
 
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h	Wed Oct 09 11:01:11 2024 +0200
@@ -57,7 +57,8 @@
     void GetChangesInternal(std::list<ServerIndexChange>& target,
                             bool& done,
                             SQLite::Statement& s,
-                            uint32_t maxResults);
+                            uint32_t maxResults,
+                            bool returnFirstResults);
 
     void GetExportedResourcesInternal(std::list<ExportedResource>& target,
                                       bool& done,
@@ -101,7 +102,8 @@
 
     virtual bool HasIntegratedFind() const ORTHANC_OVERRIDE
     {
-      return true;
+      return true;   // => This uses specialized SQL commands
+      //return false;   // => This uses Compatibility/GenericFind
     }
 
     /**
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Wed Oct 09 11:01:11 2024 +0200
@@ -281,6 +281,10 @@
     }
     
     target["Last"] = static_cast<int>(last);
+    if (!log.empty())
+    {
+      target["First"] = static_cast<int>(log.front().GetSeq());
+    }
   }
 
 
@@ -477,6 +481,10 @@
         else
         {
           assert(writeOperations != NULL);
+          if (readOnly_)
+          {
+            throw OrthancException(ErrorCode_ReadOnly, "The DB is trying to execute a ReadWrite transaction while Orthanc has been started in ReadOnly mode.");
+          }
           
           Transaction transaction(db_, *factory_, TransactionType_ReadWrite);
           {
@@ -514,10 +522,11 @@
   }
 
   
-  StatelessDatabaseOperations::StatelessDatabaseOperations(IDatabaseWrapper& db) : 
+  StatelessDatabaseOperations::StatelessDatabaseOperations(IDatabaseWrapper& db, bool readOnly) : 
     db_(db),
     mainDicomTagsRegistry_(new MainDicomTagsRegistry),
-    maxRetries_(0)
+    maxRetries_(0),
+    readOnly_(readOnly)
   {
   }
 
@@ -754,7 +763,7 @@
       }
     };
 
-    if (GetDatabaseCapabilities().HasUpdateAndGetStatistics())
+    if (GetDatabaseCapabilities().HasUpdateAndGetStatistics() && !IsReadOnly())
     {
       Operations operations;
       Apply(operations);
@@ -804,6 +813,39 @@
   }
 
 
+  void StatelessDatabaseOperations::GetChangesExtended(Json::Value& target,
+                                                       int64_t since,
+                                                       int64_t to,                               
+                                                       unsigned int maxResults,
+                                                       const std::set<ChangeType>& changeType)
+  {
+    class Operations : public ReadOnlyOperationsT5<Json::Value&, int64_t, int64_t, unsigned int, const std::set<ChangeType>&>
+    {
+    public:
+      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
+                              const Tuple& tuple) ORTHANC_OVERRIDE
+      {
+        std::list<ServerIndexChange> changes;
+        bool done;
+        bool hasLast = false;
+        int64_t last = 0;
+
+        transaction.GetChangesExtended(changes, done, tuple.get<1>(), tuple.get<2>(), tuple.get<3>(), tuple.get<4>());
+        if (changes.empty())
+        {
+          last = transaction.GetLastChangeIndex();
+          hasLast = true;
+        }
+
+        FormatLog(tuple.get<0>(), changes, "Changes", done, tuple.get<1>(), hasLast, last);
+      }
+    };
+    
+    Operations operations;
+    operations.Apply(*this, target, since, to, maxResults, changeType);
+  }
+
+
   void StatelessDatabaseOperations::GetLastChange(Json::Value& target)
   {
     class Operations : public ReadOnlyOperationsT1<Json::Value&>
@@ -1272,7 +1314,7 @@
 
     DicomTagConstraint c(tag, ConstraintType_Equal, value, true, true);
 
-    DatabaseConstraints query;
+    DatabaseDicomTagConstraints query;
     bool isIdentical;  // unused
     query.AddConstraint(c.ConvertToDatabaseConstraint(isIdentical, level, DicomTagType_Identifier));
 
@@ -1281,12 +1323,12 @@
     {
     private:
       std::vector<std::string>&   result_;
-      const DatabaseConstraints&  query_;
+      const DatabaseDicomTagConstraints&  query_;
       ResourceType                level_;
       
     public:
       Operations(std::vector<std::string>& result,
-                 const DatabaseConstraints& query,
+                 const DatabaseDicomTagConstraints& query,
                  ResourceType level) :
         result_(result),
         query_(query),
@@ -2433,8 +2475,8 @@
   }
 
 
-  bool StatelessDatabaseOperations::ReadWriteTransaction::HasReachedMaxStorageSize(uint64_t maximumStorageSize,
-                                                                                   uint64_t addedInstanceSize)
+  bool StatelessDatabaseOperations::ReadOnlyTransaction::HasReachedMaxStorageSize(uint64_t maximumStorageSize,
+                                                                                  uint64_t addedInstanceSize)
   {
     if (maximumStorageSize != 0)
     {
@@ -2455,8 +2497,8 @@
     return false;
   }                                                                           
 
-  bool StatelessDatabaseOperations::ReadWriteTransaction::HasReachedMaxPatientCount(unsigned int maximumPatientCount,
-                                                                                    const std::string& patientId)
+  bool StatelessDatabaseOperations::ReadOnlyTransaction::HasReachedMaxPatientCount(unsigned int maximumPatientCount,
+                                                                                   const std::string& patientId)
   {
     if (maximumPatientCount != 0)
     {
@@ -3323,6 +3365,17 @@
     return db_.GetDatabaseCapabilities().HasLabelsSupport();
   }
 
+  bool StatelessDatabaseOperations::HasExtendedChanges()
+  {
+    boost::shared_lock<boost::shared_mutex> lock(mutex_);
+    return db_.GetDatabaseCapabilities().HasExtendedChanges();
+  }
+
+  bool StatelessDatabaseOperations::HasFindSupport()
+  {
+    boost::shared_lock<boost::shared_mutex> lock(mutex_);
+    return db_.GetDatabaseCapabilities().HasFindSupport();
+  }
 
   void StatelessDatabaseOperations::ExecuteFind(FindResponse& response,
                                                 const FindRequest& request)
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Wed Oct 09 11:01:11 2024 +0200
@@ -125,7 +125,7 @@
 
       void ApplyLookupResources(std::list<std::string>& resourcesId,
                                 std::list<std::string>* instancesId, // Can be NULL if not needed
-                                const DatabaseConstraints& lookup,
+                                const DatabaseDicomTagConstraints& lookup,
                                 ResourceType queryLevel,
                                 const std::set<std::string>& labels,  // New in Orthanc 1.12.0
                                 LabelsConstraint labelsConstraint,    // New in Orthanc 1.12.0
@@ -163,6 +163,16 @@
         transaction_.GetChanges(target, done, since, limit);
       }
 
+      void GetChangesExtended(std::list<ServerIndexChange>& target /*out*/,
+                              bool& done /*out*/,
+                              int64_t since,
+                              int64_t to,
+                              uint32_t limit,
+                              const std::set<ChangeType>& filterType)
+      {
+        transaction_.GetChangesExtended(target, done, since, to, limit, filterType);
+      }
+
       void GetChildrenInternalId(std::list<int64_t>& target,
                                  int64_t id)
       {
@@ -295,6 +305,12 @@
         transaction_.ListAllLabels(target);
       }
 
+      bool HasReachedMaxStorageSize(uint64_t maximumStorageSize,
+                                    uint64_t addedInstanceSize);
+
+      bool HasReachedMaxPatientCount(unsigned int maximumPatientCount,
+                                     const std::string& patientId);
+
       void ExecuteFind(FindResponse& response,
                        const FindRequest& request,
                        const IDatabaseWrapper::Capabilities& capabilities)
@@ -435,12 +451,6 @@
                    uint64_t addedInstanceSize,
                    const std::string& newPatientId);
 
-      bool HasReachedMaxStorageSize(uint64_t maximumStorageSize,
-                                    uint64_t addedInstanceSize);
-
-      bool HasReachedMaxPatientCount(unsigned int maximumPatientCount,
-                                     const std::string& patientId);
-      
       bool IsRecyclingNeeded(uint64_t maximumStorageSize,
                              unsigned int maximumPatients,
                              uint64_t addedInstanceSize,
@@ -492,6 +502,7 @@
     boost::shared_mutex                          mutex_;
     std::unique_ptr<ITransactionContextFactory>  factory_;
     unsigned int                                 maxRetries_;
+    bool                                         readOnly_;
 
     void ApplyInternal(IReadOnlyOperations* readOperations,
                        IReadWriteOperations* writeOperations);
@@ -501,8 +512,13 @@
                              uint64_t maximumStorageSize,
                              unsigned int maximumPatientCount);
 
+    bool IsReadOnly()
+    {
+      return readOnly_;
+    }
+
   public:
-    explicit StatelessDatabaseOperations(IDatabaseWrapper& database);
+    explicit StatelessDatabaseOperations(IDatabaseWrapper& database, bool readOnly);
 
     void SetTransactionContextFactory(ITransactionContextFactory* factory /* takes ownership */);
 
@@ -557,8 +573,18 @@
                     int64_t since,
                     uint32_t limit);
 
+    void GetChangesExtended(Json::Value& target,
+                            int64_t since,
+                            int64_t to,
+                            uint32_t limit,
+                            const std::set<ChangeType>& filterType);
+
     void GetLastChange(Json::Value& target);
 
+    bool HasExtendedChanges();
+
+    bool HasFindSupport();
+    
     void GetExportedResources(Json::Value& target,
                               int64_t since,
                               uint32_t limit);
--- a/OrthancServer/Sources/OrthancConfiguration.cpp	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/Sources/OrthancConfiguration.cpp	Wed Oct 09 11:01:11 2024 +0200
@@ -1161,6 +1161,14 @@
         {
           warning = Warnings_003_DecoderFailure;
         }
+        else if (name == "W004_NoMainDicomTagsSignature")
+        {
+          warning = Warnings_004_NoMainDicomTagsSignature;
+        }
+        else if (name == "W005_RequestingTagFromLowerResourceLevel")
+        {
+          warning = Warnings_005_RequestingTagFromLowerResourceLevel;
+        }
         else
         {
           throw OrthancException(ErrorCode_BadFileFormat, name + " is not recognized as a valid warning name");
--- a/OrthancServer/Sources/OrthancFindRequestHandler.cpp	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/Sources/OrthancFindRequestHandler.cpp	Wed Oct 09 11:01:11 2024 +0200
@@ -444,7 +444,7 @@
      * Run the query.
      **/
 
-    ResourceFinder finder(level, false /* don't expand */);
+    ResourceFinder finder(level, ResponseContentFlags_ID);
     finder.SetDatabaseLookup(lookup);
     finder.AddRequestedTags(requestedTags);
 
--- a/OrthancServer/Sources/OrthancInitialization.cpp	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/Sources/OrthancInitialization.cpp	Wed Oct 09 11:01:11 2024 +0200
@@ -261,7 +261,17 @@
             {
               LOG(INFO) << "  - " << tagName;
             }
-            DicomMap::AddMainDicomTag(tag, level);
+
+            try
+            {
+              DicomMap::AddMainDicomTag(tag, level);
+            }
+            catch(OrthancException& e)
+            {
+              LOG(WARNING) << "  - !!! " << tagName << " is already defined as a standard MainDicomTags, it is useless to include it in the ExtraMainDicomTags";
+            }
+            
+            
           }
         }
       }
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp	Wed Oct 09 11:01:11 2024 +0200
@@ -270,7 +270,16 @@
     RegisterAnonymizeModify();
     RegisterArchive();
 
-    Register("/instances", UploadDicomFile);
+    if (!context_.IsReadOnly())
+    {
+      Register("/instances", UploadDicomFile);
+    }
+    else
+    {
+      LOG(WARNING) << "READ-ONLY SYSTEM: deactivating POST /instances route";
+    }
+
+    
 
     // Auto-generated directories
     Register("/tools", RestApi::AutoListChildren);
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestChanges.cpp	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestChanges.cpp	Wed Oct 09 11:01:11 2024 +0200
@@ -29,14 +29,15 @@
 namespace Orthanc
 {
   // Changes API --------------------------------------------------------------
+  static const unsigned int DEFAULT_LIMIT = 100;
+  static const int64_t DEFAULT_TO = -1;
  
-  static void GetSinceAndLimit(int64_t& since,
-                               unsigned int& limit,
-                               bool& last,
-                               const RestApiGetCall& call)
+  static void GetSinceToAndLimit(int64_t& since,
+                                 int64_t& to,
+                                 unsigned int& limit,
+                                 bool& last,
+                                 const RestApiGetCall& call)
   {
-    static const unsigned int DEFAULT_LIMIT = 100;
-    
     if (call.HasArgument("last"))
     {
       last = true;
@@ -48,11 +49,13 @@
     try
     {
       since = boost::lexical_cast<int64_t>(call.GetArgument("since", "0"));
+      to = boost::lexical_cast<int64_t>(call.GetArgument("to", boost::lexical_cast<std::string>(DEFAULT_TO)));
       limit = boost::lexical_cast<unsigned int>(call.GetArgument("limit", boost::lexical_cast<std::string>(DEFAULT_LIMIT)));
     }
     catch (boost::bad_lexical_cast&)
     {
       since = 0;
+      to = DEFAULT_TO;
       limit = DEFAULT_LIMIT;
       return;
     }
@@ -66,33 +69,65 @@
         .SetTag("Tracking changes")
         .SetSummary("List changes")
         .SetDescription("Whenever Orthanc receives a new DICOM instance, this event is recorded in the so-called _Changes Log_. This enables remote scripts to react to the arrival of new DICOM resources. A typical application is auto-routing, where an external script waits for a new DICOM instance to arrive into Orthanc, then forward this instance to another modality. Please note that, when resources are deleted, their corresponding change entries are also removed from the Changes Log, which helps ensuring that this log does not grow indefinitely.")
+        .SetHttpGetArgument("last", RestApiCallDocumentation::Type_Number, "Request only the last change id (this argument must be used alone)", false)
         .SetHttpGetArgument("limit", RestApiCallDocumentation::Type_Number, "Limit the number of results", false)
-        .SetHttpGetArgument("since", RestApiCallDocumentation::Type_Number, "Show only the resources since the provided index", false)
+        .SetHttpGetArgument("since", RestApiCallDocumentation::Type_Number, "Show only the resources since the provided index excluded", false)
+        .SetHttpGetArgument("to", RestApiCallDocumentation::Type_Number, "Show only the resources till the provided index included (only available if your DB backend supports ExtendedChanges)", false)
+        .SetHttpGetArgument("type", RestApiCallDocumentation::Type_String, "Show only the changes of the provided type (only available if your DB backend supports ExtendedChanges)", false)
         .AddAnswerType(MimeType_Json, "The list of changes")
         .SetAnswerField("Changes", RestApiCallDocumentation::Type_JsonListOfObjects, "The individual changes")
         .SetAnswerField("Done", RestApiCallDocumentation::Type_Boolean,
-                        "Whether the last reported change is the last of the full history")
+                        "Whether the last reported change is the last of the full history.")
         .SetAnswerField("Last", RestApiCallDocumentation::Type_Number,
                         "The index of the last reported change, can be used for the `since` argument in subsequent calls to this route")
+        .SetAnswerField("First", RestApiCallDocumentation::Type_Number,
+                        "The index of the first reported change, its value-1 can be used for the `to` argument in subsequent calls to this route when browsing the changes in reverse order")
         .SetHttpGetSample("https://orthanc.uclouvain.be/demo/changes?since=0&limit=2", true);
       return;
     }
     
     ServerContext& context = OrthancRestApi::GetContext(call);
 
-    //std::string filter = GetArgument(getArguments, "filter", "");
-    int64_t since;
+    int64_t since, to;
+    std::set<ChangeType> filterType;
+
     unsigned int limit;
     bool last;
-    GetSinceAndLimit(since, limit, last, call);
+    GetSinceToAndLimit(since, to, limit, last, call);
+
+    std::string filterArgument = call.GetArgument("type", "all");
+    if (filterArgument != "all" && filterArgument != "All")
+    {
+      std::set<std::string> filterTypeStrings;
+      Toolbox::SplitString(filterTypeStrings, filterArgument, ';');
+
+      for (std::set<std::string>::const_iterator it = filterTypeStrings.begin(); it != filterTypeStrings.end(); ++it)
+      {
+        filterType.insert(StringToChangeType(*it));
+      }
+    }
 
     Json::Value result;
     if (last)
     {
       context.GetIndex().GetLastChange(result);
     }
+    else if (context.GetIndex().HasExtendedChanges())
+    {
+      context.GetIndex().GetChangesExtended(result, since, to, limit, filterType);
+    }
     else
     {
+      if (filterType.size() > 0)
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange, "CAPABILITIES: Trying to filter changes while the Database backend does not support it (requires a DB backend with support for ExtendedChanges)");
+      }
+
+      if (to != DEFAULT_TO)
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange, "CAPABILITIES: Trying to use the 'to' parameter in /changes while the Database backend does not support it (requires a DB backend with support for ExtendedChanges)");
+      }
+
       context.GetIndex().GetChanges(result, since, limit);
     }
 
@@ -139,10 +174,10 @@
 
     ServerContext& context = OrthancRestApi::GetContext(call);
 
-    int64_t since;
+    int64_t since, to;
     unsigned int limit;
     bool last;
-    GetSinceAndLimit(since, limit, last, call);
+    GetSinceToAndLimit(since, to, limit, last, call);
 
     Json::Value result;
     if (last)
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Wed Oct 09 11:01:11 2024 +0200
@@ -41,6 +41,7 @@
 
 #include "../OrthancConfiguration.h"
 #include "../Search/DatabaseLookup.h"
+#include "../Search/DatabaseMetadataConstraint.h"
 #include "../ServerContext.h"
 #include "../ServerToolbox.h"
 #include "../SliceOrdering.h"
@@ -136,7 +137,14 @@
                              DicomToJsonFormat format,
                              bool retrieveMetadata)
   {
-    ResourceFinder finder(level, true /* expand */);
+    ResponseContentFlags responseContent = ResponseContentFlags_ExpandTrue;
+    
+    if (retrieveMetadata)
+    {
+      responseContent = static_cast<ResponseContentFlags>(static_cast<uint32_t>(responseContent) | ResponseContentFlags_Metadata);
+    }
+
+    ResourceFinder finder(level, responseContent);
     finder.SetOrthancId(level, identifier);
     finder.SetRetrieveMetadata(retrieveMetadata);
 
@@ -177,7 +185,7 @@
     std::set<DicomTag> requestedTags;
     OrthancRestApi::GetRequestedTags(requestedTags, call);
 
-    ResourceFinder finder(resourceType, expand);
+    ResourceFinder finder(resourceType, (expand ? ResponseContentFlags_ExpandTrue : ResponseContentFlags_ID));
     finder.AddRequestedTags(requestedTags);
 
     if (call.HasArgument("limit") ||
@@ -235,7 +243,7 @@
 
     const DicomToJsonFormat format = OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human);
 
-    ResourceFinder finder(resourceType, true /* expand */);
+    ResourceFinder finder(resourceType, ResponseContentFlags_ExpandTrue);
     finder.AddRequestedTags(requestedTags);
     finder.SetOrthancId(resourceType, call.GetUriComponent("id", ""));
 
@@ -3042,6 +3050,15 @@
     static const char* const KEY_SINCE = "Since";
     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
+    static const char* const KEY_ORDER_BY = "OrderBy";                    // New in Orthanc 1.12.5
+    static const char* const KEY_ORDER_BY_KEY = "Key";                    // New in Orthanc 1.12.5
+    static const char* const KEY_ORDER_BY_TYPE = "Type";                  // New in Orthanc 1.12.5
+    static const char* const KEY_ORDER_BY_DIRECTION = "Direction";        // New in Orthanc 1.12.5
+    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_RESPONSE_CONTENT = "ResponseContent";    // New in Orthanc 1.12.5
 
     if (call.IsDocumentation())
     {
@@ -3075,6 +3092,20 @@
                          "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`)");
       return;
@@ -3125,6 +3156,12 @@
       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)
     {
@@ -3137,17 +3174,57 @@
       throw OrthancException(ErrorCode_BadRequest, 
                              "Field \"" + std::string(KEY_LABELS_CONSTRAINT) + "\" must be an array of strings");
     }
-    else
+    else if (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 (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");
+    }
+    else if (request.isMember(KEY_PARENT_PATIENT) &&
+             request[KEY_PARENT_PATIENT].type() != Json::stringValue)
+    {
+      throw OrthancException(ErrorCode_BadRequest, 
+                             "Field \"" + std::string(KEY_PARENT_PATIENT) + "\" must be a string");
+    }
+    else if (request.isMember(KEY_PARENT_STUDY) &&
+             request[KEY_PARENT_STUDY].type() != Json::stringValue)
     {
-      bool expand = false;
-      if (request.isMember(KEY_EXPAND))
+      throw OrthancException(ErrorCode_BadRequest, 
+                             "Field \"" + std::string(KEY_PARENT_STUDY) + "\" must be a string");
+    }
+    else if (request.isMember(KEY_PARENT_SERIES) &&
+             request[KEY_PARENT_SERIES].type() != Json::stringValue)
+    {
+      throw OrthancException(ErrorCode_BadRequest, 
+                             "Field \"" + std::string(KEY_PARENT_SERIES) + "\" must be a string");
+    }
+    else if (true)
+    {
+      ResponseContentFlags responseContent = ResponseContentFlags_ID;
+      
+      if (request.isMember(KEY_RESPONSE_CONTENT))
       {
-        expand = request[KEY_EXPAND].asBool();
+        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;
       }
 
       const ResourceType level = StringToResourceType(request[KEY_LEVEL].asCString());
 
-      ResourceFinder finder(level, expand);
+      ResourceFinder finder(level, responseContent);
       finder.SetDatabaseLimits(context.GetDatabaseLimits(level));
 
       const DicomToJsonFormat format = OrthancRestApi::GetDicomFormat(request, DicomToJsonFormat_Human);
@@ -3187,30 +3264,66 @@
           caseSensitive = request[KEY_CASE_SENSITIVE].asBool();
         }
 
-        DatabaseLookup query;
-
-        Json::Value::Members members = request[KEY_QUERY].getMemberNames();
-        for (size_t i = 0; i < members.size(); i++)
-        {
-          if (request[KEY_QUERY][members[i]].type() != Json::stringValue)
+        { // DICOM Tag query
+          DatabaseLookup dicomTagLookup;
+
+          Json::Value::Members members = request[KEY_QUERY].getMemberNames();
+          for (size_t i = 0; i < members.size(); i++)
           {
-            throw OrthancException(ErrorCode_BadRequest,
-                                   "Tag \"" + members[i] + "\" must be associated with a string");
+            if (request[KEY_QUERY][members[i]].type() != Json::stringValue)
+            {
+              throw OrthancException(ErrorCode_BadRequest,
+                                    "Tag \"" + members[i] + "\" must be associated with a string");
+            }
+
+            const std::string value = request[KEY_QUERY][members[i]].asString();
+
+            if (!value.empty())
+            {
+              // An empty string corresponds to an universal constraint,
+              // so we ignore it. This mimics the behavior of class
+              // "OrthancFindRequestHandler"
+              dicomTagLookup.AddRestConstraint(FromDcmtkBridge::ParseTag(members[i]),
+                                      value, caseSensitive, true);
+            }
           }
 
-          const std::string value = request[KEY_QUERY][members[i]].asString();
-
-          if (!value.empty())
+          finder.SetDatabaseLookup(dicomTagLookup);
+        }
+
+        { // Metadata query
+          Json::Value::Members members = request[KEY_QUERY_METADATA].getMemberNames();
+          for (size_t i = 0; i < members.size(); i++)
           {
-            // An empty string corresponds to an universal constraint,
-            // so we ignore it. This mimics the behavior of class
-            // "OrthancFindRequestHandler"
-            query.AddRestConstraint(FromDcmtkBridge::ParseTag(members[i]),
-                                    value, caseSensitive, true);
+            if (request[KEY_QUERY_METADATA][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();
+
+            if (!value.empty())
+            {
+              if (value.find('\\') != std::string::npos)
+              {
+                std::vector<std::string> items;
+                Toolbox::TokenizeString(items, value, '\\');
+                
+                finder.AddMetadataConstraint(new DatabaseMetadataConstraint(metadata, ConstraintType_List, items, caseSensitive));
+              }
+              else if (value.find('*') != std::string::npos || value.find('?') != std::string::npos)
+              {
+                finder.AddMetadataConstraint(new DatabaseMetadataConstraint(metadata, ConstraintType_Wildcard, value, caseSensitive));
+              }
+              else
+              {
+                finder.AddMetadataConstraint(new DatabaseMetadataConstraint(metadata, ConstraintType_Equal, value, caseSensitive));
+              }
+            }
           }
         }
-
-        finder.SetDatabaseLookup(query);
       }
 
       if (request.isMember(KEY_REQUESTED_TAGS))
@@ -3258,6 +3371,82 @@
         }
       }
 
+      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[KEY_ORDER_BY][i].type() != Json::objectValue)
+          {
+            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)
+            {
+              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);
@@ -3297,7 +3486,7 @@
     std::set<DicomTag> requestedTags;
     OrthancRestApi::GetRequestedTags(requestedTags, call);
 
-    ResourceFinder finder(end, expand);
+    ResourceFinder finder(end, (expand ? ResponseContentFlags_ExpandTrue : ResponseContentFlags_ID));
     finder.SetOrthancId(start, call.GetUriComponent("id", ""));
     finder.AddRequestedTags(requestedTags);
 
@@ -3920,13 +4109,23 @@
     Register("/series", ListResources<ResourceType_Series>);
     Register("/studies", ListResources<ResourceType_Study>);
 
-    Register("/instances/{id}", DeleteSingleResource<ResourceType_Instance>);
+    if (!context_.IsReadOnly())
+    {
+      Register("/instances/{id}", DeleteSingleResource<ResourceType_Instance>);
+      Register("/patients/{id}", DeleteSingleResource<ResourceType_Patient>);
+      Register("/series/{id}", DeleteSingleResource<ResourceType_Series>);
+      Register("/studies/{id}", DeleteSingleResource<ResourceType_Study>);
+
+      Register("/tools/bulk-delete", BulkDelete);
+    }
+    else
+    {
+      LOG(WARNING) << "READ-ONLY SYSTEM: deactivating DELETE routes";
+    }
+
     Register("/instances/{id}", GetSingleResource<ResourceType_Instance>);
-    Register("/patients/{id}", DeleteSingleResource<ResourceType_Patient>);
     Register("/patients/{id}", GetSingleResource<ResourceType_Patient>);
-    Register("/series/{id}", DeleteSingleResource<ResourceType_Series>);
     Register("/series/{id}", GetSingleResource<ResourceType_Series>);
-    Register("/studies/{id}", DeleteSingleResource<ResourceType_Study>);
     Register("/studies/{id}", GetSingleResource<ResourceType_Study>);
 
     Register("/instances/{id}/statistics", GetResourceStatistics);
@@ -3971,7 +4170,16 @@
     Register("/instances/{id}/numpy", GetNumpyInstance);  // New in Orthanc 1.10.0
 
     Register("/patients/{id}/protected", IsProtectedPatient);
-    Register("/patients/{id}/protected", SetPatientProtection);
+  
+    if (!context_.IsReadOnly())
+    {
+      Register("/patients/{id}/protected", SetPatientProtection);
+    }
+    else
+    {
+      LOG(WARNING) << "READ-ONLY SYSTEM: deactivating PUT /patients/{id}/protected route";
+    }
+
 
     std::vector<std::string> resourceTypes;
     resourceTypes.push_back("patients");
@@ -3989,14 +4197,15 @@
       // New in Orthanc 1.12.0
       Register("/" + resourceTypes[i] + "/{id}/labels", ListLabels);
       Register("/" + resourceTypes[i] + "/{id}/labels/{label}", GetLabel);
-      Register("/" + resourceTypes[i] + "/{id}/labels/{label}", RemoveLabel);
-      Register("/" + resourceTypes[i] + "/{id}/labels/{label}", AddLabel);
+
+      if (!context_.IsReadOnly())
+      {
+        Register("/" + resourceTypes[i] + "/{id}/labels/{label}", RemoveLabel);
+        Register("/" + resourceTypes[i] + "/{id}/labels/{label}", AddLabel);
+      }
 
       Register("/" + resourceTypes[i] + "/{id}/attachments", ListAttachments);
-      Register("/" + resourceTypes[i] + "/{id}/attachments/{name}", DeleteAttachment);
       Register("/" + resourceTypes[i] + "/{id}/attachments/{name}", GetAttachmentOperations);
-      Register("/" + resourceTypes[i] + "/{id}/attachments/{name}", UploadAttachment);
-      Register("/" + resourceTypes[i] + "/{id}/attachments/{name}/compress", ChangeAttachmentCompression<CompressionType_ZlibWithSize>);
       Register("/" + resourceTypes[i] + "/{id}/attachments/{name}/compressed-data", GetAttachmentData<0>);
       Register("/" + resourceTypes[i] + "/{id}/attachments/{name}/compressed-md5", GetAttachmentCompressedMD5);
       Register("/" + resourceTypes[i] + "/{id}/attachments/{name}/compressed-size", GetAttachmentCompressedSize);
@@ -4004,12 +4213,29 @@
       Register("/" + resourceTypes[i] + "/{id}/attachments/{name}/is-compressed", IsAttachmentCompressed);
       Register("/" + resourceTypes[i] + "/{id}/attachments/{name}/md5", GetAttachmentMD5);
       Register("/" + resourceTypes[i] + "/{id}/attachments/{name}/size", GetAttachmentSize);
-      Register("/" + resourceTypes[i] + "/{id}/attachments/{name}/uncompress", ChangeAttachmentCompression<CompressionType_None>);
       Register("/" + resourceTypes[i] + "/{id}/attachments/{name}/info", GetAttachmentInfo);
       Register("/" + resourceTypes[i] + "/{id}/attachments/{name}/verify-md5", VerifyAttachment);
+
+      if (!context_.IsReadOnly())
+      {
+        Register("/" + resourceTypes[i] + "/{id}/attachments/{name}", DeleteAttachment);
+        Register("/" + resourceTypes[i] + "/{id}/attachments/{name}", UploadAttachment);
+        Register("/" + resourceTypes[i] + "/{id}/attachments/{name}/compress", ChangeAttachmentCompression<CompressionType_ZlibWithSize>);
+        Register("/" + resourceTypes[i] + "/{id}/attachments/{name}/uncompress", ChangeAttachmentCompression<CompressionType_None>);
+      }
     }
 
-    Register("/tools/invalidate-tags", InvalidateTags);
+    if (context_.IsReadOnly())
+    {
+      LOG(WARNING) << "READ-ONLY SYSTEM: deactivating PUT, POST and DELETE attachments routes";
+      LOG(WARNING) << "READ-ONLY SYSTEM: deactivating PUT and DELETE labels routes";
+    }
+
+    if (!context_.IsReadOnly())
+    {
+      Register("/tools/invalidate-tags", InvalidateTags);
+    }
+
     Register("/tools/lookup", Lookup);
     Register("/tools/find", Find);
 
@@ -4036,13 +4262,19 @@
     Register("/series/{id}/ordered-slices", OrderSlices);
     Register("/series/{id}/numpy", GetNumpySeries);  // New in Orthanc 1.10.0
 
-    Register("/patients/{id}/reconstruct", ReconstructResource<ResourceType_Patient>);
-    Register("/studies/{id}/reconstruct", ReconstructResource<ResourceType_Study>);
-    Register("/series/{id}/reconstruct", ReconstructResource<ResourceType_Series>);
-    Register("/instances/{id}/reconstruct", ReconstructResource<ResourceType_Instance>);
-    Register("/tools/reconstruct", ReconstructAllResources);
+    if (!context_.IsReadOnly())
+    {
+      Register("/patients/{id}/reconstruct", ReconstructResource<ResourceType_Patient>);
+      Register("/studies/{id}/reconstruct", ReconstructResource<ResourceType_Study>);
+      Register("/series/{id}/reconstruct", ReconstructResource<ResourceType_Series>);
+      Register("/instances/{id}/reconstruct", ReconstructResource<ResourceType_Instance>);
+      Register("/tools/reconstruct", ReconstructAllResources);
+    }
+    else
+    {
+      LOG(WARNING) << "READ-ONLY SYSTEM: deactivating /reconstruct routes";
+    }
 
     Register("/tools/bulk-content", BulkContent);
-    Register("/tools/bulk-delete", BulkDelete);
-  }
+   }
 }
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp	Wed Oct 09 11:01:11 2024 +0200
@@ -92,6 +92,10 @@
     static const char* const MAXIMUM_STORAGE_MODE = "MaximumStorageMode";
     static const char* const USER_METADATA = "UserMetadata";
     static const char* const HAS_LABELS = "HasLabels";
+    static const char* const CAPABILITIES = "Capabilities";
+    static const char* const HAS_EXTENDED_CHANGES = "HasExtendedChanges";
+    static const char* const HAS_EXTENDED_FIND = "HasExtendedFind";
+    static const char* const READ_ONLY = "ReadOnly";
 
     if (call.IsDocumentation())
     {
@@ -138,6 +142,10 @@
                         "The configured UserMetadata (new in Orthanc 1.12.0)")
         .SetAnswerField(HAS_LABELS, RestApiCallDocumentation::Type_Boolean,
                         "Whether the database back-end supports labels (new in Orthanc 1.12.0)")
+        .SetAnswerField(CAPABILITIES, RestApiCallDocumentation::Type_JsonObject,
+                        "Whether the back-end supports optional features like 'HasExtendedChanges', 'HasExtendedFind' (new in Orthanc 1.12.5) ")
+        .SetAnswerField(READ_ONLY, RestApiCallDocumentation::Type_Boolean,
+                        "Whether Orthanc is running in read only mode (new in Orthanc 1.12.5)")
         .SetHttpGetSample("https://orthanc.uclouvain.be/demo/system", true);
       return;
     }
@@ -169,6 +177,7 @@
 
     result[STORAGE_AREA_PLUGIN] = Json::nullValue;
     result[DATABASE_BACKEND_PLUGIN] = Json::nullValue;
+    result[READ_ONLY] = context.IsReadOnly();
 
 #if ORTHANC_ENABLE_PLUGINS == 1
     result[PLUGINS_ENABLED] = true;
@@ -196,6 +205,9 @@
     GetUserMetadataConfiguration(result[USER_METADATA]);
 
     result[HAS_LABELS] = OrthancRestApi::GetIndex(call).HasLabelsSupport();
+    result[CAPABILITIES] = Json::objectValue;
+    result[CAPABILITIES][HAS_EXTENDED_CHANGES] = OrthancRestApi::GetIndex(call).HasExtendedChanges();
+    result[CAPABILITIES][HAS_EXTENDED_FIND] = OrthancRestApi::GetIndex(call).HasFindSupport();
     
     call.GetOutput().AnswerJson(result);
   }
--- a/OrthancServer/Sources/OrthancWebDav.cpp	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/Sources/OrthancWebDav.cpp	Wed Oct 09 11:01:11 2024 +0200
@@ -936,7 +936,7 @@
 
       Visitor visitor(resources);
 
-      ResourceFinder finder(ResourceType_Study, false /* no expand */);
+      ResourceFinder finder(ResourceType_Study, ResponseContentFlags_ID);
       finder.SetDatabaseLookup(query);
       finder.Execute(visitor, GetContext());
     }
@@ -1014,7 +1014,7 @@
 
       Visitor visitor;
 
-      ResourceFinder finder(ResourceType_Study, false /* no expand */);
+      ResourceFinder finder(ResourceType_Study, ResponseContentFlags_ID);
       finder.SetDatabaseLookup(query);
       finder.Execute(visitor, context_);
 
@@ -1392,7 +1392,7 @@
         return false;
       }
 
-      ResourceFinder finder(level, false /* don't expand */);
+      ResourceFinder finder(level, ResponseContentFlags_ID);
       finder.SetDatabaseLookup(query);
       finder.SetRetrieveMetadata(true);
 
@@ -1443,7 +1443,7 @@
                              ResourceType level,
                              const DatabaseLookup& query)
   {
-    ResourceFinder finder(level, true /* expand */);
+    ResourceFinder finder(level, ResponseContentFlags_ExpandTrue);
     finder.SetDatabaseLookup(query);
 
     Json::Value expanded;
@@ -1513,7 +1513,7 @@
       
         mime = MimeType_Dicom;
 
-        ResourceFinder finder(ResourceType_Instance, false /* no expand */);
+        ResourceFinder finder(ResourceType_Instance, ResponseContentFlags_ID);
         finder.SetDatabaseLookup(query);
         finder.SetRetrieveMetadata(true);
         finder.SetRetrieveAttachments(true);
@@ -1643,7 +1643,7 @@
 
         DicomDeleteVisitor visitor(context_, level);
 
-        ResourceFinder finder(level, false /* no expand */);
+        ResourceFinder finder(level, ResponseContentFlags_ID);
         finder.SetDatabaseLookup(query);
         finder.Execute(visitor, context_);
         return true;
--- a/OrthancServer/Sources/ResourceFinder.cpp	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/Sources/ResourceFinder.cpp	Wed Oct 09 11:01:11 2024 +0200
@@ -56,7 +56,6 @@
     if (request_.GetLevel() == parentLevel)
     {
       requestedComputedTags_.insert(tag);
-      hasRequestedTags_ = true;
       request_.GetChildrenSpecification(childLevel).SetRetrieveIdentifiers(true);
     }
   }
@@ -187,19 +186,38 @@
     }
   }
 
+  static void GetMainDicomSequencesFromMetadata(DicomMap& target, const FindResponse::Resource& resource, ResourceType level)
+  {
+    // read all main sequences from DB
+    std::string serializedSequences;
+    if (resource.LookupMetadata(serializedSequences, level, MetadataType_MainDicomSequences))
+    {
+      Json::Value jsonMetadata;
+      Toolbox::ReadJson(jsonMetadata, serializedSequences);
+
+      if (jsonMetadata["Version"].asInt() == 1)
+      {
+        target.FromDicomAsJson(jsonMetadata["Sequences"], true /* append */, true /* parseSequences */);
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_NotImplemented);
+      }
+    }
+  }
+
 
   void ResourceFinder::Expand(Json::Value& target,
                               const FindResponse::Resource& resource,
                               ServerIndex& index,
-                              DicomToJsonFormat format,
-                              bool includeAllMetadata) const
+                              DicomToJsonFormat format) const
   {
     /**
      * This method closely follows "SerializeExpandedResource()" in
      * "ServerContext.cpp" from Orthanc 1.12.4.
      **/
 
-    if (!expand_)
+    if (responseContent_ == ResponseContentFlags_ID)
     {
       throw OrthancException(ErrorCode_BadSequenceOfCalls);
     }
@@ -214,28 +232,31 @@
     target["Type"] = GetResourceTypeText(resource.GetLevel(), false, true);
     target["ID"] = resource.GetIdentifier();
 
-    switch (resource.GetLevel())
+    if (responseContent_ & ResponseContentFlags_Parent)
     {
-      case ResourceType_Patient:
-        break;
+      switch (resource.GetLevel())
+      {
+        case ResourceType_Patient:
+          break;
 
-      case ResourceType_Study:
-        target["ParentPatient"] = resource.GetParentIdentifier();
-        break;
+        case ResourceType_Study:
+          target["ParentPatient"] = resource.GetParentIdentifier();
+          break;
 
-      case ResourceType_Series:
-        target["ParentStudy"] = resource.GetParentIdentifier();
-        break;
+        case ResourceType_Series:
+          target["ParentStudy"] = resource.GetParentIdentifier();
+          break;
 
-      case ResourceType_Instance:
-        target["ParentSeries"] = resource.GetParentIdentifier();
-        break;
+        case ResourceType_Instance:
+          target["ParentSeries"] = resource.GetParentIdentifier();
+          break;
 
-      default:
-        throw OrthancException(ErrorCode_InternalError);
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
     }
 
-    if (resource.GetLevel() != ResourceType_Instance)
+    if ((responseContent_ & ResponseContentFlags_Children) && (resource.GetLevel() != ResourceType_Instance))
     {
       const std::set<std::string>& children = resource.GetChildrenIdentifiers(GetChildResourceType(resource.GetLevel()));
 
@@ -273,52 +294,65 @@
 
       case ResourceType_Series:
       {
-        uint32_t expectedNumberOfInstances;
-        SeriesStatus status = GetSeriesStatus(expectedNumberOfInstances, resource);
-
-        target["Status"] = EnumerationToString(status);
-
-        static const char* const EXPECTED_NUMBER_OF_INSTANCES = "ExpectedNumberOfInstances";
+        if ((responseContent_ & ResponseContentFlags_Status) || (responseContent_ & ResponseContentFlags_MetadataLegacy) )
+        {
+          uint32_t expectedNumberOfInstances;
+          SeriesStatus status = GetSeriesStatus(expectedNumberOfInstances, resource);
+          
+          if (responseContent_ & ResponseContentFlags_Status )
+          {
+            target["Status"] = EnumerationToString(status);
+          }
 
-        if (status == SeriesStatus_Unknown)
-        {
-          target[EXPECTED_NUMBER_OF_INSTANCES] = Json::nullValue;
+          if (responseContent_ & ResponseContentFlags_MetadataLegacy)
+          {
+            static const char* const EXPECTED_NUMBER_OF_INSTANCES = "ExpectedNumberOfInstances";
+
+            if (status == SeriesStatus_Unknown)
+            {
+              target[EXPECTED_NUMBER_OF_INSTANCES] = Json::nullValue;
+            }
+            else
+            {
+              target[EXPECTED_NUMBER_OF_INSTANCES] = expectedNumberOfInstances;
+            }
+          }
         }
-        else
-        {
-          target[EXPECTED_NUMBER_OF_INSTANCES] = expectedNumberOfInstances;
-        }
-
         break;
       }
 
       case ResourceType_Instance:
       {
-        FileInfo info;
-        if (resource.LookupAttachment(info, FileContentType_Dicom))
+        if (responseContent_ & ResponseContentFlags_AttachmentsLegacy)
         {
-          target["FileSize"] = static_cast<Json::UInt64>(info.GetUncompressedSize());
-          target["FileUuid"] = info.GetUuid();
-        }
-        else
-        {
-          throw OrthancException(ErrorCode_InternalError);
+          FileInfo info;
+          if (resource.LookupAttachment(info, FileContentType_Dicom))
+          {
+            target["FileSize"] = static_cast<Json::UInt64>(info.GetUncompressedSize());
+            target["FileUuid"] = info.GetUuid();
+          }
+          else
+          {
+            throw OrthancException(ErrorCode_InternalError);
+          }
         }
 
-        static const char* const INDEX_IN_SERIES = "IndexInSeries";
-
-        std::string s;
-        uint32_t indexInSeries;
-        if (resource.LookupMetadata(s, ResourceType_Instance, MetadataType_Instance_IndexInSeries) &&
-            SerializationToolbox::ParseUnsignedInteger32(indexInSeries, s))
+        if (responseContent_ & ResponseContentFlags_MetadataLegacy)
         {
-          target[INDEX_IN_SERIES] = indexInSeries;
+          static const char* const INDEX_IN_SERIES = "IndexInSeries";
+
+          std::string s;
+          uint32_t indexInSeries;
+          if (resource.LookupMetadata(s, ResourceType_Instance, MetadataType_Instance_IndexInSeries) &&
+              SerializationToolbox::ParseUnsignedInteger32(indexInSeries, s))
+          {
+            target[INDEX_IN_SERIES] = indexInSeries;
+          }
+          else
+          {
+            target[INDEX_IN_SERIES] = Json::nullValue;
+          }
         }
-        else
-        {
-          target[INDEX_IN_SERIES] = Json::nullValue;
-        }
-
         break;
       }
 
@@ -327,58 +361,46 @@
     }
 
     std::string s;
-    if (resource.LookupMetadata(s, resource.GetLevel(), MetadataType_AnonymizedFrom))
+    if (responseContent_ & ResponseContentFlags_MetadataLegacy)
     {
-      target["AnonymizedFrom"] = s;
-    }
-
-    if (resource.LookupMetadata(s, resource.GetLevel(), MetadataType_ModifiedFrom))
-    {
-      target["ModifiedFrom"] = s;
-    }
+      if (resource.LookupMetadata(s, resource.GetLevel(), MetadataType_AnonymizedFrom))
+      {
+        target["AnonymizedFrom"] = s;
+      }
 
-    if (resource.GetLevel() == ResourceType_Patient ||
-        resource.GetLevel() == ResourceType_Study ||
-        resource.GetLevel() == ResourceType_Series)
-    {
-      target["IsStable"] = !index.IsUnstableResource(resource.GetLevel(), resource.GetInternalId());
+      if (resource.LookupMetadata(s, resource.GetLevel(), MetadataType_ModifiedFrom))
+      {
+        target["ModifiedFrom"] = s;
+      }
 
-      if (resource.LookupMetadata(s, resource.GetLevel(), MetadataType_LastUpdate))
+      if (resource.GetLevel() == ResourceType_Patient ||
+          resource.GetLevel() == ResourceType_Study ||
+          resource.GetLevel() == ResourceType_Series)
       {
-        target["LastUpdate"] = s;
+        if (resource.LookupMetadata(s, resource.GetLevel(), MetadataType_LastUpdate))
+        {
+          target["LastUpdate"] = s;
+        }
       }
     }
 
+    if (responseContent_ & ResponseContentFlags_IsStable)
+    {
+      if (resource.GetLevel() == ResourceType_Patient ||
+          resource.GetLevel() == ResourceType_Study ||
+          resource.GetLevel() == ResourceType_Series)
+      {
+        target["IsStable"] = !index.IsUnstableResource(resource.GetLevel(), resource.GetInternalId());
+      }
+    }
+
+    if (responseContent_ & ResponseContentFlags_MainDicomTags)
     {
       DicomMap allMainDicomTags;
       resource.GetMainDicomTags(allMainDicomTags, resource.GetLevel());
 
-      /**
-       * This section was part of "StatelessDatabaseOperations::ExpandResource()"
-       * in Orthanc <= 1.12.3
-       **/
-
       // read all main sequences from DB
-      std::string serializedSequences;
-      if (resource.LookupMetadata(serializedSequences, resource.GetLevel(), MetadataType_MainDicomSequences))
-      {
-        Json::Value jsonMetadata;
-        Toolbox::ReadJson(jsonMetadata, serializedSequences);
-
-        if (jsonMetadata["Version"].asInt() == 1)
-        {
-          allMainDicomTags.FromDicomAsJson(jsonMetadata["Sequences"], true /* append */, true /* parseSequences */);
-        }
-        else
-        {
-          throw OrthancException(ErrorCode_NotImplemented);
-        }
-      }
-
-      /**
-       * End of section from StatelessDatabaseOperations
-       **/
-
+      GetMainDicomSequencesFromMetadata(allMainDicomTags, resource, resource.GetLevel());
 
       static const char* const MAIN_DICOM_TAGS = "MainDicomTags";
       static const char* const PATIENT_MAIN_DICOM_TAGS = "PatientMainDicomTags";
@@ -401,6 +423,7 @@
       }
     }
 
+    if (responseContent_ & ResponseContentFlags_Labels)
     {
       Json::Value labels = Json::arrayValue;
 
@@ -413,7 +436,7 @@
       target["Labels"] = labels;
     }
 
-    if (includeAllMetadata)  // new in Orthanc 1.12.4
+    if (responseContent_ & ResponseContentFlags_Metadata)  // new in Orthanc 1.12.4
     {
       const std::map<MetadataType, std::string>& m = resource.GetMetadata(resource.GetLevel());
 
@@ -426,6 +449,26 @@
 
       target["Metadata"] = metadata;
     }
+
+    if (responseContent_ & ResponseContentFlags_Attachments)  // new in Orthanc 1.12.5
+    {
+      const std::map<FileContentType, FileInfo>& attachments = resource.GetAttachments();
+
+      target["Attachments"] = Json::arrayValue;
+
+      for (std::map<FileContentType, FileInfo>::const_iterator it = attachments.begin(); it != attachments.end(); ++it)
+      {
+        Json::Value attachment = Json::objectValue;    
+        attachment["Uuid"] = it->second.GetUuid();
+        attachment["ContentType"] = it->second.GetContentType();
+        attachment["UncompressedSize"] = Json::Value::UInt64(it->second.GetUncompressedSize());
+        attachment["CompressedSize"] = Json::Value::UInt64(it->second.GetCompressedSize());
+        attachment["UncompressedMD5"] = it->second.GetUncompressedMD5();
+        attachment["CompressedMD5"] = it->second.GetCompressedMD5();
+
+        target["Attachments"].append(attachment);
+      }
+    }
   }
 
 
@@ -478,7 +521,7 @@
 
 
   ResourceFinder::ResourceFinder(ResourceType level,
-                                 bool expand) :
+                                 ResponseContentFlags responseContent) :
     request_(level),
     databaseLimits_(0),
     isSimpleLookup_(true),
@@ -487,43 +530,56 @@
     hasLimitsCount_(false),
     limitsSince_(0),
     limitsCount_(0),
-    expand_(expand),
+    responseContent_(responseContent),
     allowStorageAccess_(true),
-    hasRequestedTags_(false)
+    isWarning002Enabled_(false),
+    isWarning004Enabled_(false),
+    isWarning005Enabled_(false)
   {
+    {
+      OrthancConfiguration::ReaderLock lock;
+      isWarning002Enabled_ = lock.GetConfiguration().IsWarningEnabled(Warnings_002_InconsistentDicomTagsInDb);
+      isWarning004Enabled_ = lock.GetConfiguration().IsWarningEnabled(Warnings_004_NoMainDicomTagsSignature);
+      isWarning005Enabled_ = lock.GetConfiguration().IsWarningEnabled(Warnings_005_RequestingTagFromLowerResourceLevel);
+    }
+
     UpdateRequestLimits();
 
-    if (expand)
-    {
-      request_.SetRetrieveMainDicomTags(true);
-      request_.SetRetrieveMetadata(true);
-      request_.SetRetrieveLabels(true);
+    request_.SetRetrieveMainDicomTags(responseContent_ & ResponseContentFlags_MainDicomTags);
+    request_.SetRetrieveMetadata((responseContent_ & ResponseContentFlags_Metadata) || (responseContent_ & ResponseContentFlags_MetadataLegacy));
+    request_.SetRetrieveLabels(responseContent_ & ResponseContentFlags_Labels);
 
-      switch (level)
-      {
-        case ResourceType_Patient:
-          request_.GetChildrenSpecification(ResourceType_Study).SetRetrieveIdentifiers(true);
-          break;
+    switch (level)
+    {
+      case ResourceType_Patient:
+        request_.GetChildrenSpecification(ResourceType_Study).SetRetrieveIdentifiers(responseContent_ & ResponseContentFlags_Children);
+        request_.SetRetrieveAttachments(responseContent_ & ResponseContentFlags_Attachments); 
+        break;
+
+      case ResourceType_Study:
+        request_.GetChildrenSpecification(ResourceType_Series).SetRetrieveIdentifiers(responseContent_ & ResponseContentFlags_Children);
+        request_.SetRetrieveParentIdentifier(responseContent_ & ResponseContentFlags_Parent);
+        request_.SetRetrieveAttachments(responseContent_ & ResponseContentFlags_Attachments); 
+        break;
 
-        case ResourceType_Study:
-          request_.GetChildrenSpecification(ResourceType_Series).SetRetrieveIdentifiers(true);
-          request_.SetRetrieveParentIdentifier(true);
-          break;
-
-        case ResourceType_Series:
+      case ResourceType_Series:
+        if (responseContent_ & ResponseContentFlags_Status)
+        {
           request_.GetChildrenSpecification(ResourceType_Instance).AddMetadata(MetadataType_Instance_IndexInSeries); // required for the SeriesStatus
-          request_.GetChildrenSpecification(ResourceType_Instance).SetRetrieveIdentifiers(true);
-          request_.SetRetrieveParentIdentifier(true);
-          break;
+        }
+        request_.GetChildrenSpecification(ResourceType_Instance).SetRetrieveIdentifiers(responseContent_ & ResponseContentFlags_Children);
+        request_.SetRetrieveParentIdentifier(responseContent_ & ResponseContentFlags_Parent);
+        request_.SetRetrieveAttachments(responseContent_ & ResponseContentFlags_Attachments); 
+        break;
 
-        case ResourceType_Instance:
-          request_.SetRetrieveAttachments(true); // for FileSize & FileUuid
-          request_.SetRetrieveParentIdentifier(true);
-          break;
+      case ResourceType_Instance:
+        request_.SetRetrieveAttachments((responseContent_ & ResponseContentFlags_AttachmentsLegacy) // for FileSize & FileUuid
+                                        || (responseContent_ & ResponseContentFlags_Attachments)); 
+        request_.SetRetrieveParentIdentifier(true);
+        break;
 
-        default:
-          throw OrthancException(ErrorCode_ParameterOutOfRange);
-      }
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
   }
 
@@ -596,7 +652,7 @@
 
     for (size_t i = 0; i < request_.GetDicomTagConstraints().GetSize(); i++)
     {
-      const DatabaseConstraint& constraint = request_.GetDicomTagConstraints().GetConstraint(i);
+      const DatabaseDicomTagConstraint& constraint = request_.GetDicomTagConstraints().GetConstraint(i);
       if (constraint.GetLevel() == request_.GetLevel())
       {
         request_.SetRetrieveMainDicomTags(true);
@@ -625,12 +681,13 @@
 
   void ResourceFinder::AddRequestedTag(const DicomTag& tag)
   {
+    requestedTags_.insert(tag);
+
     if (DicomMap::IsMainDicomTag(tag, ResourceType_Patient))
     {
       if (request_.GetLevel() == ResourceType_Patient)
       {
         request_.SetRetrieveMainDicomTags(true);
-        requestedPatientTags_.insert(tag);
       }
       else
       {
@@ -638,8 +695,6 @@
          * This comes from the fact that patient-level tags are copied
          * at the study level, as implemented by "ResourcesContent::AddResource()".
          **/
-        requestedStudyTags_.insert(tag);
-
         if (request_.GetLevel() == ResourceType_Study)
         {
           request_.SetRetrieveMainDicomTags(true);
@@ -647,21 +702,20 @@
         else
         {
           request_.GetParentSpecification(ResourceType_Study).SetRetrieveMainDicomTags(true);
+          request_.GetParentSpecification(ResourceType_Study).SetRetrieveMetadata(true);  // to get the MainDicomSequences
         }
-
-        requestedStudyTags_.insert(tag);
       }
-
-      hasRequestedTags_ = true;
     }
     else if (DicomMap::IsMainDicomTag(tag, ResourceType_Study))
     {
       if (request_.GetLevel() == ResourceType_Patient)
       {
-        LOG(WARNING) << "Requested tag " << tag.Format()
-                     << " should only be read at the study, series, or instance level";
-        requestedTagsFromFileStorage_.insert(tag);
-        request_.SetRetrieveOneInstanceIdentifier(true);
+        if (isWarning005Enabled_)
+        {
+          LOG(WARNING) << "W005: Requested tag " << tag.Format()
+                       << " should only be read at the study, series, or instance level";
+        }
+        request_.SetRetrieveOneInstanceMetadataAndAttachments(true); // we might need to get it from one instance
       }
       else
       {
@@ -672,22 +726,21 @@
         else
         {
           request_.GetParentSpecification(ResourceType_Study).SetRetrieveMainDicomTags(true);
+          request_.GetParentSpecification(ResourceType_Study).SetRetrieveMetadata(true);  // to get the MainDicomSequences
         }
-
-        requestedStudyTags_.insert(tag);
       }
-
-      hasRequestedTags_ = true;
     }
     else if (DicomMap::IsMainDicomTag(tag, ResourceType_Series))
     {
       if (request_.GetLevel() == ResourceType_Patient ||
           request_.GetLevel() == ResourceType_Study)
       {
-        LOG(WARNING) << "Requested tag " << tag.Format()
-                     << " should only be read at the series or instance level";
-        requestedTagsFromFileStorage_.insert(tag);
-        request_.SetRetrieveOneInstanceIdentifier(true);
+        if (isWarning005Enabled_)
+        {
+          LOG(WARNING) << "W005: Requested tag " << tag.Format()
+                      << " should only be read at the series or instance level";
+        }
+        request_.SetRetrieveOneInstanceMetadataAndAttachments(true); // we might need to get it from one instance
       }
       else
       {
@@ -698,12 +751,9 @@
         else
         {
           request_.GetParentSpecification(ResourceType_Series).SetRetrieveMainDicomTags(true);
+          request_.GetParentSpecification(ResourceType_Series).SetRetrieveMetadata(true);  // to get the MainDicomSequences
         }
-
-        requestedSeriesTags_.insert(tag);
       }
-
-      hasRequestedTags_ = true;
     }
     else if (DicomMap::IsMainDicomTag(tag, ResourceType_Instance))
     {
@@ -711,18 +761,17 @@
           request_.GetLevel() == ResourceType_Study ||
           request_.GetLevel() == ResourceType_Series)
       {
-        LOG(WARNING) << "Requested tag " << tag.Format()
-                     << " should only be read at the instance level";
-        requestedTagsFromFileStorage_.insert(tag);
-        request_.SetRetrieveOneInstanceIdentifier(true);
+        if (isWarning005Enabled_)
+        {
+          LOG(WARNING) << "W005: Requested tag " << tag.Format()
+                       << " should only be read at the instance level";
+        }
+        request_.SetRetrieveOneInstanceMetadataAndAttachments(true); // we might need to get it from one instance
       }
       else
       {
         request_.SetRetrieveMainDicomTags(true);
-        requestedInstanceTags_.insert(tag);
       }
-
-      hasRequestedTags_ = true;
     }
     else if (tag == DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES)
     {
@@ -751,13 +800,11 @@
     else if (tag == DICOM_TAG_SOP_CLASSES_IN_STUDY)
     {
       requestedComputedTags_.insert(tag);
-      hasRequestedTags_ = true;
       request_.GetChildrenSpecification(ResourceType_Instance).AddMetadata(MetadataType_Instance_SopClassUid);
     }
     else if (tag == DICOM_TAG_MODALITIES_IN_STUDY)
     {
       requestedComputedTags_.insert(tag);
-      hasRequestedTags_ = true;
       if (request_.GetLevel() < ResourceType_Series)
       {
         request_.GetChildrenSpecification(ResourceType_Series).AddMainDicomTag(DICOM_TAG_MODALITY);
@@ -770,20 +817,18 @@
     else if (tag == DICOM_TAG_INSTANCE_AVAILABILITY)
     {
       requestedComputedTags_.insert(tag);
-      hasRequestedTags_ = true;
     }
     else
     {
       // This is neither a main DICOM tag, nor a computed DICOM tag:
-      // We will be forced to access the DICOM file anyway
-      requestedTagsFromFileStorage_.insert(tag);
+      // We might need to access a DICOM file or the MainDicomSequences metadata
+      
+      request_.SetRetrieveMetadata(true);
 
       if (request_.GetLevel() != ResourceType_Instance)
       {
-        request_.SetRetrieveOneInstanceIdentifier(true);
+        request_.SetRetrieveOneInstanceMetadataAndAttachments(true);
       }
-
-      hasRequestedTags_ = true;
     }
   }
 
@@ -797,30 +842,51 @@
   }
 
 
-  static void InjectRequestedTags(DicomMap& requestedTags,
-                                  std::set<DicomTag>& missingTags /* out */,
+  static void InjectRequestedTags(DicomMap& target,
+                                  std::set<DicomTag>& remainingRequestedTags /* in & out */,
                                   const FindResponse::Resource& resource,
-                                  ResourceType level,
-                                  const std::set<DicomTag>& tags)
+                                  ResourceType level/*,
+                                  const std::set<DicomTag>& tags*/)
   {
-    if (!tags.empty())
+    if (!remainingRequestedTags.empty() && level <= resource.GetLevel())
     {
+      std::set<DicomTag> savedMainDicomTags;
+
       DicomMap m;
-      resource.GetMainDicomTags(m, level);
+      resource.GetMainDicomTags(m, level);                          // read DicomTags from DB
 
-      for (std::set<DicomTag>::const_iterator it = tags.begin(); it != tags.end(); ++it)
+      if (resource.GetMetadata(level).size() > 0)
       {
-        std::string value;
-        if (m.LookupStringValue(value, *it, false /* not binary */))
+        GetMainDicomSequencesFromMetadata(m, resource, level);        // read DicomSequences from metadata
+      
+        // check which tags have been saved in DB; that's the way to know if they are missing because they were not saved or because they have no value
+        
+        std::string signature = DicomMap::GetDefaultMainDicomTagsSignatureFrom1_11(level); // default signature in case it's not in the metadata (= the signature for 1.11.0)
+        if (resource.LookupMetadata(signature, level, MetadataType_MainDicomTagsSignature))
         {
-          requestedTags.SetValue(*it, value, false /* not binary */);
-        }
-        else
-        {
-          // This is the case where the Housekeeper should be run
-          missingTags.insert(*it);
+          if (level == ResourceType_Study) // when we retrieve the study tags, we actually also get the patient tags that are also saved at study level but not included in the signature
+          {
+            signature += ";" + DicomMap::GetDefaultMainDicomTagsSignatureFrom1_11(ResourceType_Patient); // append the default signature (from before 1.11.0)
+          }
+
+          FromDcmtkBridge::ParseListOfTags(savedMainDicomTags, signature);
         }
       }
+
+      std::set<DicomTag> copiedTags;
+      for (std::set<DicomTag>::const_iterator it = remainingRequestedTags.begin(); it != remainingRequestedTags.end(); ++it)
+      {
+        if (target.CopyTagIfExists(m, *it))
+        {
+          copiedTags.insert(*it);
+        }
+        else if (savedMainDicomTags.find(*it) != savedMainDicomTags.end())  // the tag should have been saved in DB but has no value so we consider it has been copied
+        {
+          copiedTags.insert(*it);
+        }
+      }
+
+      Toolbox::RemoveSets(remainingRequestedTags, copiedTags);
     }
   }
 
@@ -839,47 +905,75 @@
 
       LOG(WARNING) << "W001: Accessing DICOM tags from storage when accessing "
                    << Orthanc::GetResourceTypeText(resource.GetLevel(), false, false)
+                   << " " << resource.GetIdentifier()
                    << ": " << missings;
     }
 
-    std::string instancePublicId;
+    // TODO-FIND: What do we do if the DICOM has been removed since the request?
+    // Do we fail, or do we skip the resource?
 
-    if (request.IsRetrieveOneInstanceIdentifier())
+    Json::Value tmpDicomAsJson;
+
+    if (request.GetLevel() == ResourceType_Instance &&
+        request.IsRetrieveMetadata() &&
+        request.IsRetrieveAttachments())
     {
-      instancePublicId = resource.GetOneInstanceIdentifier();
+      LOG(INFO) << "Will retrieve missing DICOM tags from instance: " << resource.GetIdentifier();
+
+      context.ReadDicomAsJson(tmpDicomAsJson, resource.GetIdentifier(), resource.GetMetadata(ResourceType_Instance),
+                              resource.GetAttachments(), missingTags /* ignoreTagLength */);
     }
-    else if (request.GetLevel() == ResourceType_Instance)
+    else if (request.GetLevel() != ResourceType_Instance &&
+             request.IsRetrieveOneInstanceMetadataAndAttachments())
     {
-      instancePublicId = resource.GetIdentifier();
+      LOG(INFO) << "Will retrieve missing DICOM tags from instance: " << resource.GetOneInstancePublicId();
+
+      context.ReadDicomAsJson(tmpDicomAsJson, resource.GetOneInstancePublicId(), resource.GetOneInstanceMetadata(),
+                              resource.GetOneInstanceAttachments(), missingTags /* ignoreTagLength */);
     }
     else
     {
+      // TODO-FIND: This fallback shouldn't be necessary
+
       FindRequest requestDicomAttachment(request.GetLevel());
       requestDicomAttachment.SetOrthancId(request.GetLevel(), resource.GetIdentifier());
-      requestDicomAttachment.SetRetrieveOneInstanceIdentifier(true);
+
+      if (request.GetLevel() == ResourceType_Instance)
+      {
+        requestDicomAttachment.SetRetrieveMetadata(true);
+        requestDicomAttachment.SetRetrieveAttachments(true);
+      }
+      else
+      {
+        requestDicomAttachment.SetRetrieveOneInstanceMetadataAndAttachments(true);
+      }
 
       FindResponse responseDicomAttachment;
       context.GetIndex().ExecuteFind(responseDicomAttachment, requestDicomAttachment);
 
-      if (responseDicomAttachment.GetSize() != 1 ||
-          !responseDicomAttachment.GetResourceByIndex(0).HasOneInstanceIdentifier())
+      if (responseDicomAttachment.GetSize() != 1)
       {
         throw OrthancException(ErrorCode_InexistentFile);
       }
       else
       {
-        instancePublicId = responseDicomAttachment.GetResourceByIndex(0).GetOneInstanceIdentifier();
+        const FindResponse::Resource& response = responseDicomAttachment.GetResourceByIndex(0);
+        const std::string instancePublicId = response.GetIdentifier();
+        LOG(INFO) << "Will retrieve missing DICOM tags from instance: " << instancePublicId;
+
+        if (request.GetLevel() == ResourceType_Instance)
+        {
+          context.ReadDicomAsJson(tmpDicomAsJson, response.GetIdentifier(), response.GetMetadata(ResourceType_Instance),
+                                  response.GetAttachments(), missingTags /* ignoreTagLength */);
+        }
+        else
+        {
+          context.ReadDicomAsJson(tmpDicomAsJson, response.GetOneInstancePublicId(), response.GetOneInstanceMetadata(),
+                                  response.GetOneInstanceAttachments(), missingTags /* ignoreTagLength */);
+        }
       }
     }
 
-    LOG(INFO) << "Will retrieve missing DICOM tags from instance: " << instancePublicId;
-
-    // TODO-FIND: What do we do if the DICOM has been removed since the request?
-    // Do we fail, or do we skip the resource?
-
-    Json::Value tmpDicomAsJson;
-    context.ReadDicomAsJson(tmpDicomAsJson, instancePublicId, missingTags /* ignoreTagLength */);
-
     DicomMap tmpDicomMap;
     tmpDicomMap.FromDicomAsJson(tmpDicomAsJson, false /* append */, true /* parseSequences*/);
 
@@ -901,6 +995,15 @@
   void ResourceFinder::Execute(IVisitor& visitor,
                                ServerContext& context) const
   {
+    bool isWarning002Enabled = false;
+    bool isWarning004Enabled = false;
+
+    {
+      OrthancConfiguration::ReaderLock lock;
+      isWarning002Enabled = lock.GetConfiguration().IsWarningEnabled(Warnings_002_InconsistentDicomTagsInDb);
+      isWarning004Enabled = lock.GetConfiguration().IsWarningEnabled(Warnings_004_NoMainDicomTagsSignature);
+    }
+
     FindResponse response;
     context.GetIndex().ExecuteFind(response, request_);
 
@@ -934,19 +1037,30 @@
     {
       const FindResponse::Resource& resource = response.GetResourceByIndex(i);
 
-      DicomMap requestedTags;
+#if 0
+      {
+        Json::Value v;
+        resource.DebugExport(v, request_);
+        std::cout << v.toStyledString();
+      }
+#endif
 
-      if (hasRequestedTags_)
-      {
-        InjectComputedTags(requestedTags, resource);
+      DicomMap outRequestedTags;
 
-        std::set<DicomTag> missingTags = requestedTagsFromFileStorage_;
-        InjectRequestedTags(requestedTags, missingTags, resource, ResourceType_Patient, requestedPatientTags_);
-        InjectRequestedTags(requestedTags, missingTags, resource, ResourceType_Study, requestedStudyTags_);
-        InjectRequestedTags(requestedTags, missingTags, resource, ResourceType_Series, requestedSeriesTags_);
-        InjectRequestedTags(requestedTags, missingTags, resource, ResourceType_Instance, requestedInstanceTags_);
+      if (HasRequestedTags())
+      {
+        std::set<DicomTag> remainingRequestedTags = requestedTags_; // at this point, all requested tags are "missing"
+
+        InjectComputedTags(outRequestedTags, resource);
+        Toolbox::RemoveSets(remainingRequestedTags, requestedComputedTags_);
 
-        if (!missingTags.empty())
+        InjectRequestedTags(outRequestedTags, remainingRequestedTags, resource, ResourceType_Patient);
+        InjectRequestedTags(outRequestedTags, remainingRequestedTags, resource, ResourceType_Study);
+        InjectRequestedTags(outRequestedTags, remainingRequestedTags, resource, ResourceType_Series);
+        InjectRequestedTags(outRequestedTags, remainingRequestedTags, resource, ResourceType_Instance);
+
+        if (!remainingRequestedTags.empty() && 
+            !DicomMap::HasOnlyComputedTags(remainingRequestedTags)) // if the only remaining tags are computed tags, it is worthless to read them from disk
         {
           if (!allowStorageAccess_)
           {
@@ -955,9 +1069,32 @@
           }
           else
           {
-            ReadMissingTagsFromStorageArea(requestedTags, context, request_, resource, missingTags);
+            ReadMissingTagsFromStorageArea(outRequestedTags, context, request_, resource, remainingRequestedTags);
           }
         }
+
+        std::string mainDicomTagsSignature;
+        if (isWarning002Enabled &&
+            resource.LookupMetadata(mainDicomTagsSignature, resource.GetLevel(), MetadataType_MainDicomTagsSignature) &&
+            mainDicomTagsSignature != DicomMap::GetMainDicomTagsSignature(resource.GetLevel()))
+        {
+          LOG(WARNING) << "W002: " << Orthanc::GetResourceTypeText(resource.GetLevel(), false , false)
+                       << " has been stored with another version of Main Dicom Tags list, you should POST to /"
+                       << Orthanc::GetResourceTypeText(resource.GetLevel(), true, false)
+                       << "/" << resource.GetIdentifier()
+                       << "/reconstruct to update the list of tags saved in DB or run the Housekeeper plugin.  Some MainDicomTags might be missing from this answer.";
+        }
+        else if (isWarning004Enabled && 
+                 request_.IsRetrieveMetadata() &&
+                 !resource.LookupMetadata(mainDicomTagsSignature, resource.GetLevel(), MetadataType_MainDicomTagsSignature))
+        {
+          LOG(WARNING) << "W004: " << Orthanc::GetResourceTypeText(resource.GetLevel(), false , false)
+                       << " has been stored with an old Orthanc version and does not have a MainDicomTagsSignature, you should POST to /"
+                       << Orthanc::GetResourceTypeText(resource.GetLevel(), true, false)
+                       << "/" << resource.GetIdentifier()
+                       << "/reconstruct to update the list of tags saved in DB or run the Housekeeper plugin.  Some MainDicomTags might be missing from this answer.";
+        }
+
       }
 
       bool match = true;
@@ -966,7 +1103,7 @@
       {
         DicomMap tags;
         resource.GetAllMainDicomTags(tags);
-        tags.Merge(requestedTags);
+        tags.Merge(outRequestedTags);
         match = lookup_->IsMatch(tags);
       }
 
@@ -974,7 +1111,7 @@
       {
         if (pagingMode_ == PagingMode_FullDatabase)
         {
-          visitor.Apply(resource, requestedTags);
+          visitor.Apply(resource, outRequestedTags);
         }
         else
         {
@@ -992,7 +1129,7 @@
           }
           else
           {
-            visitor.Apply(resource, requestedTags);
+            visitor.Apply(resource, outRequestedTags);
             countResults++;
           }
         }
@@ -1040,10 +1177,10 @@
       virtual void Apply(const FindResponse::Resource& resource,
                          const DicomMap& requestedTags) ORTHANC_OVERRIDE
       {
-        if (that_.expand_)
+        if (that_.responseContent_ != ResponseContentFlags_ID)
         {
           Json::Value item;
-          that_.Expand(item, resource, index_, format_, includeAllMetadata_);
+          that_.Expand(item, resource, index_, format_);
 
           if (hasRequestedTags_)
           {
@@ -1067,7 +1204,7 @@
 
     target = Json::arrayValue;
 
-    Visitor visitor(*this, context.GetIndex(), target, format, hasRequestedTags_, includeAllMetadata);
+    Visitor visitor(*this, context.GetIndex(), target, format, HasRequestedTags(), includeAllMetadata);
     Execute(visitor, context);
   }
 
--- a/OrthancServer/Sources/ResourceFinder.h	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/Sources/ResourceFinder.h	Wed Oct 09 11:01:11 2024 +0200
@@ -65,16 +65,15 @@
     bool                             hasLimitsCount_;
     uint64_t                         limitsSince_;
     uint64_t                         limitsCount_;
-    bool                             expand_;
+    ResponseContentFlags             responseContent_;
     bool                             allowStorageAccess_;
-    bool                             hasRequestedTags_;
-    std::set<DicomTag>               requestedPatientTags_;
-    std::set<DicomTag>               requestedStudyTags_;
-    std::set<DicomTag>               requestedSeriesTags_;
-    std::set<DicomTag>               requestedInstanceTags_;
-    std::set<DicomTag>               requestedTagsFromFileStorage_;
+    std::set<DicomTag>               requestedTags_;
     std::set<DicomTag>               requestedComputedTags_;
 
+    bool                             isWarning002Enabled_;
+    bool                             isWarning004Enabled_;
+    bool                             isWarning005Enabled_;
+
     bool IsRequestedComputedTag(const DicomTag& tag) const
     {
       return requestedComputedTags_.find(tag) != requestedComputedTags_.end();
@@ -97,9 +96,14 @@
 
     void UpdateRequestLimits();
 
+    bool HasRequestedTags() const
+    {
+      return requestedTags_.size() > 0;
+    }
+
   public:
     ResourceFinder(ResourceType level,
-                   bool expand);
+                   ResponseContentFlags responseContent);
 
     void SetDatabaseLimits(uint64_t limits);
 
@@ -129,6 +133,23 @@
 
     void AddRequestedTags(const std::set<DicomTag>& tags);
 
+    void AddOrdering(const DicomTag& tag,
+                     FindRequest::OrderingDirection direction)
+    {
+      request_.AddOrdering(tag, direction);
+    }
+
+    void AddOrdering(MetadataType metadataType,
+                     FindRequest::OrderingDirection direction)
+    {
+      request_.AddOrdering(metadataType, direction);
+    }
+
+    void AddMetadataConstraint(DatabaseMetadataConstraint* constraint)
+    {
+      request_.AddMetadataConstraint(constraint);
+    }
+
     void SetLabels(const std::set<std::string>& labels)
     {
       request_.SetLabels(labels);
@@ -144,9 +165,9 @@
       request_.SetLabelsConstraint(constraint);
     }
 
-    void SetRetrieveOneInstanceIdentifier(bool retrieve)
+    void SetRetrieveOneInstanceMetadataAndAttachments(bool retrieve)
     {
-      request_.SetRetrieveOneInstanceIdentifier(retrieve);
+      request_.SetRetrieveOneInstanceMetadataAndAttachments(retrieve);
     }
 
     void SetRetrieveMetadata(bool retrieve)
@@ -163,8 +184,7 @@
     void Expand(Json::Value& target,
                 const FindResponse::Resource& resource,
                 ServerIndex& index,
-                DicomToJsonFormat format,
-                bool includeAllMetadata /* Same as: ExpandResourceFlags_IncludeAllMetadata */) const;
+                DicomToJsonFormat format) const;
 
     void Execute(IVisitor& visitor,
                  ServerContext& context) const;
--- a/OrthancServer/Sources/Search/DatabaseConstraint.cpp	Wed Sep 04 10:54:00 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/OrthancServer/Sources/Search/DatabaseConstraint.h	Wed Sep 04 10:54:00 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;
-  };
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Search/DatabaseDicomTagConstraint.cpp	Wed Oct 09 11:01:11 2024 +0200
@@ -0,0 +1,112 @@
+/**
+ * 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/>.
+ **/
+
+
+#include "../PrecompiledHeadersServer.h"
+#include "DatabaseDicomTagConstraint.h"
+
+#include "../../../OrthancFramework/Sources/OrthancException.h"
+
+#if ORTHANC_ENABLE_PLUGINS == 1
+#  include "../../Plugins/Engine/PluginsEnumerations.h"
+#endif
+
+#include <boost/lexical_cast.hpp>
+#include <cassert>
+
+
+namespace Orthanc
+{
+  DatabaseDicomTagConstraint::DatabaseDicomTagConstraint(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);
+    }
+  }      
+
+    
+  const std::string& DatabaseDicomTagConstraint::GetValue(size_t index) const
+  {
+    if (index >= values_.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      return values_[index];
+    }
+  }
+
+
+  const std::string& DatabaseDicomTagConstraint::GetSingleValue() const
+  {
+    if (values_.size() != 1)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return values_[0];
+    }
+  }
+
+
+#if ORTHANC_ENABLE_PLUGINS == 1
+  void DatabaseDicomTagConstraint::EncodeForPlugins(OrthancPluginDatabaseConstraint& constraint,
+                                                    std::vector<const char*>& tmpValues) const
+  {
+    memset(&constraint, 0, sizeof(constraint));
+    
+    tmpValues.resize(values_.size());
+
+    for (size_t i = 0; i < values_.size(); i++)
+    {
+      tmpValues[i] = values_[i].c_str();
+    }
+
+    constraint.level = Plugins::Convert(level_);
+    constraint.tagGroup = tag_.GetGroup();
+    constraint.tagElement = tag_.GetElement();
+    constraint.isIdentifierTag = isIdentifier_;
+    constraint.isCaseSensitive = caseSensitive_;
+    constraint.isMandatory = mandatory_;
+    constraint.type = Plugins::Convert(constraintType_);
+    constraint.valuesCount = values_.size();
+    constraint.values = (tmpValues.empty() ? NULL : &tmpValues[0]);
+  }
+#endif    
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Search/DatabaseDicomTagConstraint.h	Wed Oct 09 11:01:11 2024 +0200
@@ -0,0 +1,101 @@
+/**
+ * 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
+
+#include "../../../OrthancFramework/Sources/DicomFormat/DicomMap.h"
+#include "../ServerEnumerations.h"
+#include "IDatabaseConstraint.h"
+
+#if ORTHANC_ENABLE_PLUGINS == 1
+#  include "../../Plugins/Include/orthanc/OrthancCDatabasePlugin.h"
+#endif
+
+namespace Orthanc
+{
+  class DatabaseDicomTagConstraint : public IDatabaseConstraint
+  {
+  private:
+    ResourceType              level_;
+    DicomTag                  tag_;
+    bool                      isIdentifier_;
+    ConstraintType            constraintType_;
+    std::vector<std::string>  values_;
+    bool                      caseSensitive_;
+    bool                      mandatory_;
+
+  public:
+    DatabaseDicomTagConstraint(ResourceType level,
+                               const DicomTag& tag,
+                               bool isIdentifier,
+                               ConstraintType type,
+                               const std::vector<std::string>& values,
+                               bool caseSensitive,
+                               bool mandatory);
+    
+    ResourceType GetLevel() const
+    {
+      return level_;
+    }
+
+    const DicomTag& GetTag() const
+    {
+      return tag_;
+    }
+
+    bool IsIdentifier() const
+    {
+      return isIdentifier_;
+    }
+
+    virtual ConstraintType GetConstraintType() const ORTHANC_OVERRIDE
+    {
+      return constraintType_;
+    }
+
+    virtual size_t GetValuesCount() const ORTHANC_OVERRIDE
+    {
+      return values_.size();
+    }
+
+    virtual const std::string& GetValue(size_t index) const ORTHANC_OVERRIDE;
+
+    virtual const std::string& GetSingleValue() const ORTHANC_OVERRIDE;
+
+    virtual bool IsCaseSensitive() const ORTHANC_OVERRIDE
+    {
+      return caseSensitive_;
+    }
+
+    virtual bool IsMandatory() const ORTHANC_OVERRIDE
+    {
+      return mandatory_;
+    }
+
+
+#if ORTHANC_ENABLE_PLUGINS == 1
+    void EncodeForPlugins(OrthancPluginDatabaseConstraint& constraint,
+                          std::vector<const char*>& tmpValues) const;
+#endif    
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Search/DatabaseDicomTagConstraints.cpp	Wed Oct 09 11:01:11 2024 +0200
@@ -0,0 +1,132 @@
+/**
+ * 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/>.
+ **/
+
+
+#include "../PrecompiledHeadersServer.h"
+#include "DatabaseDicomTagConstraints.h"
+
+#include "../../../OrthancFramework/Sources/OrthancException.h"
+
+#include <boost/lexical_cast.hpp>
+#include <cassert>
+
+
+namespace Orthanc
+{
+  void DatabaseDicomTagConstraints::Clear()
+  {
+    for (size_t i = 0; i < constraints_.size(); i++)
+    {
+      assert(constraints_[i] != NULL);
+      delete constraints_[i];
+    }
+
+    constraints_.clear();
+  }
+
+
+  void DatabaseDicomTagConstraints::AddConstraint(DatabaseDicomTagConstraint* constraint)
+  {
+    if (constraint == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    else
+    {
+      constraints_.push_back(constraint);
+    }
+  }
+
+
+  const DatabaseDicomTagConstraint& DatabaseDicomTagConstraints::GetConstraint(size_t index) const
+  {
+    if (index >= constraints_.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      assert(constraints_[index] != NULL);
+      return *constraints_[index];
+    }
+  }
+
+
+  std::string DatabaseDicomTagConstraints::Format() const
+  {
+    std::string s;
+
+    for (size_t i = 0; i < constraints_.size(); i++)
+    {
+      assert(constraints_[i] != NULL);
+      const DatabaseDicomTagConstraint& 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/OrthancServer/Sources/Search/DatabaseDicomTagConstraints.h	Wed Oct 09 11:01:11 2024 +0200
@@ -0,0 +1,61 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-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
+
+#include "DatabaseDicomTagConstraint.h"
+
+#include <deque>
+
+namespace Orthanc
+{
+  class DatabaseDicomTagConstraints : public boost::noncopyable
+  {
+  private:
+    std::deque<DatabaseDicomTagConstraint*>  constraints_;
+
+  public:
+    ~DatabaseDicomTagConstraints()
+    {
+      Clear();
+    }
+
+    void Clear();
+
+    void AddConstraint(DatabaseDicomTagConstraint* constraint);  // Takes ownership
+
+    bool IsEmpty() const
+    {
+      return constraints_.empty();
+    }
+
+    size_t GetSize() const
+    {
+      return constraints_.size();
+    }
+
+    const DatabaseDicomTagConstraint& GetConstraint(size_t index) const;
+
+    std::string Format() const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Search/DatabaseMetadataConstraint.cpp	Wed Oct 09 11:01:11 2024 +0200
@@ -0,0 +1,93 @@
+/**
+ * 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/>.
+ **/
+
+
+#include "../PrecompiledHeadersServer.h"
+#include "DatabaseMetadataConstraint.h"
+
+#include "../../../OrthancFramework/Sources/OrthancException.h"
+
+#include <boost/lexical_cast.hpp>
+#include <cassert>
+
+
+namespace Orthanc
+{
+  DatabaseMetadataConstraint::DatabaseMetadataConstraint(MetadataType metadata,
+                                                         ConstraintType type,
+                                                         const std::vector<std::string>& values,
+                                                         bool caseSensitive) :
+    metadata_(metadata),
+    constraintType_(type),
+    values_(values),
+    caseSensitive_(caseSensitive)
+  {
+    if (type != ConstraintType_List &&
+        values_.size() != 1)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }      
+
+
+  DatabaseMetadataConstraint::DatabaseMetadataConstraint(MetadataType metadata,
+                                                         ConstraintType type,
+                                                         const std::string& value,
+                                                         bool caseSensitive) :
+    metadata_(metadata),
+    constraintType_(type),
+    caseSensitive_(caseSensitive)
+  {
+    if (type == ConstraintType_List)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    values_.push_back(value);
+  }      
+
+  const std::string& DatabaseMetadataConstraint::GetValue(size_t index) const
+  {
+    if (index >= values_.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      return values_[index];
+    }
+  }
+
+
+  const std::string& DatabaseMetadataConstraint::GetSingleValue() const
+  {
+    if (values_.size() != 1)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return values_[0];
+    }
+  }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Search/DatabaseMetadataConstraint.h	Wed Oct 09 11:01:11 2024 +0200
@@ -0,0 +1,81 @@
+/**
+ * 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
+
+#include "../../../OrthancFramework/Sources/DicomFormat/DicomMap.h"
+#include "../ServerEnumerations.h"
+#include "IDatabaseConstraint.h"
+
+namespace Orthanc
+{
+  class DatabaseMetadataConstraint : public IDatabaseConstraint
+  {
+  private:
+    MetadataType              metadata_;
+    ConstraintType            constraintType_;
+    std::vector<std::string>  values_;
+    bool                      caseSensitive_;
+
+  public:
+    DatabaseMetadataConstraint(MetadataType metadata,
+                               ConstraintType type,
+                               const std::string& value,
+                               bool caseSensitive);
+
+    DatabaseMetadataConstraint(MetadataType metadata,
+                               ConstraintType type,
+                               const std::vector<std::string>& values,
+                               bool caseSensitive);
+
+    const MetadataType& GetMetadata() const
+    {
+      return metadata_;
+    }
+
+    virtual ConstraintType GetConstraintType() const ORTHANC_OVERRIDE
+    {
+      return constraintType_;
+    }
+
+    virtual size_t GetValuesCount() const ORTHANC_OVERRIDE
+    {
+      return values_.size();
+    }
+
+    virtual const std::string& GetValue(size_t index) const ORTHANC_OVERRIDE;
+
+    virtual const std::string& GetSingleValue() const ORTHANC_OVERRIDE;
+
+    virtual bool IsCaseSensitive() const ORTHANC_OVERRIDE
+    {
+      return caseSensitive_;
+    }
+
+    virtual bool IsMandatory() const ORTHANC_OVERRIDE
+    {
+      return true;
+    }
+
+  };
+}
--- a/OrthancServer/Sources/Search/DicomTagConstraint.cpp	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/Sources/Search/DicomTagConstraint.cpp	Wed Oct 09 11:01:11 2024 +0200
@@ -30,7 +30,7 @@
 
 #include "../../../OrthancFramework/Sources/OrthancException.h"
 #include "../../../OrthancFramework/Sources/Toolbox.h"
-#include "DatabaseConstraint.h"
+#include "DatabaseDicomTagConstraint.h"
 
 #include <boost/regex.hpp>
 
@@ -154,7 +154,7 @@
   }
     
 
-  DicomTagConstraint::DicomTagConstraint(const DatabaseConstraint& constraint) :
+  DicomTagConstraint::DicomTagConstraint(const DatabaseDicomTagConstraint& constraint) :
     tag_(constraint.GetTag()),
     constraintType_(constraint.GetConstraintType()),
     caseSensitive_(constraint.IsCaseSensitive()),
@@ -369,9 +369,9 @@
   }
 
 
-  DatabaseConstraint* DicomTagConstraint::ConvertToDatabaseConstraint(bool& isIdentical,
-                                                                      ResourceType level,
-                                                                      DicomTagType tagType) const
+  DatabaseDicomTagConstraint* DicomTagConstraint::ConvertToDatabaseConstraint(bool& isIdentical,
+                                                                              ResourceType level,
+                                                                              DicomTagType tagType) const
   {
     bool isIdentifier, caseSensitive;
     
@@ -415,7 +415,7 @@
       }
     }
 
-    return new DatabaseConstraint(level, tag_, isIdentifier, constraintType_,
-                                  values, caseSensitive, mandatory_);
+    return new DatabaseDicomTagConstraint(level, tag_, isIdentifier, constraintType_,
+                                          values, caseSensitive, mandatory_);
   }  
 }
--- a/OrthancServer/Sources/Search/DicomTagConstraint.h	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/Sources/Search/DicomTagConstraint.h	Wed Oct 09 11:01:11 2024 +0200
@@ -25,7 +25,7 @@
 
 #include "../ServerEnumerations.h"
 #include "../../../OrthancFramework/Sources/DicomFormat/DicomMap.h"
-#include "DatabaseConstraint.h"
+#include "DatabaseDicomTagConstraint.h"
 
 #include <boost/shared_ptr.hpp>
 
@@ -62,7 +62,7 @@
 
     explicit DicomTagConstraint(const DicomTagConstraint& other);
     
-    explicit DicomTagConstraint(const DatabaseConstraint& constraint);
+    explicit DicomTagConstraint(const DatabaseDicomTagConstraint& constraint);
 
     const DicomTag& GetTag() const
     {
@@ -109,8 +109,8 @@
 
     std::string Format() const;
 
-    DatabaseConstraint* ConvertToDatabaseConstraint(bool& isIdentical /* out */,
-                                                    ResourceType level,
-                                                    DicomTagType tagType) const;
+    DatabaseDicomTagConstraint* ConvertToDatabaseConstraint(bool& isIdentical /* out */,
+                                                            ResourceType level,
+                                                            DicomTagType tagType) const;
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Search/IDatabaseConstraint.h	Wed Oct 09 11:01:11 2024 +0200
@@ -0,0 +1,50 @@
+/**
+ * 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
+
+#include "../ServerEnumerations.h"
+#include <boost/noncopyable.hpp>
+
+namespace Orthanc
+{
+  class IDatabaseConstraint : public boost::noncopyable
+  {
+  public:
+    virtual ~IDatabaseConstraint()
+    {
+    }
+    
+    virtual ConstraintType GetConstraintType() const = 0;
+
+    virtual size_t GetValuesCount() const = 0;
+
+    virtual const std::string& GetValue(size_t index) const = 0;
+
+    virtual const std::string& GetSingleValue() const = 0;
+
+    virtual bool IsCaseSensitive() const  = 0;
+
+    virtual bool IsMandatory() const  = 0;
+  };
+}
--- a/OrthancServer/Sources/Search/ISqlLookupFormatter.cpp	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/Sources/Search/ISqlLookupFormatter.cpp	Wed Oct 09 11:01:11 2024 +0200
@@ -21,26 +21,14 @@
  **/
 
 
-#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 "../PrecompiledHeadersServer.h"
 #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 "../../../OrthancFramework/Sources/OrthancException.h"
+#include "../../../OrthancFramework/Sources/Toolbox.h"
+#include "../Database/FindRequest.h"
+#include "DatabaseDicomTagConstraint.h"
+#include "../Database/MainDicomTagsRegistry.h"
 
 #include <cassert>
 #include <boost/lexical_cast.hpp>
@@ -69,11 +57,32 @@
         throw OrthancException(ErrorCode_InternalError);
     }
   }      
-  
+
+  static std::string FormatLevel(const char* prefix, ResourceType level)
+  {
+    switch (level)
+    {
+      case ResourceType_Patient:
+        return std::string(prefix) + "patients";
+        
+      case ResourceType_Study:
+        return std::string(prefix) + "studies";
+        
+      case ResourceType_Series:
+        return std::string(prefix) + "series";
+        
+      case ResourceType_Instance:
+        return std::string(prefix) + "instances";
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+  }      
+
 
   static bool FormatComparison(std::string& target,
                                ISqlLookupFormatter& formatter,
-                               const DatabaseConstraint& constraint,
+                               const IDatabaseConstraint& constraint,
                                size_t index,
                                bool escapeBrackets)
   {
@@ -245,7 +254,7 @@
 
 
   static void FormatJoin(std::string& target,
-                         const DatabaseConstraint& constraint,
+                         const DatabaseDicomTagConstraint& constraint,
                          size_t index)
   {
     std::string tag = "t" + boost::lexical_cast<std::string>(index);
@@ -275,6 +284,99 @@
                boost::lexical_cast<std::string>(constraint.GetTag().GetElement()));
   }
 
+  static void FormatJoin(std::string& target,
+                         const DatabaseMetadataConstraint& constraint,
+                         ResourceType level,
+                         size_t index)
+  {
+    std::string tag = "t" + boost::lexical_cast<std::string>(index);
+
+    if (constraint.IsMandatory())
+    {
+      target = " INNER JOIN ";
+    }
+    else
+    {
+      target = " LEFT JOIN ";
+    }
+
+    target += "Metadata ";
+
+    target += tag + " ON " + tag + ".id = " + FormatLevel(level) +
+               ".internalId AND " + tag + ".type = " +
+               boost::lexical_cast<std::string>(constraint.GetMetadata());
+  }
+
+
+  static void FormatJoinForOrdering(std::string& target,
+                                    const DicomTag& tag,
+                                    size_t index,
+                                    ResourceType requestLevel)
+  {
+    std::string orderArg = "order" + boost::lexical_cast<std::string>(index);
+
+    target.clear();
+
+    ResourceType tagLevel;
+    DicomTagType tagType;
+    MainDicomTagsRegistry registry;
+
+    registry.LookupTag(tagLevel, tagType, tag);
+
+    if (tagLevel == ResourceType_Patient && requestLevel == ResourceType_Study)
+    { // Patient tags are copied at study level
+      tagLevel = ResourceType_Study;
+    }
+
+    std::string tagTable;
+    if (tagType == DicomTagType_Identifier)
+    {
+      tagTable = "DicomIdentifiers ";
+    }
+    else
+    {
+      tagTable = "MainDicomTags ";
+    }
+
+    std::string tagFilter = orderArg + ".tagGroup = " + boost::lexical_cast<std::string>(tag.GetGroup()) + " AND " + orderArg + ".tagElement = " + boost::lexical_cast<std::string>(tag.GetElement());
+
+    if (tagLevel == requestLevel)
+    {
+      target = " LEFT JOIN " + tagTable + " " + orderArg + " ON " + orderArg + ".id = " + FormatLevel(requestLevel) +
+                ".internalId AND " + tagFilter;
+    }
+    else if (static_cast<int32_t>(requestLevel) - static_cast<int32_t>(tagLevel) == 1)
+    {
+      target = " INNER JOIN Resources " + orderArg + "parent ON " + orderArg + "parent.internalId = " + FormatLevel(requestLevel) + ".parentId "
+               " LEFT JOIN " + tagTable + " " + orderArg + " ON " + orderArg + ".id = " + orderArg + "parent.internalId AND " + tagFilter;
+    }
+    else if (static_cast<int32_t>(requestLevel) - static_cast<int32_t>(tagLevel) == 2)
+    {
+      target = " INNER JOIN Resources " + orderArg + "parent ON " + orderArg + "parent.internalId = " + FormatLevel(requestLevel) + ".parentId "
+               " INNER JOIN Resources " + orderArg + "grandparent ON " + orderArg + "grandparent.internalId = " + orderArg + "parent.parentId "
+               " LEFT JOIN " + tagTable + " " + orderArg + " ON " + orderArg + ".id = " + orderArg + "grandparent.internalId AND " + tagFilter;
+    }
+    else if (static_cast<int32_t>(requestLevel) - static_cast<int32_t>(tagLevel) == 3)
+    {
+      target = " INNER JOIN Resources " + orderArg + "parent ON " + orderArg + "parent.internalId = " + FormatLevel(requestLevel) + ".parentId "
+               " INNER JOIN Resources " + orderArg + "grandparent ON " + orderArg + "grandparent.internalId = " + orderArg + "parent.parentId "
+               " INNER JOIN Resources " + orderArg + "grandgrandparent ON " + orderArg + "grandgrandparent.internalId = " + orderArg + "grandparent.parentId "
+               " LEFT JOIN " + tagTable + " " + orderArg + " ON " + orderArg + ".id = " + orderArg + "grandgrandparent.internalId AND " + tagFilter;
+    }
+  }
+
+  static void FormatJoinForOrdering(std::string& target,
+                                    const MetadataType& metadata,
+                                    size_t index,
+                                    ResourceType requestLevel)
+  {
+    std::string arg = "order" + boost::lexical_cast<std::string>(index);
+
+
+    target = " INNER JOIN Metadata " + arg + " ON " + arg + ".id = " + FormatLevel(requestLevel) +
+             ".internalId AND " + arg + ".type = " +
+             boost::lexical_cast<std::string>(metadata);
+  }
 
   static std::string Join(const std::list<std::string>& values,
                           const std::string& prefix,
@@ -309,7 +411,7 @@
 
   static bool FormatComparison2(std::string& target,
                                 ISqlLookupFormatter& formatter,
-                                const DatabaseConstraint& constraint,
+                                const DatabaseDicomTagConstraint& constraint,
                                 bool escapeBrackets)
   {
     std::string comparison;
@@ -479,7 +581,7 @@
   void ISqlLookupFormatter::GetLookupLevels(ResourceType& lowerLevel,
                                             ResourceType& upperLevel,
                                             const ResourceType& queryLevel,
-                                            const DatabaseConstraints& lookup)
+                                            const DatabaseDicomTagConstraints& lookup)
   {
     assert(ResourceType_Patient < ResourceType_Study &&
            ResourceType_Study < ResourceType_Series &&
@@ -507,12 +609,13 @@
 
   void ISqlLookupFormatter::Apply(std::string& sql,
                                   ISqlLookupFormatter& formatter,
-                                  const DatabaseConstraints& lookup,
+                                  const DatabaseDicomTagConstraints& lookup,
                                   ResourceType queryLevel,
                                   const std::set<std::string>& labels,
                                   LabelsConstraint labelsConstraint,
                                   size_t limit)
   {
+    // get the limit levels of the DICOM Tags lookup
     ResourceType lowerLevel, upperLevel;
     GetLookupLevels(lowerLevel, upperLevel, queryLevel, lookup);
 
@@ -527,7 +630,7 @@
     
     for (size_t i = 0; i < lookup.GetSize(); i++)
     {
-      const DatabaseConstraint& constraint = lookup.GetConstraint(i);
+      const DatabaseDicomTagConstraint& constraint = lookup.GetConstraint(i);
 
       std::string comparison;
       
@@ -617,7 +720,7 @@
     }
   }
 
-#if ORTHANC_BUILDING_SERVER_LIBRARY == 1
+
   void ISqlLookupFormatter::Apply(std::string& sql,
                                   ISqlLookupFormatter& formatter,
                                   const FindRequest& request)
@@ -632,59 +735,144 @@
     assert(upperLevel <= queryLevel &&
            queryLevel <= lowerLevel);
 
+    std::string ordering;
+    std::string orderingJoins;
+
+    if (request.GetOrdering().size() > 0)
+    {
+      int counter = 0;
+      std::vector<std::string> orderByFields;
+      for (std::deque<FindRequest::Ordering*>::const_iterator it = request.GetOrdering().begin(); it != request.GetOrdering().end(); ++it)
+      {
+        std::string orderingJoin;
+
+        switch ((*it)->GetKeyType())
+        {
+          case FindRequest::KeyType_DicomTag:
+            FormatJoinForOrdering(orderingJoin, (*it)->GetDicomTag(), counter, request.GetLevel());
+            break;
+          case FindRequest::KeyType_Metadata:
+            FormatJoinForOrdering(orderingJoin, (*it)->GetMetadataType(), counter, request.GetLevel());
+            break;
+          default:
+            throw OrthancException(ErrorCode_InternalError);
+        }
+        orderingJoins += orderingJoin;
+        
+        std::string orderByField;
+
+#if ORTHANC_SQLITE_VERSION < 3030001
+        // this is a way to push NULL values at the end before "NULLS LAST" was introduced:
+        // first filter by 0/1 and then by the column value itself
+        orderByField += "order" + boost::lexical_cast<std::string>(counter) + ".value IS NULL, ";
+#endif
+        orderByField += "order" + boost::lexical_cast<std::string>(counter) + ".value";
+
+        if ((*it)->GetDirection() == FindRequest::OrderingDirection_Ascending)
+        {
+          orderByField += " ASC";
+        }
+        else
+        {
+          orderByField += " DESC";
+        }
+        orderByFields.push_back(orderByField);
+        ++counter;
+      }
+
+      std::string orderByFieldsString;
+      Toolbox::JoinStrings(orderByFieldsString, orderByFields, ", ");
+
+      ordering = "ROW_NUMBER() OVER (ORDER BY " + orderByFieldsString;
+#if ORTHANC_SQLITE_VERSION >= 3030001
+      ordering += " NULLS LAST";
+#endif
+      ordering += ") AS rowNumber";
+    }
+    else
+    {
+      ordering = "ROW_NUMBER() OVER (ORDER BY " + strQueryLevel + ".publicId) AS rowNumber";  // we need a default ordering in order to make default queries repeatable when using since&limit
+    }
 
     sql = ("SELECT " +
            strQueryLevel + ".publicId, " +
-           strQueryLevel + ".internalId" +
+           strQueryLevel + ".internalId, " +
+           ordering + 
            " FROM Resources AS " + strQueryLevel);
 
 
     std::string joins, comparisons;
 
+    // handle parent constraints
     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();
 
-      ResourceType topParentLevel = request.GetOrthancIdentifiers().DetectLevel();
-      const std::string& strTopParentLevel = FormatLevel(topParentLevel);
+      if (topParentLevel == queryLevel)
+      {
+        comparisons += " AND " + FormatLevel(topParentLevel) + ".publicId = " + formatter.GenerateParameter(request.GetOrthancIdentifiers().GetLevel(topParentLevel));
+      }
+      else
+      {
+        comparisons += " AND " + FormatLevel("parent", topParentLevel) + ".publicId = " + formatter.GenerateParameter(request.GetOrthancIdentifiers().GetLevel(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");
+        for (int level = queryLevel; level > topParentLevel; level--)
+        {
+          joins += " INNER JOIN Resources " +
+                  FormatLevel("parent", static_cast<ResourceType>(level - 1)) + " ON " +
+                  FormatLevel("parent", static_cast<ResourceType>(level - 1)) + ".internalId = ";
+          if (level == queryLevel)
+          {
+            joins += FormatLevel(static_cast<ResourceType>(level)) + ".parentId";
+          }
+          else
+          {
+            joins += FormatLevel("parent", static_cast<ResourceType>(level)) + ".parentId";
+          }
+        }
       }
     }
-    else
+
+    size_t count = 0;
+    
+    const DatabaseDicomTagConstraints& dicomTagsConstraints = request.GetDicomTagConstraints();
+    for (size_t i = 0; i < dicomTagsConstraints.GetSize(); i++)
     {
-      size_t count = 0;
+      const DatabaseDicomTagConstraint& constraint = dicomTagsConstraints.GetConstraint(i);
+
+      std::string comparison;
       
-      const DatabaseConstraints& dicomTagsConstraints = request.GetDicomTagConstraints();
-      for (size_t i = 0; i < dicomTagsConstraints.GetSize(); i++)
+      if (FormatComparison(comparison, formatter, constraint, count, escapeBrackets))
       {
-        const DatabaseConstraint& constraint = dicomTagsConstraints.GetConstraint(i);
+        std::string join;
+        FormatJoin(join, constraint, count);
+        joins += join;
 
-        std::string comparison;
+        if (!comparison.empty())
+        {
+          comparisons += " AND " + comparison;
+        }
         
-        if (FormatComparison(comparison, formatter, constraint, count, escapeBrackets))
-        {
-          std::string join;
-          FormatJoin(join, constraint, count);
-          joins += join;
+        count ++;
+      }
+    }
 
-          if (!comparison.empty())
-          {
-            comparisons += " AND " + comparison;
-          }
-          
-          count ++;
+    for (std::deque<DatabaseMetadataConstraint*>::const_iterator it = request.GetMetadataConstraint().begin(); it != request.GetMetadataConstraint().end(); ++it)
+    {
+      std::string comparison;
+      
+      if (FormatComparison(comparison, formatter, *(*it), count, escapeBrackets))
+      {
+        std::string join;
+        FormatJoin(join, *(*it), request.GetLevel(), count);
+        joins += join;
+
+        if (!comparison.empty())
+        {
+          comparisons += " AND " + comparison;
         }
+        
+        count ++;
       }
     }
 
@@ -748,27 +936,18 @@
                       ".internalId AND selectedLabels.label IN (" + Join(formattedLabels, "", ", ") + ")) " + condition);
     }
 
-    sql += joins + Join(where, " WHERE ", " AND ");
+    sql += joins + orderingJoins + Join(where, " WHERE ", " AND ");
 
     if (request.HasLimits())
     {
-      if (request.GetLimitsCount() > 0)
-      {
-        sql += " LIMIT " + boost::lexical_cast<std::string>(request.GetLimitsCount());
-      }
-      if (request.GetLimitsSince() > 0)
-      {
-        sql += " OFFSET " + boost::lexical_cast<std::string>(request.GetLimitsSince());
-      }
+      sql += formatter.FormatLimits(request.GetLimitsSince(), request.GetLimitsCount());
     }
-
   }
-#endif
 
 
   void ISqlLookupFormatter::ApplySingleLevel(std::string& sql,
                                              ISqlLookupFormatter& formatter,
-                                             const DatabaseConstraints& lookup,
+                                             const DatabaseDicomTagConstraints& lookup,
                                              ResourceType queryLevel,
                                              const std::set<std::string>& labels,
                                              LabelsConstraint labelsConstraint,
@@ -787,7 +966,7 @@
 
     for (size_t i = 0; i < lookup.GetSize(); i++)
     {
-      const DatabaseConstraint& constraint = lookup.GetConstraint(i);
+      const DatabaseDicomTagConstraint& constraint = lookup.GetConstraint(i);
 
       std::string comparison;
       
@@ -879,5 +1058,4 @@
       sql += " LIMIT " + boost::lexical_cast<std::string>(limit);
     }
   }
-
 }
--- a/OrthancServer/Sources/Search/ISqlLookupFormatter.h	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/Sources/Search/ISqlLookupFormatter.h	Wed Oct 09 11:01:11 2024 +0200
@@ -23,18 +23,14 @@
 
 #pragma once
 
-#if ORTHANC_BUILDING_SERVER_LIBRARY == 1
-#  include "../../../OrthancFramework/Sources/Enumerations.h"
-#else
-#  include <Enumerations.h>
-#endif
+#include "../../../OrthancFramework/Sources/Enumerations.h"
 
 #include <boost/noncopyable.hpp>
 #include <vector>
 
 namespace Orthanc
 {
-  class DatabaseConstraints;
+  class DatabaseDicomTagConstraints;
   class FindRequest;
 
   enum LabelsConstraint
@@ -44,7 +40,6 @@
     LabelsConstraint_None
   };
 
-  // This class is also used by the "orthanc-databases" project
   class ISqlLookupFormatter : public boost::noncopyable
   {
   public:
@@ -58,6 +53,8 @@
 
     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:
@@ -68,11 +65,11 @@
     static void GetLookupLevels(ResourceType& lowerLevel,
                                 ResourceType& upperLevel,
                                 const ResourceType& queryLevel,
-                                const DatabaseConstraints& lookup);
+                                const DatabaseDicomTagConstraints& lookup);
 
     static void Apply(std::string& sql,
                       ISqlLookupFormatter& formatter,
-                      const DatabaseConstraints& lookup,
+                      const DatabaseDicomTagConstraints& lookup,
                       ResourceType queryLevel,
                       const std::set<std::string>& labels,  // New in Orthanc 1.12.0
                       LabelsConstraint labelsConstraint,    // New in Orthanc 1.12.0
@@ -80,16 +77,14 @@
 
     static void ApplySingleLevel(std::string& sql,
                                  ISqlLookupFormatter& formatter,
-                                 const DatabaseConstraints& lookup,
+                                 const DatabaseDicomTagConstraints& 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/OrthancServer/Sources/ServerContext.cpp	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/Sources/ServerContext.cpp	Wed Oct 09 11:01:11 2024 +0200
@@ -355,8 +355,9 @@
   ServerContext::ServerContext(IDatabaseWrapper& database,
                                IStorageArea& area,
                                bool unitTesting,
-                               size_t maxCompletedJobs) :
-    index_(*this, database, (unitTesting ? 20 : 500)),
+                               size_t maxCompletedJobs,
+                               bool readOnly) :
+    index_(*this, database, (unitTesting ? 20 : 500), readOnly),
     area_(area),
     compressionEnabled_(false),
     storeMD5_(true),
@@ -382,6 +383,7 @@
     ingestTranscodingOfUncompressed_(true),
     ingestTranscodingOfCompressed_(true),
     preferredTransferSyntax_(DicomTransferSyntax_LittleEndianExplicit),
+    readOnly_(readOnly),
     deidentifyLogs_(false),
     serverStartTimeUtc_(boost::posix_time::second_clock::universal_time())
   {
@@ -398,7 +400,14 @@
           new SharedArchive(lock.GetConfiguration().GetUnsignedIntegerParameter("MediaArchiveSize", 1)));
         defaultLocalAet_ = lock.GetConfiguration().GetOrthancAET();
         jobsEngine_.SetWorkersCount(lock.GetConfiguration().GetUnsignedIntegerParameter("ConcurrentJobs", 2));
+
         saveJobs_ = lock.GetConfiguration().GetBooleanParameter("SaveJobs", true);
+        if (readOnly_ && saveJobs_)
+        {
+          LOG(WARNING) << "READ-ONLY SYSTEM: SaveJobs = true is incompatible with a ReadOnly system, ignoring this configuration";
+          saveJobs_ = false;
+        }
+
         metricsRegistry_->SetEnabled(lock.GetConfiguration().GetBooleanParameter("MetricsEnabled", true));
 
         // New configuration options in Orthanc 1.5.1
@@ -1030,11 +1039,91 @@
     dicomAsJson["7fe0,0010"] = pixelData;
   }
 
+
+  static bool LookupMetadata(std::string& value,
+                             MetadataType key,
+                             const std::map<MetadataType, std::string>& instanceMetadata)
+  {
+    std::map<MetadataType, std::string>::const_iterator found = instanceMetadata.find(key);
+
+    if (found == instanceMetadata.end())
+    {
+      return false;
+    }
+    else
+    {
+      value = found->second;
+      return true;
+    }
+  }
+
+
+  static bool LookupAttachment(FileInfo& target,
+                               FileContentType type,
+                               const std::map<FileContentType, FileInfo>& attachments)
+  {
+    std::map<FileContentType, FileInfo>::const_iterator found = attachments.find(type);
+
+    if (found == attachments.end())
+    {
+      return false;
+    }
+    else if (found->second.GetContentType() == type)
+    {
+      target = found->second;
+      return true;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_DatabasePlugin);
+    }
+  }
+
   
   void ServerContext::ReadDicomAsJson(Json::Value& result,
                                       const std::string& instancePublicId,
                                       const std::set<DicomTag>& ignoreTagLength)
   {
+    // TODO-FIND: This is a compatibility method, should be removed
+
+    std::map<MetadataType, std::string> metadata;
+    std::map<FileContentType, FileInfo> attachments;
+
+    FileInfo attachment;
+    int64_t revision;  // Ignored
+
+    if (index_.LookupAttachment(attachment, revision, instancePublicId, FileContentType_Dicom))
+    {
+      attachments[FileContentType_Dicom] = attachment;
+    }
+
+    if (index_.LookupAttachment(attachment, revision, instancePublicId, FileContentType_DicomUntilPixelData))
+    {
+      attachments[FileContentType_DicomUntilPixelData] = attachment;
+    }
+
+    if (index_.LookupAttachment(attachment, revision, instancePublicId, FileContentType_DicomAsJson))
+    {
+      attachments[FileContentType_DicomAsJson] = attachment;
+    }
+
+    std::string s;
+    if (index_.LookupMetadata(s, revision, instancePublicId, ResourceType_Instance,
+                              MetadataType_Instance_PixelDataOffset))
+    {
+      metadata[MetadataType_Instance_PixelDataOffset] = s;
+    }
+
+    ReadDicomAsJson(result, instancePublicId, metadata, attachments, ignoreTagLength);
+  }
+
+
+  void ServerContext::ReadDicomAsJson(Json::Value& result,
+                                      const std::string& instancePublicId,
+                                      const std::map<MetadataType, std::string>& instanceMetadata,
+                                      const std::map<FileContentType, FileInfo>& instanceAttachments,
+                                      const std::set<DicomTag>& ignoreTagLength)
+  {
     /**
      * CASE 1: The DICOM file, truncated at pixel data, is available
      * as an attachment (it was created either because the storage
@@ -1043,9 +1132,8 @@
      **/
     
     FileInfo attachment;
-    int64_t revision;  // Ignored
 
-    if (index_.LookupAttachment(attachment, revision, instancePublicId, FileContentType_DicomUntilPixelData))
+    if (LookupAttachment(attachment, FileContentType_DicomUntilPixelData, instanceAttachments))
     {
       std::string dicom;
 
@@ -1071,8 +1159,7 @@
 
       {
         std::string s;
-        if (index_.LookupMetadata(s, revision, instancePublicId, ResourceType_Instance,
-                                  MetadataType_Instance_PixelDataOffset))
+        if (LookupMetadata(s, MetadataType_Instance_PixelDataOffset, instanceMetadata))
         {
           hasPixelDataOffset = false;
 
@@ -1103,7 +1190,7 @@
 
       if (hasPixelDataOffset &&
           area_.HasReadRange() &&
-          index_.LookupAttachment(attachment, revision, instancePublicId, FileContentType_Dicom) &&
+          LookupAttachment(attachment, FileContentType_Dicom, instanceAttachments) &&
           attachment.GetCompressionType() == CompressionType_None)
       {
         /**
@@ -1126,7 +1213,7 @@
         InjectEmptyPixelData(result);
       }
       else if (ignoreTagLength.empty() &&
-               index_.LookupAttachment(attachment, revision, instancePublicId, FileContentType_DicomAsJson))
+               LookupAttachment(attachment, FileContentType_DicomAsJson, instanceAttachments))
       {
         /**
          * CASE 3: This instance was created using Orthanc <=
@@ -1755,6 +1842,7 @@
       }
     }
 
+#if ORTHANC_ENABLE_PLUGINS == 1
     if (HasPlugins() && GetPlugins().HasCustomTranscoder())
     {
       LOG(INFO) << "The plugins and built-in image decoders failed to decode a frame, "
@@ -1772,6 +1860,7 @@
         return file->DecodeFrame(frameIndex);
       }
     }
+#endif
 
     return NULL;
   }
--- a/OrthancServer/Sources/ServerContext.h	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/Sources/ServerContext.h	Wed Oct 09 11:01:11 2024 +0200
@@ -258,6 +258,7 @@
     boost::mutex dynamicOptionsMutex_;
     bool isUnknownSopClassAccepted_;
     std::set<DicomTransferSyntax>  acceptedTransferSyntaxes_;
+    bool readOnly_;
 
     StoreResult StoreAfterTranscoding(std::string& resultPublicId,
                                       DicomInstanceToStore& dicom,
@@ -302,7 +303,8 @@
     ServerContext(IDatabaseWrapper& database,
                   IStorageArea& area,
                   bool unitTesting,
-                  size_t maxCompletedJobs);
+                  size_t maxCompletedJobs,
+                  bool readOnly);
 
     ~ServerContext();
 
@@ -325,6 +327,15 @@
     {
       return compressionEnabled_;
     }
+    bool IsReadOnly() const
+    {
+      return readOnly_;
+    }
+
+    bool IsSaveJobs() const
+    {
+      return saveJobs_;
+    }
 
     bool AddAttachment(int64_t& newRevision,
                        const std::string& resourceId,
@@ -354,10 +365,16 @@
 
     void ReadDicomAsJson(Json::Value& result,
                          const std::string& instancePublicId,
+                         const std::map<MetadataType, std::string>& instanceMetadata,
+                         const std::map<FileContentType, FileInfo>& instanceAttachments,
                          const std::set<DicomTag>& ignoreTagLength);
 
     void ReadDicomAsJson(Json::Value& result,
-                         const std::string& instancePublicId);
+                         const std::string& instancePublicId,
+                         const std::set<DicomTag>& ignoreTagLength);  // TODO-FIND: Can this be removed?
+
+    void ReadDicomAsJson(Json::Value& result,
+                         const std::string& instancePublicId);  // TODO-FIND: Can this be removed?
 
     void ReadDicom(std::string& dicom,
                    const std::string& instancePublicId);
--- a/OrthancServer/Sources/ServerEnumerations.cpp	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/Sources/ServerEnumerations.cpp	Wed Oct 09 11:01:11 2024 +0200
@@ -431,6 +431,86 @@
     }
   }
 
+  ChangeType StringToChangeType(const std::string& value)
+  {
+    if (value == "CompletedSeries")
+    {
+      return ChangeType_CompletedSeries;
+    }
+    else if (value == "NewInstance")
+    {
+      return ChangeType_NewInstance;
+    }
+    else if (value == "NewPatient")
+    {
+      return ChangeType_NewPatient;
+    }
+    else if (value == "NewSeries")
+    {
+      return ChangeType_NewSeries;
+    }
+    else if (value == "NewStudy")
+    {
+      return ChangeType_NewStudy;
+    }
+    else if (value == "AnonymizedStudy")
+    {
+      return ChangeType_AnonymizedStudy;
+    }
+    else if (value == "AnonymizedSeries")
+    {
+      return ChangeType_AnonymizedSeries;
+    }
+    else if (value == "ModifiedStudy")
+    {
+      return ChangeType_ModifiedStudy;
+    }
+    else if (value == "ModifiedSeries")
+    {
+      return ChangeType_ModifiedSeries;
+    }
+    else if (value == "AnonymizedPatient")
+    {
+      return ChangeType_AnonymizedPatient;
+    }
+    else if (value == "ModifiedPatient")
+    {
+      return ChangeType_ModifiedPatient;
+    }
+    else if (value == "StablePatient")
+    {
+      return ChangeType_StablePatient;
+    }
+    else if (value == "StableStudy")
+    {
+      return ChangeType_StableStudy;
+    }
+    else if (value == "StableSeries")
+    {
+      return ChangeType_StableSeries;
+    }
+    else if (value == "Deleted")
+    {
+      return ChangeType_Deleted;
+    }
+    else if (value == "NewChildInstance")
+    {
+      return ChangeType_NewChildInstance;
+    }
+    else if (value == "UpdatedAttachment")
+    {
+      return ChangeType_UpdatedAttachment;
+    }
+    else if (value == "UpdatedMetadata")
+    {
+      return ChangeType_UpdatedMetadata;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange, "Invalid value for a change: " + value);
+    }
+  }
+
 
   const char* EnumerationToString(Verbosity verbosity)
   {
@@ -536,4 +616,45 @@
         throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
   }
-}
+
+  ResponseContentFlags StringToResponseContent(const std::string& value)
+  {
+    if (value == "MainDicomTags")
+    {
+      return ResponseContentFlags_MainDicomTags;
+    }
+    else if (value == "Metadata")
+    {
+      return ResponseContentFlags_Metadata;
+    }
+    else if (value == "Status")
+    {
+      return ResponseContentFlags_Status;
+    }
+    else if (value == "Parent")
+    {
+      return ResponseContentFlags_Parent;
+    }
+    else if (value == "Children")
+    {
+      return ResponseContentFlags_Children;
+    }
+    else if (value == "Labels")
+    {
+      return ResponseContentFlags_Labels;
+    }
+    else if (value == "Attachments")
+    {
+      return ResponseContentFlags_Attachments;
+    }
+    else if (value == "IsStable")
+    {
+      return ResponseContentFlags_IsStable;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "Unrecognized value for \"ResponseContent\": " + value);
+    }    
+  }
+}
\ No newline at end of file
--- a/OrthancServer/Sources/ServerEnumerations.h	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/Sources/ServerEnumerations.h	Wed Oct 09 11:01:11 2024 +0200
@@ -112,6 +112,49 @@
     TransactionType_ReadWrite
   };
 
+  enum ConstraintType
+  {
+    ConstraintType_Equal,
+    ConstraintType_SmallerOrEqual,
+    ConstraintType_GreaterOrEqual,
+    ConstraintType_Wildcard,
+    ConstraintType_List
+  };
+
+  enum ResponseContentFlags
+  {
+    ResponseContentFlags_ID                   = (1 << 0),
+    ResponseContentFlags_Type                 = (1 << 1),
+    ResponseContentFlags_RequestedTags        = (1 << 2),
+    ResponseContentFlags_MainDicomTags        = (1 << 3),
+    ResponseContentFlags_MetadataLegacy       = (1 << 4),    // when "Expand": true -> all metadata are included at root level
+    ResponseContentFlags_AttachmentsLegacy    = (1 << 5),    // when "Expand": true -> include attachments info at instance level
+    ResponseContentFlags_Metadata             = (1 << 6),    // all metadata are listed in a "Metadata" field
+    ResponseContentFlags_Attachments          = (1 << 7),    // all attachments are listed in a "Attachments" field
+    ResponseContentFlags_Status               = (1 << 8),
+    ResponseContentFlags_Parent               = (1 << 9),
+    ResponseContentFlags_Children             = (1 << 10),
+    ResponseContentFlags_Labels               = (1 << 11),
+    ResponseContentFlags_IsStable             = (1 << 12),
+
+    // Some predefined combinations
+    ResponseContentFlags_ExpandTrue  = (ResponseContentFlags_ID |
+                                        ResponseContentFlags_Type |
+                                        ResponseContentFlags_RequestedTags |
+                                        ResponseContentFlags_MainDicomTags |
+                                        ResponseContentFlags_MetadataLegacy |
+                                        ResponseContentFlags_AttachmentsLegacy | 
+                                        ResponseContentFlags_Status | 
+                                        ResponseContentFlags_Parent | 
+                                        ResponseContentFlags_Children | 
+                                        ResponseContentFlags_Labels |
+                                        ResponseContentFlags_IsStable),  // equivalent to "Expand": true
+    
+    ResponseContentFlags_Default = (ResponseContentFlags_ID |
+                                    ResponseContentFlags_Type |
+                                    ResponseContentFlags_RequestedTags) // minimal content as soon as you have a "ResponseContent"
+    
+  };
 
   /**
    * WARNING: Do not change the explicit values in the enumerations
@@ -207,6 +250,8 @@
     Warnings_001_TagsBeingReadFromStorage,
     Warnings_002_InconsistentDicomTagsInDb,
     Warnings_003_DecoderFailure,              // new in Orthanc 1.12.5
+    Warnings_004_NoMainDicomTagsSignature,    // new in Orthanc 1.12.5
+    Warnings_005_RequestingTagFromLowerResourceLevel    // new in Orthanc 1.12.5
   };
 
 
@@ -239,6 +284,8 @@
 
   Verbosity StringToVerbosity(const std::string& str);
 
+  ResponseContentFlags StringToResponseContent(const std::string& str);
+
   std::string EnumerationToString(FileContentType type);
 
   std::string GetFileContentMime(FileContentType type);
@@ -251,6 +298,8 @@
   const char* EnumerationToString(StoreStatus status);
 
   const char* EnumerationToString(ChangeType type);
+  
+  ChangeType StringToChangeType(const std::string& value);
 
   const char* EnumerationToString(Verbosity verbosity);
 
--- a/OrthancServer/Sources/ServerIndex.cpp	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/Sources/ServerIndex.cpp	Wed Oct 09 11:01:11 2024 +0200
@@ -271,7 +271,7 @@
   {
     Logging::SetCurrentThreadName("DB-STATS");
 
-    static const unsigned int SLEEP_SECONDS = 60;
+    static const unsigned int SLEEP_SECONDS = 10;
 
     if (threadSleepGranularityMilliseconds > 1000)
     {
@@ -346,23 +346,35 @@
 
   ServerIndex::ServerIndex(ServerContext& context,
                            IDatabaseWrapper& db,
-                           unsigned int threadSleepGranularityMilliseconds) :
-    StatelessDatabaseOperations(db),
+                           unsigned int threadSleepGranularityMilliseconds,
+                           bool readOnly) :
+    StatelessDatabaseOperations(db, readOnly),
     done_(false),
     maximumStorageMode_(MaxStorageMode_Recycle),
     maximumStorageSize_(0),
-    maximumPatients_(0)
+    maximumPatients_(0),
+    readOnly_(readOnly)
   {
     SetTransactionContextFactory(new TransactionContextFactory(context));
 
     // Initial recycling if the parameters have changed since the last
     // execution of Orthanc
-    StandaloneRecycling(maximumStorageMode_, maximumStorageSize_, maximumPatients_);
+    if (!readOnly)
+    {
+      StandaloneRecycling(maximumStorageMode_, maximumStorageSize_, maximumPatients_);
+    }
 
     // For some DB engines (like SQLite), make sure we flush the DB to disk at regular interval
     if (GetDatabaseCapabilities().HasFlushToDisk())
     {
-      flushThread_ = boost::thread(FlushThread, this, threadSleepGranularityMilliseconds);
+      if (readOnly)
+      {
+        LOG(WARNING) << "READ-ONLY SYSTEM: not starting the flush disk thread";
+      }
+      else
+      {
+        flushThread_ = boost::thread(FlushThread, this, threadSleepGranularityMilliseconds);
+      }
     }
 
     // For some DB plugins that implements the UpdateAndGetStatistics function, updating 
@@ -370,11 +382,25 @@
     // -> make sure they are updated at regular interval
     if (GetDatabaseCapabilities().HasUpdateAndGetStatistics())
     {
-      updateStatisticsThread_ = boost::thread(UpdateStatisticsThread, this, threadSleepGranularityMilliseconds);
+      if (readOnly)
+      {
+        LOG(WARNING) << "READ-ONLY SYSTEM: not starting the UpdateStatisticsThread";
+      }
+      else
+      {
+        updateStatisticsThread_ = boost::thread(UpdateStatisticsThread, this, threadSleepGranularityMilliseconds);
+      }
     }
 
-    unstableResourcesMonitorThread_ = boost::thread
-      (UnstableResourcesMonitorThread, this, threadSleepGranularityMilliseconds);
+    if (readOnly)
+    {
+      LOG(WARNING) << "READ-ONLY SYSTEM: not starting the unstable resources monitor thread";
+    }
+    else
+    {
+      unstableResourcesMonitorThread_ = boost::thread
+        (UnstableResourcesMonitorThread, this, threadSleepGranularityMilliseconds);
+    }
   }
 
 
--- a/OrthancServer/Sources/ServerIndex.h	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/Sources/ServerIndex.h	Wed Oct 09 11:01:11 2024 +0200
@@ -50,6 +50,7 @@
     MaxStorageMode  maximumStorageMode_;
     uint64_t        maximumStorageSize_;
     unsigned int    maximumPatients_;
+    bool            readOnly_;
 
     static void FlushThread(ServerIndex* that,
                             unsigned int threadSleep);
@@ -67,7 +68,8 @@
   public:
     ServerIndex(ServerContext& context,
                 IDatabaseWrapper& database,
-                unsigned int threadSleepGranularityMilliseconds);
+                unsigned int threadSleepGranularityMilliseconds,
+                bool readOnly);
 
     ~ServerIndex();
 
--- a/OrthancServer/Sources/main.cpp	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/Sources/main.cpp	Wed Oct 09 11:01:11 2024 +0200
@@ -825,6 +825,7 @@
     PrintErrorCode(ErrorCode_MainDicomTagsMultiplyDefined, "A main DICOM Tag has been defined multiple times for the same resource level");
     PrintErrorCode(ErrorCode_ForbiddenAccess, "Access to a resource is forbidden");
     PrintErrorCode(ErrorCode_DuplicateResource, "Duplicate resource");
+    PrintErrorCode(ErrorCode_IncompatibleConfigurations, "Your configuration file contains configuration that are mutually incompatible");
     PrintErrorCode(ErrorCode_SQLiteNotOpened, "SQLite: The database is not opened");
     PrintErrorCode(ErrorCode_SQLiteAlreadyOpened, "SQLite: Connection is already open");
     PrintErrorCode(ErrorCode_SQLiteCannotOpen, "SQLite: Unable to open the database");
@@ -836,7 +837,7 @@
     PrintErrorCode(ErrorCode_SQLiteFlush, "SQLite: Unable to flush the database");
     PrintErrorCode(ErrorCode_SQLiteCannotRun, "SQLite: Cannot run a cached statement");
     PrintErrorCode(ErrorCode_SQLiteCannotStep, "SQLite: Cannot step over a cached statement");
-    PrintErrorCode(ErrorCode_SQLiteBindOutOfRange, "SQLite: Bing a value while out of range (serious error)");
+    PrintErrorCode(ErrorCode_SQLiteBindOutOfRange, "SQLite: Bind a value while out of range (serious error)");
     PrintErrorCode(ErrorCode_SQLitePrepareStatement, "SQLite: Cannot prepare a cached statement");
     PrintErrorCode(ErrorCode_SQLiteTransactionAlreadyStarted, "SQLite: Beginning the same transaction twice");
     PrintErrorCode(ErrorCode_SQLiteTransactionCommit, "SQLite: Failure when committing the transaction");
@@ -1517,6 +1518,7 @@
                                    bool loadJobsFromDatabase)
 {
   size_t maxCompletedJobs;
+  bool readOnly;
   
   {
     OrthancConfiguration::ReaderLock lock;
@@ -1544,6 +1546,9 @@
       LOG(WARNING) << "Setting option \"JobsHistorySize\" to zero is not recommended";
     }
 
+    // New option in Orthanc 1.12.5
+    readOnly = lock.GetConfiguration().GetBooleanParameter("ReadOnly", false);
+    
     // Configuration of DICOM TLS for Orthanc SCU (since Orthanc 1.9.0)
     DicomAssociationParameters::SetDefaultOwnCertificatePath(
       lock.GetConfiguration().GetStringParameter(KEY_DICOM_TLS_PRIVATE_KEY, ""),
@@ -1558,46 +1563,54 @@
       lock.GetConfiguration().GetBooleanParameter(KEY_DICOM_TLS_REMOTE_CERTIFICATE_REQUIRED, true));
   }
   
-  ServerContext context(database, storageArea, false /* not running unit tests */, maxCompletedJobs);
+  ServerContext context(database, storageArea, false /* not running unit tests */, maxCompletedJobs, readOnly);
 
   {
     OrthancConfiguration::ReaderLock lock;
 
-    context.SetCompressionEnabled(lock.GetConfiguration().GetBooleanParameter("StorageCompression", false));
-    context.SetStoreMD5ForAttachments(lock.GetConfiguration().GetBooleanParameter("StoreMD5ForAttachments", true));
+    if (context.IsReadOnly())
+    {
+      LOG(WARNING) << "READ-ONLY SYSTEM: ignoring these configurations: StorageCompression, StoreMD5ForAttachments, OverwriteInstances, MaximumPatientCount, MaximumStorageSize, MaximumStorageMode, SaveJobs"; 
+    }
+    else
+    {
+      context.SetCompressionEnabled(lock.GetConfiguration().GetBooleanParameter("StorageCompression", false));
+      context.SetStoreMD5ForAttachments(lock.GetConfiguration().GetBooleanParameter("StoreMD5ForAttachments", true));
 
-    // New option in Orthanc 1.4.2
-    context.SetOverwriteInstances(lock.GetConfiguration().GetBooleanParameter("OverwriteInstances", false));
+      // New option in Orthanc 1.4.2
+      context.SetOverwriteInstances(lock.GetConfiguration().GetBooleanParameter("OverwriteInstances", false));
 
-    try
-    {
-      context.GetIndex().SetMaximumPatientCount(lock.GetConfiguration().GetUnsignedIntegerParameter("MaximumPatientCount", 0));
-    }
-    catch (...)
-    {
-      context.GetIndex().SetMaximumPatientCount(0);
+      try
+      {
+        context.GetIndex().SetMaximumPatientCount(lock.GetConfiguration().GetUnsignedIntegerParameter("MaximumPatientCount", 0));
+      }
+      catch (...)
+      {
+        context.GetIndex().SetMaximumPatientCount(0);
+      }
+
+      try
+      {
+        uint64_t size = lock.GetConfiguration().GetUnsignedIntegerParameter("MaximumStorageSize", 0);
+        context.GetIndex().SetMaximumStorageSize(size * 1024 * 1024);
+      }
+      catch (...)
+      {
+        context.GetIndex().SetMaximumStorageSize(0);
+      }
+
+      try
+      {
+        std::string mode = lock.GetConfiguration().GetStringParameter("MaximumStorageMode", "Recycle");
+        context.GetIndex().SetMaximumStorageMode(StringToMaxStorageMode(mode));
+      }
+      catch (...)
+      {
+        context.GetIndex().SetMaximumStorageMode(MaxStorageMode_Recycle);
+      }
     }
 
-    try
-    {
-      uint64_t size = lock.GetConfiguration().GetUnsignedIntegerParameter("MaximumStorageSize", 0);
-      context.GetIndex().SetMaximumStorageSize(size * 1024 * 1024);
-    }
-    catch (...)
-    {
-      context.GetIndex().SetMaximumStorageSize(0);
-    }
-
-    try
-    {
-      std::string mode = lock.GetConfiguration().GetStringParameter("MaximumStorageMode", "Recycle");
-      context.GetIndex().SetMaximumStorageMode(StringToMaxStorageMode(mode));
-    }
-    catch (...)
-    {
-      context.GetIndex().SetMaximumStorageMode(MaxStorageMode_Recycle);
-    }
-
+    // note: this config is valid in ReadOnlyMode
     try
     {
       uint64_t size = lock.GetConfiguration().GetUnsignedIntegerParameter("MaximumStorageCacheSize", 128);
@@ -1955,7 +1968,7 @@
           SQLiteDatabaseWrapper inMemoryDatabase;
           inMemoryDatabase.Open();
           MemoryStorageArea inMemoryStorage;
-          ServerContext context(inMemoryDatabase, inMemoryStorage, true /* unit testing */, 0 /* max completed jobs */);
+          ServerContext context(inMemoryDatabase, inMemoryStorage, true /* unit testing */, 0 /* max completed jobs */, false /* readonly */);
           OrthancRestApi restApi(context, false /* no Orthanc Explorer */);
           restApi.GenerateOpenApiDocumentation(openapi);
           context.Stop();
@@ -2006,7 +2019,7 @@
           SQLiteDatabaseWrapper inMemoryDatabase;
           inMemoryDatabase.Open();
           MemoryStorageArea inMemoryStorage;
-          ServerContext context(inMemoryDatabase, inMemoryStorage, true /* unit testing */, 0 /* max completed jobs */);
+          ServerContext context(inMemoryDatabase, inMemoryStorage, true /* unit testing */, 0 /* max completed jobs */, false /* readonly */);
           OrthancRestApi restApi(context, false /* no Orthanc Explorer */);
           restApi.GenerateReStructuredTextCheatSheet(cheatsheet, "https://orthanc.uclouvain.be/api/index.html");
           context.Stop();
--- a/OrthancServer/UnitTestsSources/ServerIndexTests.cpp	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/UnitTestsSources/ServerIndexTests.cpp	Wed Oct 09 11:01:11 2024 +0200
@@ -166,7 +166,7 @@
       
       DicomTagConstraint c(tag, type, value, true, true);
       
-      DatabaseConstraints lookup;
+      DatabaseDicomTagConstraints lookup;
       bool isEquivalent;  // unused
       lookup.AddConstraint(c.ConvertToDatabaseConstraint(isEquivalent, level, DicomTagType_Identifier));
 
@@ -187,7 +187,7 @@
       DicomTagConstraint c1(tag, type1, value1, true, true);
       DicomTagConstraint c2(tag, type2, value2, true, true);
 
-      DatabaseConstraints lookup;
+      DatabaseDicomTagConstraints lookup;
       bool isEquivalent;  // unused
       lookup.AddConstraint(c1.ConvertToDatabaseConstraint(isEquivalent, level, DicomTagType_Identifier));
       lookup.AddConstraint(c2.ConvertToDatabaseConstraint(isEquivalent, level, DicomTagType_Identifier));
@@ -621,7 +621,7 @@
   FilesystemStorage storage(path);
   SQLiteDatabaseWrapper db;   // The SQLite DB is in memory
   db.Open();
-  ServerContext context(db, storage, true /* running unit tests */, 10);
+  ServerContext context(db, storage, true /* running unit tests */, 10, false /* readonly */);
   context.SetupJobsEngine(true, false);
 
   ServerIndex& index = context.GetIndex();
@@ -703,7 +703,7 @@
   FilesystemStorage storage(path);
   SQLiteDatabaseWrapper db;   // The SQLite DB is in memory
   db.Open();
-  ServerContext context(db, storage, true /* running unit tests */, 10);
+  ServerContext context(db, storage, true /* running unit tests */, 10, false /* readonly */);
   context.SetupJobsEngine(true, false);
   ServerIndex& index = context.GetIndex();
 
@@ -820,7 +820,7 @@
     MemoryStorageArea storage;
     SQLiteDatabaseWrapper db;   // The SQLite DB is in memory
     db.Open();
-    ServerContext context(db, storage, true /* running unit tests */, 10);
+    ServerContext context(db, storage, true /* running unit tests */, 10, false /* readonly */);
     context.SetupJobsEngine(true, false);
     context.SetCompressionEnabled(true);
 
@@ -985,7 +985,7 @@
     MemoryStorageArea storage;
     SQLiteDatabaseWrapper db;   // The SQLite DB is in memory
     db.Open();
-    ServerContext context(db, storage, true /* running unit tests */, 10);
+    ServerContext context(db, storage, true /* running unit tests */, 10, false /* readonly */);
     context.SetupJobsEngine(true, false);
     context.SetCompressionEnabled(compression);
 
--- a/OrthancServer/UnitTestsSources/ServerJobsTests.cpp	Wed Sep 04 10:54:00 2024 +0200
+++ b/OrthancServer/UnitTestsSources/ServerJobsTests.cpp	Wed Oct 09 11:01:11 2024 +0200
@@ -536,7 +536,7 @@
     OrthancJobsSerialization()
     {
       db_.Open();
-      context_.reset(new ServerContext(db_, storage_, true /* running unit tests */, 10));
+      context_.reset(new ServerContext(db_, storage_, true /* running unit tests */, 10, false /* readonly */));
       context_->SetupJobsEngine(true, false);
     }
 
--- a/TODO	Wed Sep 04 10:54:00 2024 +0200
+++ b/TODO	Wed Oct 09 11:01:11 2024 +0200
@@ -203,6 +203,8 @@
   We should first filter in SQL by StudyDate only, combine it with StudyTime into a single 
   DateTime string and filter again in C++.
   https://discourse.orthanc-server.org/t/performin-find-within-orthanc-for-time-frames/4704
+* Worklist plugin: support MPPS
+  https://github.com/orthanc-server/orthanc-setup-samples/blob/master/python-samples/worklist-with-mpps.py
 
 --------------------
 Internationalization
@@ -220,8 +222,6 @@
 Performance
 ===========
 
-* (3) ServerContext::DicomCacheLocker => give access to the raw buffer,
-  useful in ServerContext::DecodeDicomInstance()
 * (2) DicomMap: create a cache to the main DICOM tags index
 * (3) Check out rapidjson: https://github.com/miloyip/nativejson-benchmark
 * For C-Find results: we could store the computed tags