changeset 5759:868e3df40d5e find-refactoring

merged large-queries -> find-refactoring
author Alain Mazy <am@orthanc.team>
date Thu, 05 Sep 2024 18:53:33 +0200
parents fc591f166d53 (current diff) ca06dde85358 (diff)
children 0b44920843b5
files
diffstat 18 files changed, 415 insertions(+), 27 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Wed Sep 04 10:32:55 2024 +0200
+++ b/NEWS	Thu Sep 05 18:53:33 2024 +0200
@@ -10,6 +10,13 @@
 
 * 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).
+* 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
 
 Maintenance
 -----------
--- a/OrthancFramework/Resources/CodeGeneration/ErrorCodes.json	Wed Sep 04 10:32:55 2024 +0200
+++ b/OrthancFramework/Resources/CodeGeneration/ErrorCodes.json	Thu Sep 05 18:53:33 2024 +0200
@@ -337,7 +337,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/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp	Wed Sep 04 10:32:55 2024 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp	Thu Sep 05 18:53:33 2024 +0200
@@ -626,6 +626,33 @@
       }
     }
 
+    virtual void GetChangesExtended(std::list<ServerIndexChange>& target /*out*/,
+                                    bool& done /*out*/,
+                                    int64_t since,
+                                    int64_t to,
+                                    uint32_t limit,
+                                    ChangeType changeType) 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);
+      request.mutable_get_changes_extended()->set_change_type(changeType);
+      ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_CHANGES_2, request);
+
+      done = response.get_changes().done();
+
+      target.clear();
+      for (int i = 0; i < response.get_changes().changes().size(); i++)
+      {
+        target.push_back(Convert(response.get_changes().changes(i)));
+      }
+    }
+
     
     virtual void GetChildrenInternalId(std::list<int64_t>& target,
                                        int64_t id) ORTHANC_OVERRIDE
@@ -1647,9 +1674,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:32:55 2024 +0200
+++ b/OrthancServer/Plugins/Engine/PluginsEnumerations.cpp	Thu Sep 05 18:53:33 2024 +0200
@@ -122,6 +122,9 @@
         case ChangeType_UpdatedMetadata:
           return OrthancPluginChangeType_UpdatedMetadata;
 
+        case ChangeType_INTERNAL_All:
+          return _OrthancPluginChangeType_All;
+
         default:
           throw OrthancException(ErrorCode_ParameterOutOfRange);
       }
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Wed Sep 04 10:32:55 2024 +0200
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Thu Sep 05 18:53:33 2024 +0200
@@ -748,6 +748,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
@@ -772,6 +773,7 @@
     OrthancPluginChangeType_JobSuccess = 17,        /*!< A Job has completed successfully */
     OrthancPluginChangeType_JobFailure = 18,        /*!< A Job has failed */
 
+    _OrthancPluginChangeType_All = 65535,           /*!< All jobs (when used as a filter in GetChanges) */
     _OrthancPluginChangeType_INTERNAL = 0x7fffffff
   } OrthancPluginChangeType;
 
--- a/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto	Wed Sep 04 10:32:55 2024 +0200
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto	Thu Sep 05 18:53:33 2024 +0200
@@ -142,6 +142,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 +296,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_2 = 51;               // New in Orthanc 1.12.5
 }
 
 message Rollback {
@@ -415,6 +417,19 @@
   }
 }
 
+message GetChangesExtended {
+  message Request {
+    int64 since = 1;
+    int64 to = 2;
+    int32 change_type = 3;
+    uint32 limit = 4;
+  }
+  message Response {
+    repeated ServerIndexChange changes = 1;
+    bool done = 2;
+  }
+}
+
 message GetChildrenInternalId {
   message Request {
     int64 id = 1;
@@ -974,6 +989,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 +1044,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 {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/ImplementationNotes/DatabasesClassHierarchy.txt	Thu Sep 05 18:53:33 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/Sources/Database/BaseDatabaseWrapper.cpp	Wed Sep 04 10:32:55 2024 +0200
+++ b/OrthancServer/Sources/Database/BaseDatabaseWrapper.cpp	Thu Sep 05 18:53:33 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,
+                                                                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:32:55 2024 +0200
+++ b/OrthancServer/Sources/Database/BaseDatabaseWrapper.h	Thu Sep 05 18:53:33 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,
+                                      ChangeType filterType) ORTHANC_OVERRIDE;
     };
 
     virtual uint64_t MeasureLatency() ORTHANC_OVERRIDE;
--- a/OrthancServer/Sources/Database/IDatabaseWrapper.h	Wed Sep 04 10:32:55 2024 +0200
+++ b/OrthancServer/Sources/Database/IDatabaseWrapper.h	Thu Sep 05 18:53:33 2024 +0200
@@ -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;
@@ -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,
+                                      ChangeType filterType) = 0;
     };
 
 
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp	Wed Sep 04 10:32:55 2024 +0200
+++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp	Thu Sep 05 18:53:33 2024 +0200
@@ -235,11 +235,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 +253,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();
+        }
+      }
     }
 
 
@@ -860,10 +876,76 @@
                             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);
+      GetChangesExtended(target, done, since, -1, limit, ChangeType_INTERNAL_All);
+    }
+
+    virtual void GetChangesExtended(std::list<ServerIndexChange>& target /*out*/,
+                                    bool& done /*out*/,
+                                    int64_t since,
+                                    int64_t to,
+                                    uint32_t limit,
+                                    ChangeType filterType) ORTHANC_OVERRIDE
+    {
+      std::vector<std::string> filters;
+      bool hasSince = false;
+      bool hasTo = false;
+      bool hasFilterType = false;
+
+      if (since > 0)
+      {
+        hasSince = true;
+        filters.push_back("seq>?");
+      }
+      if (to != -1)
+      {
+        hasTo = true;
+        filters.push_back("seq<=?");
+      }
+      if (filterType != ChangeType_INTERNAL_All)
+      {
+        hasFilterType = true;
+        filters.push_back("changeType=?");
+      }
+
+      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);
+      }
+      if (hasFilterType)
+      {
+        s.BindInt(paramCounter++, filterType);
+      }
+      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 +1006,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 +1729,7 @@
     // TODO: implement revisions in SQLite
     dbCapabilities_.SetFlushToDisk(true);
     dbCapabilities_.SetLabelsSupport(true);
+    dbCapabilities_.SetHasExtendedChanges(true);
     db_.Open(path);
   }
 
@@ -1659,6 +1742,7 @@
     // TODO: implement revisions in SQLite
     dbCapabilities_.SetFlushToDisk(true);
     dbCapabilities_.SetLabelsSupport(true);
+    dbCapabilities_.SetHasExtendedChanges(true);
     db_.OpenInMemory();
   }
 
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h	Wed Sep 04 10:32:55 2024 +0200
+++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h	Thu Sep 05 18:53:33 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,
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Wed Sep 04 10:32:55 2024 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Thu Sep 05 18:53:33 2024 +0200
@@ -281,6 +281,10 @@
     }
     
     target["Last"] = static_cast<int>(last);
+    if (!log.empty())
+    {
+      target["First"] = static_cast<int>(log.front().GetSeq());
+    }
   }
 
 
@@ -1083,6 +1087,39 @@
   }
 
 
+  void StatelessDatabaseOperations::GetChangesExtended(Json::Value& target,
+                                                       int64_t since,
+                                                       int64_t to,                               
+                                                       unsigned int maxResults,
+                                                       ChangeType changeType)
+  {
+    class Operations : public ReadOnlyOperationsT5<Json::Value&, int64_t, int64_t, unsigned int, unsigned int>
+    {
+    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>(), static_cast<ChangeType>(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&>
@@ -3673,6 +3710,11 @@
     return db_.GetDatabaseCapabilities().HasLabelsSupport();
   }
 
+  bool StatelessDatabaseOperations::HasExtendedChanges()
+  {
+    boost::shared_lock<boost::shared_mutex> lock(mutex_);
+    return db_.GetDatabaseCapabilities().HasExtendedChanges();
+  }
 
   void StatelessDatabaseOperations::ExecuteFind(FindResponse& response,
                                                 const FindRequest& request)
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Wed Sep 04 10:32:55 2024 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Thu Sep 05 18:53:33 2024 +0200
@@ -259,6 +259,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,
+                              ChangeType filterType)
+      {
+        transaction_.GetChangesExtended(target, done, since, to, limit, filterType);
+      }
+
       void GetChildrenInternalId(std::list<int64_t>& target,
                                  int64_t id)
       {
@@ -659,8 +669,16 @@
                     int64_t since,
                     uint32_t limit);
 
+    void GetChangesExtended(Json::Value& target,
+                            int64_t since,
+                            int64_t to,
+                            uint32_t limit,
+                            ChangeType filterType);
+
     void GetLastChange(Json::Value& target);
 
+    bool HasExtendedChanges();
+
     void GetExportedResources(Json::Value& target,
                               int64_t since,
                               uint32_t limit);
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestChanges.cpp	Wed Sep 04 10:32:55 2024 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestChanges.cpp	Thu Sep 05 18:53:33 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,59 @@
         .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;
+    ChangeType filterType = ChangeType_INTERNAL_All;
+
     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")
+    {
+      filterType = StringToChangeType(filterArgument);
+    }
 
     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 != ChangeType_INTERNAL_All)
+      {
+        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 +168,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/OrthancRestSystem.cpp	Wed Sep 04 10:32:55 2024 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp	Thu Sep 05 18:53:33 2024 +0200
@@ -92,6 +92,8 @@
     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";
 
     if (call.IsDocumentation())
     {
@@ -138,6 +140,8 @@
                         "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' (new in Orthanc 1.12.5) ")
         .SetHttpGetSample("https://orthanc.uclouvain.be/demo/system", true);
       return;
     }
@@ -196,6 +200,8 @@
     GetUserMetadataConfiguration(result[USER_METADATA]);
 
     result[HAS_LABELS] = OrthancRestApi::GetIndex(call).HasLabelsSupport();
+    result[CAPABILITIES] = Json::objectValue;
+    result[CAPABILITIES][HAS_EXTENDED_CHANGES] = OrthancRestApi::GetIndex(call).HasExtendedChanges();
     
     call.GetOutput().AnswerJson(result);
   }
--- a/OrthancServer/Sources/ServerEnumerations.cpp	Wed Sep 04 10:32:55 2024 +0200
+++ b/OrthancServer/Sources/ServerEnumerations.cpp	Thu Sep 05 18:53:33 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)
   {
--- a/OrthancServer/Sources/ServerEnumerations.h	Wed Sep 04 10:32:55 2024 +0200
+++ b/OrthancServer/Sources/ServerEnumerations.h	Thu Sep 05 18:53:33 2024 +0200
@@ -191,7 +191,9 @@
 
     // The changes below this point are not logged into the database
     ChangeType_Deleted = 4096,
-    ChangeType_NewChildInstance = 4097
+    ChangeType_NewChildInstance = 4097,
+
+    ChangeType_INTERNAL_All = 65535 // used to filter changes
   };
 
   enum BuiltinDecoderTranscoderOrder
@@ -251,6 +253,8 @@
   const char* EnumerationToString(StoreStatus status);
 
   const char* EnumerationToString(ChangeType type);
+  
+  ChangeType StringToChangeType(const std::string& value);
 
   const char* EnumerationToString(Verbosity verbosity);