changeset 771:5019e732058d

merged pg-next-699 -> default
author Alain Mazy <am@orthanc.team>
date Fri, 28 Nov 2025 15:38:54 +0100
parents da9d68c3bf6b (current diff) bd6fcb94d68f (diff)
children 36f4a9992d11
files
diffstat 23 files changed, 584 insertions(+), 192 deletions(-) [+]
line wrap: on
line diff
--- a/Framework/Plugins/DatabaseBackendAdapterV4.cpp	Wed Nov 26 14:20:16 2025 +0100
+++ b/Framework/Plugins/DatabaseBackendAdapterV4.cpp	Fri Nov 28 15:38:54 2025 +0100
@@ -464,6 +464,10 @@
         response.mutable_get_system_information()->set_supports_queues(accessor.GetBackend().HasQueues());
 #endif
 
+#if ORTHANC_PLUGINS_HAS_RESERVE_QUEUE_VALUE == 1
+        response.mutable_get_system_information()->set_supports_reserve_queue_value(accessor.GetBackend().HasReserveQueueValue());
+#endif
+
 #if ORTHANC_PLUGINS_HAS_KEY_VALUE_STORES == 1
         response.mutable_get_system_information()->set_supports_key_value_stores(accessor.GetBackend().HasKeyValueStores());
 #endif
@@ -1391,6 +1395,37 @@
 
 #endif
 
+#if ORTHANC_PLUGINS_HAS_RESERVE_QUEUE_VALUE == 1
+      case Orthanc::DatabasePluginMessages::OPERATION_RESERVE_QUEUE_VALUE:
+      {
+        std::string value;
+        uint64_t valueId;
+        bool found = backend.ReserveQueueValue(value, valueId, manager,
+                                               request.reserve_queue_value().queue_id(),
+                                               request.reserve_queue_value().origin() == Orthanc::DatabasePluginMessages::QUEUE_ORIGIN_FRONT,
+                                               request.reserve_queue_value().release_timeout());
+        response.mutable_reserve_queue_value()->set_found(found);
+        
+        if (found)
+        {
+          response.mutable_reserve_queue_value()->set_value(value);
+          response.mutable_reserve_queue_value()->set_value_id(valueId);
+        }
+
+        break;
+      }
+
+      case Orthanc::DatabasePluginMessages::OPERATION_ACKNOWLEDGE_QUEUE_VALUE:
+      {
+        backend.AcknowledgeQueueValue(manager,
+                                      request.acknowledge_queue_value().queue_id(),
+                                      request.acknowledge_queue_value().value_id());
+
+        break;
+      }
+
+#endif
+
 #if ORTHANC_PLUGINS_HAS_ATTACHMENTS_CUSTOM_DATA == 1
       case Orthanc::DatabasePluginMessages::OPERATION_GET_ATTACHMENT_CUSTOM_DATA:
       {
--- a/Framework/Plugins/IDatabaseBackend.h	Wed Nov 26 14:20:16 2025 +0100
+++ b/Framework/Plugins/IDatabaseBackend.h	Fri Nov 28 15:38:54 2025 +0100
@@ -139,6 +139,8 @@
 
     virtual bool HasQueues() const = 0;
 
+    virtual bool HasReserveQueueValue() const = 0;
+
     virtual bool HasAuditLogs() const = 0;
 
     virtual void AddAttachment(DatabaseManager& manager,
@@ -525,6 +527,19 @@
                                   const std::string& queueId) = 0;
 #endif
 
+#if ORTHANC_PLUGINS_HAS_RESERVE_QUEUE_VALUE == 1
+    virtual bool ReserveQueueValue(std::string& value,
+                                   uint64_t& valueId,
+                                   DatabaseManager& manager,
+                                   const std::string& queueId,
+                                   bool fromFront,
+                                   uint32_t reserveTimeout) = 0;
+
+    virtual void AcknowledgeQueueValue(DatabaseManager& manager,
+                                       const std::string& queueId,
+                                       uint64_t valueId) = 0;
+#endif
+
 #if ORTHANC_PLUGINS_HAS_ATTACHMENTS_CUSTOM_DATA == 1
     virtual void GetAttachmentCustomData(std::string& customData,
                                          DatabaseManager& manager,
--- a/Framework/Plugins/ISqlLookupFormatter.h	Wed Nov 26 14:20:16 2025 +0100
+++ b/Framework/Plugins/ISqlLookupFormatter.h	Fri Nov 28 15:38:54 2025 +0100
@@ -30,6 +30,7 @@
 
 #pragma once
 
+#include "../Common/Dictionary.h"
 #include "MessagesToolbox.h"
 
 #include <boost/noncopyable.hpp>
@@ -79,6 +80,8 @@
 
     virtual std::string FormatFloatCast() const = 0;
 
+    virtual const Dictionary& GetDictionary() const = 0;
+
     static void GetLookupLevels(Orthanc::ResourceType& lowerLevel,
                                 Orthanc::ResourceType& upperLevel,
                                 const Orthanc::ResourceType& queryLevel,
--- a/Framework/Plugins/IndexBackend.cpp	Wed Nov 26 14:20:16 2025 +0100
+++ b/Framework/Plugins/IndexBackend.cpp	Fri Nov 28 15:38:54 2025 +0100
@@ -39,6 +39,15 @@
 
 namespace OrthancDatabases
 {
+  static int64_t GetSecondsSinceEpoch()
+  {
+    // https://www.boost.org/doc/libs/1_69_0/doc/html/date_time/examples.html#date_time.examples.seconds_since_epoch
+    static const boost::posix_time::ptime EPOCH(boost::gregorian::date(1970, 1, 1));
+    const boost::posix_time::ptime now = boost::posix_time::microsec_clock::universal_time();
+    return (now - EPOCH).total_seconds();
+  }
+
+
   static std::string ConvertWildcardToLike(const std::string& query)
   {
     std::string s = query;
@@ -4505,30 +4514,38 @@
     }
 
 
-    bool IndexBackend::DequeueValueSQLite(std::string& value,
-                                          DatabaseManager& manager,
-                                          const std::string& queueId,
-                                          bool fromFront)
+    bool IndexBackend::DequeueValue(std::string& value,
+                                    DatabaseManager& manager,
+                                    const std::string& queueId,
+                                    bool fromFront)
     {
-      assert(manager.GetDialect() == Dialect_SQLite);
-
       LookupFormatter formatter(manager.GetDialect());
 
       std::unique_ptr<DatabaseManager::CachedStatement> statement;
 
       std::string queueIdParameter = formatter.GenerateParameter(queueId);
-
-      if (fromFront)
+      std::string nowParameter = formatter.GenerateParameter(GetSecondsSinceEpoch());
+
+      switch (manager.GetDialect())
       {
-        statement.reset(new DatabaseManager::CachedStatement(
-                          STATEMENT_FROM_HERE, manager,
-                          "SELECT id, value FROM Queues WHERE queueId=" + queueIdParameter + " ORDER BY id ASC LIMIT 1"));
-      }
-      else
-      {
-        statement.reset(new DatabaseManager::CachedStatement(
-                          STATEMENT_FROM_HERE, manager,
-                          "SELECT id, value FROM Queues WHERE queueId=" + queueIdParameter + " ORDER BY id DESC LIMIT 1"));
+        case Dialect_PostgreSQL:
+          if (fromFront)
+          {
+            statement.reset(new DatabaseManager::CachedStatement(
+                              STATEMENT_FROM_HERE, manager,
+                              "WITH poppedRows AS (DELETE FROM Queues WHERE id = (SELECT MIN(id) FROM Queues WHERE queueId=" + queueIdParameter + " AND (reservedUntil IS NULL OR reservedUntil <= " + nowParameter + ")) RETURNING value) "
+                              "SELECT value FROM poppedRows"));
+          }
+          else
+          {
+            statement.reset(new DatabaseManager::CachedStatement(
+                              STATEMENT_FROM_HERE, manager,
+                              "WITH poppedRows AS (DELETE FROM Queues WHERE id = (SELECT MAX(id) FROM Queues WHERE queueId=" + queueIdParameter + " AND (reservedUntil IS NULL OR reservedUntil <= " + nowParameter + ")) RETURNING value) "
+                              "SELECT value FROM poppedRows"));
+          }
+          break;
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
       }
 
       statement->Execute(formatter.GetDictionary());
@@ -4539,83 +4556,12 @@
       }
       else
       {
-        statement->SetResultFieldType(0, ValueType_Integer64);
-        statement->SetResultFieldType(1, ValueType_BinaryString);
-
-        value = statement->ReadString(1);
-
-        {
-          DatabaseManager::CachedStatement s2(STATEMENT_FROM_HERE, manager,
-                                              "DELETE FROM Queues WHERE id=${id}");
-
-          s2.SetParameterType("id", ValueType_Integer64);
-
-          Dictionary args;
-          args.SetIntegerValue("id", statement->ReadInteger64(0));
-
-          s2.Execute(args);
-        }
-
+        statement->SetResultFieldType(0, ValueType_BinaryString);
+        value = statement->ReadString(0);
         return true;
       }
     }
 
-
-    bool IndexBackend::DequeueValue(std::string& value,
-                                    DatabaseManager& manager,
-                                    const std::string& queueId,
-                                    bool fromFront)
-    {
-      if (manager.GetDialect() == Dialect_SQLite)
-      {
-        return DequeueValueSQLite(value, manager, queueId, fromFront);
-      }
-      else
-      {
-        LookupFormatter formatter(manager.GetDialect());
-
-        std::unique_ptr<DatabaseManager::CachedStatement> statement;
-
-        std::string queueIdParameter = formatter.GenerateParameter(queueId);
-
-        switch (manager.GetDialect())
-        {
-          case Dialect_PostgreSQL:
-            if (fromFront)
-            {
-              statement.reset(new DatabaseManager::CachedStatement(
-                                STATEMENT_FROM_HERE, manager,
-                                "WITH poppedRows AS (DELETE FROM Queues WHERE id = (SELECT MIN(id) FROM Queues WHERE queueId=" + queueIdParameter + ") RETURNING value) "
-                                "SELECT value FROM poppedRows"));
-            }
-            else
-            {
-              statement.reset(new DatabaseManager::CachedStatement(
-                                STATEMENT_FROM_HERE, manager,
-                                "WITH poppedRows AS (DELETE FROM Queues WHERE id = (SELECT MAX(id) FROM Queues WHERE queueId=" + queueIdParameter + ") RETURNING value) "
-                                "SELECT value FROM poppedRows"));
-            }
-            break;
-
-          default:
-            throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
-        }
-
-        statement->Execute(formatter.GetDictionary());
-
-        if (statement->IsDone())
-        {
-          return false;
-        }
-        else
-        {
-          statement->SetResultFieldType(0, ValueType_BinaryString);
-          value = statement->ReadString(0);
-          return true;
-        }
-      }
-    }
-
     uint64_t IndexBackend::GetQueueSize(DatabaseManager& manager,
                                         const std::string& queueId)
     {
@@ -4636,6 +4582,87 @@
     }
 #endif
 
+#if ORTHANC_PLUGINS_HAS_RESERVE_QUEUE_VALUE == 1
+    bool IndexBackend::ReserveQueueValue(std::string& value,
+                                         uint64_t& valueId,
+                                         DatabaseManager& manager,
+                                         const std::string& queueId,
+                                         bool fromFront,
+                                         uint32_t reserveTimeout)
+    {
+      LookupFormatter formatter(manager.GetDialect());
+
+      std::string queueIdParameter = formatter.GenerateParameter(queueId);
+      std::string reserveTimeoutParameter = formatter.GenerateParameter(reserveTimeout);
+      std::string nowParameter = formatter.GenerateParameter(GetSecondsSinceEpoch());
+
+      std::string minMax = (fromFront ? "MIN" : "MAX");
+      std::string sql;
+
+      switch (manager.GetDialect())
+      {
+        case Dialect_PostgreSQL:
+          sql = "WITH RowToUpdate AS (SELECT " + minMax + "(id) FROM Queues WHERE queueId=" + queueIdParameter + " AND (reservedUntil IS NULL OR reservedUntil <= " + nowParameter + ")) "
+                "  UPDATE Queues SET reservedUntil = " + nowParameter + " + " + reserveTimeoutParameter + " WHERE id IN (SELECT * FROM RowToUpdate) "
+                "  RETURNING id, value;";
+          break;
+
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+      }
+
+      DatabaseManager::CachedStatement statement(STATEMENT_FROM_HERE_DYNAMIC(sql), manager, sql);
+      statement.Execute(formatter.GetDictionary());
+
+      if (statement.IsDone())
+      {
+        return false;
+      }
+      else
+      {
+        statement.SetResultFieldType(0, ValueType_Integer64);
+        valueId = statement.ReadInteger64(0);
+
+        statement.SetResultFieldType(1, ValueType_BinaryString);
+        value = statement.ReadString(1);
+        return true;
+      }
+    }
+
+    void IndexBackend::AcknowledgeQueueValue(DatabaseManager& manager,
+                                             const std::string& queueId,
+                                             uint64_t valueId)
+    {
+      LookupFormatter formatter(manager.GetDialect());
+
+      std::unique_ptr<DatabaseManager::CachedStatement> statement;
+
+      std::string queueIdParameter = formatter.GenerateParameter(queueId);
+      std::string valueIdParameter = formatter.GenerateParameter(valueId);
+      int64_t now = GetSecondsSinceEpoch();
+      std::string nowParameter = formatter.GenerateParameter(now);
+
+      switch (manager.GetDialect())
+      {
+        case Dialect_PostgreSQL:
+          statement.reset(new DatabaseManager::CachedStatement(
+                            STATEMENT_FROM_HERE, manager,
+                            "DELETE FROM Queues WHERE queueId=" + queueIdParameter + " AND id=" + valueIdParameter +
+                            " AND reservedUntil IS NOT NULL AND " + nowParameter + " < reservedUntil RETURNING id"));
+          break;
+
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+      }
+
+      statement->Execute(formatter.GetDictionary());
+      if (statement->IsDone())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource, "Unable to acknowledge a queue value. Has it expired ?");
+      }
+    }
+#endif
+
 #if ORTHANC_PLUGINS_HAS_ATTACHMENTS_CUSTOM_DATA == 1
     void IndexBackend::GetAttachmentCustomData(std::string& customData,
                                                DatabaseManager& manager,
@@ -4815,4 +4842,8 @@
     }
 #endif
 
+  ISqlLookupFormatter* IndexBackend::CreateLookupFormatter(Dialect dialect)
+  {
+    return new LookupFormatter(dialect);
+  }
 }
--- a/Framework/Plugins/IndexBackend.h	Wed Nov 26 14:20:16 2025 +0100
+++ b/Framework/Plugins/IndexBackend.h	Fri Nov 28 15:38:54 2025 +0100
@@ -39,7 +39,9 @@
   class IndexBackend : public IDatabaseBackend
   {
   private:
+#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
     class LookupFormatter;
+#endif
 
     OrthancPluginContext*  context_;
     bool                   readOnly_;
@@ -49,7 +51,6 @@
     std::unique_ptr<IDatabaseBackendOutput::IFactory>  outputFactory_;
     
   protected:
-
     virtual void ClearDeletedFiles(DatabaseManager& manager);
 
     virtual void ClearDeletedResources(DatabaseManager& manager);
@@ -84,13 +85,6 @@
                                        const Dictionary& args,
                                        uint32_t limit);
 
-#if ORTHANC_PLUGINS_HAS_QUEUES == 1
-    bool DequeueValueSQLite(std::string& value,
-                            DatabaseManager& manager,
-                            const std::string& queueId,
-                            bool fromFront);
-#endif
-
   public:
     explicit IndexBackend(OrthancPluginContext* context,
                           bool readOnly,
@@ -510,6 +504,19 @@
 
 #endif
 
+#if ORTHANC_PLUGINS_HAS_RESERVE_QUEUE_VALUE == 1
+    virtual bool ReserveQueueValue(std::string& value,
+                                   uint64_t& valueId,
+                                   DatabaseManager& manager,
+                                   const std::string& queueId,
+                                   bool fromFront,
+                                   uint32_t reserveTimeout) ORTHANC_OVERRIDE;
+
+    virtual void AcknowledgeQueueValue(DatabaseManager& manager,
+                                       const std::string& queueId,
+                                       uint64_t valueId) ORTHANC_OVERRIDE;
+#endif
+
 #if ORTHANC_PLUGINS_HAS_ATTACHMENTS_CUSTOM_DATA == 1
     virtual void GetAttachmentCustomData(std::string& customData,
                                          DatabaseManager& manager,
@@ -518,7 +525,6 @@
     virtual void SetAttachmentCustomData(DatabaseManager& manager,
                                          const std::string& attachmentUuid,
                                          const std::string& customData) ORTHANC_OVERRIDE;
-
 #endif
 
 #if ORTHANC_PLUGINS_HAS_AUDIT_LOGS == 1
@@ -569,5 +575,9 @@
     static DatabaseManager* CreateSingleDatabaseManager(IDatabaseBackend& backend,
                                                         bool hasIdentifierTags,
                                                         const std::list<IdentifierTag>& identifierTags);
+
+#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
+    ISqlLookupFormatter* CreateLookupFormatter(Dialect dialect);
+#endif
   };
 }
--- a/Framework/Plugins/IndexUnitTests.h	Wed Nov 26 14:20:16 2025 +0100
+++ b/Framework/Plugins/IndexUnitTests.h	Fri Nov 28 15:38:54 2025 +0100
@@ -28,6 +28,7 @@
 #include "GlobalProperties.h"
 
 #include <Compatibility.h>  // For std::unique_ptr<>
+#include <SystemToolbox.h>
 
 #include <gtest/gtest.h>
 #include <list>
@@ -1065,5 +1066,69 @@
   }
 #endif
 
+#if ORTHANC_PLUGINS_HAS_RESERVE_QUEUE_VALUE == 1
+  {
+    std::string value;
+    uint64_t valueIdA, valueIdB, valueIdC, valueIdD, valueIdE, valueIdFail;
+
+    {
+      manager->StartTransaction(TransactionType_ReadWrite);
+
+      db.EnqueueValue(*manager, "test", "a");
+      db.EnqueueValue(*manager, "test", "b");
+      db.EnqueueValue(*manager, "test", "c");
+      db.EnqueueValue(*manager, "test", "d");
+      db.EnqueueValue(*manager, "test", "e");
+
+      ASSERT_EQ(5u, db.GetQueueSize(*manager, "test"));
+
+      ASSERT_TRUE(db.ReserveQueueValue(value, valueIdA, *manager, "test", true, 1000));
+      ASSERT_EQ("a", value);
+      ASSERT_TRUE(db.ReserveQueueValue(value, valueIdB, *manager, "test", true, 1));
+      ASSERT_EQ("b", value);
+      ASSERT_TRUE(db.ReserveQueueValue(value, valueIdE, *manager, "test", false, 1));
+      ASSERT_EQ("e", value);
+      ASSERT_TRUE(db.ReserveQueueValue(value, valueIdD, *manager, "test", false, 1));
+      ASSERT_EQ("d", value);
+      manager->CommitTransaction(); 
+    }
+
+    {
+      manager->StartTransaction(TransactionType_ReadWrite);
+
+      db.AcknowledgeQueueValue(*manager, "test", valueIdA);
+      db.AcknowledgeQueueValue(*manager, "test", valueIdE);
+      manager->CommitTransaction(); 
+    }
+
+    Orthanc::SystemToolbox::USleep(2000000);  // Wait 2 seconds -> b and d should be released
+
+    {
+      manager->StartTransaction(TransactionType_ReadWrite);
+      ASSERT_TRUE(db.ReserveQueueValue(value, valueIdB, *manager, "test", true, 1));
+      ASSERT_EQ("b", value);
+      ASSERT_TRUE(db.ReserveQueueValue(value, valueIdD, *manager, "test", false, 1));
+      ASSERT_EQ("d", value);
+      ASSERT_TRUE(db.ReserveQueueValue(value, valueIdC, *manager, "test", false, 1));
+      ASSERT_EQ("c", value);
+      ASSERT_FALSE(db.ReserveQueueValue(value, valueIdFail, *manager, "test", false, 1));
+
+      manager->CommitTransaction();
+    }
+
+    Orthanc::SystemToolbox::USleep(2000000);  // Wait 2 seconds -> b, c and d should be released
+
+    // try to acknowledge a value after it has expired
+    {
+      manager->StartTransaction(TransactionType_ReadWrite);
+
+      ASSERT_THROW(db.AcknowledgeQueueValue(*manager, "test", valueIdC), Orthanc::OrthancException);
+
+      manager->CommitTransaction();
+    }
+
+  }
+#endif
+
   manager->Close();
 }
--- a/Framework/Plugins/MessagesToolbox.h	Wed Nov 26 14:20:16 2025 +0100
+++ b/Framework/Plugins/MessagesToolbox.h	Fri Nov 28 15:38:54 2025 +0100
@@ -54,6 +54,7 @@
 #define ORTHANC_PLUGINS_HAS_ATTACHMENTS_CUSTOM_DATA 0
 #define ORTHANC_PLUGINS_HAS_KEY_VALUE_STORES 0
 #define ORTHANC_PLUGINS_HAS_QUEUES 0
+#define ORTHANC_PLUGINS_HAS_RESERVE_QUEUE_VALUE 0
 #define ORTHANC_PLUGINS_HAS_AUDIT_LOGS 0
 
 #if defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE)
@@ -75,6 +76,13 @@
 #  endif
 #endif
 
+#if defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE)
+#  if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 10)
+#    undef  ORTHANC_PLUGINS_HAS_RESERVE_QUEUE_VALUE
+#    define ORTHANC_PLUGINS_HAS_RESERVE_QUEUE_VALUE 1
+#  endif
+#endif
+
 
 #include <Enumerations.h>
 
--- a/Framework/PostgreSQL/PostgreSQLIncludes.h	Wed Nov 26 14:20:16 2025 +0100
+++ b/Framework/PostgreSQL/PostgreSQLIncludes.h	Fri Nov 28 15:38:54 2025 +0100
@@ -44,12 +44,19 @@
 #  error PG_VERSION_NUM is not defined
 #endif
 
-#if PG_VERSION_NUM >= 110000
-#  include <catalog/pg_type_d.h>
+
+
+#if PG_VERSION_NUM < 180000
+#  if PG_VERSION_NUM >= 110000
+#    include <catalog/pg_type_d.h>
+#  else
+#    include <postgres.h>
+#    undef LOG  // This one comes from <postgres.h>, and conflicts with <Core/Logging.h>
+#    include <catalog/pg_type.h>
+#  endif
 #else
-#  include <postgres.h>
-#  undef LOG  // This one comes from <postgres.h>, and conflicts with <Core/Logging.h>
-#  include <catalog/pg_type.h>
+// from libpq 18, we avoid using server headers to simplify the "configure steps"
+#  include "PostgreSQLOids.h"
 #endif
 
 #include <libpq-fe.h>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/PostgreSQL/PostgreSQLOids.h	Fri Nov 28 15:38:54 2025 +0100
@@ -0,0 +1,47 @@
+/**
+ * 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-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#if PG_VERSION_NUM < 180000
+#  error This file shall not be included if you are linking against libpq < 18
+#endif
+// Object ID type in PostgreSQL
+typedef unsigned int Oid;
+
+// Core built-in type OIDs.  
+// All these OIDs are guaranteed not to change.
+// By defining them here, we avoid including server only headers
+#define BOOLOID        16
+#define BYTEAOID       17
+#define CHAROID        18
+#define NAMEOID        19
+#define INT8OID        20
+#define INT2OID        21
+#define INT4OID        23
+#define TEXTOID        25
+#define OIDOID         26
+#define VARCHAROID     1043
+#define TIMESTAMPOID   1114
+#define TIMESTAMPTZOID 1184
+#define VOIDOID        2278
\ No newline at end of file
--- a/MySQL/Plugins/MySQLIndex.h	Wed Nov 26 14:20:16 2025 +0100
+++ b/MySQL/Plugins/MySQLIndex.h	Fri Nov 28 15:38:54 2025 +0100
@@ -76,6 +76,11 @@
       return false;
     }
 
+    virtual bool HasReserveQueueValue() const ORTHANC_OVERRIDE
+    {
+      return false;
+    }
+
     virtual bool HasAuditLogs() const ORTHANC_OVERRIDE
     {
       return false;
--- a/Odbc/Plugins/OdbcIndex.h	Wed Nov 26 14:20:16 2025 +0100
+++ b/Odbc/Plugins/OdbcIndex.h	Fri Nov 28 15:38:54 2025 +0100
@@ -88,6 +88,11 @@
       return false;
     }
 
+    virtual bool HasReserveQueueValue() const ORTHANC_OVERRIDE
+    {
+      return false;
+    }
+
     virtual bool HasAuditLogs() const ORTHANC_OVERRIDE
     {
       return false;
--- a/PostgreSQL/CMakeLists.txt	Wed Nov 26 14:20:16 2025 +0100
+++ b/PostgreSQL/CMakeLists.txt	Fri Nov 28 15:38:54 2025 +0100
@@ -95,7 +95,8 @@
   POSTGRESQL_UPGRADE_REV2_TO_REV3    ${CMAKE_SOURCE_DIR}/Plugins/SQL/Upgrades/Rev2ToRev3.sql
   POSTGRESQL_UPGRADE_REV3_TO_REV4    ${CMAKE_SOURCE_DIR}/Plugins/SQL/Upgrades/Rev3ToRev4.sql
   POSTGRESQL_UPGRADE_REV4_TO_REV5    ${CMAKE_SOURCE_DIR}/Plugins/SQL/Upgrades/Rev4ToRev5.sql
-  POSTGRESQL_UPGRADE_REV5_TO_REV6  ${CMAKE_SOURCE_DIR}/Plugins/SQL/Upgrades/Rev5ToRev6.sql
+  POSTGRESQL_UPGRADE_REV5_TO_REV6    ${CMAKE_SOURCE_DIR}/Plugins/SQL/Upgrades/Rev5ToRev6.sql
+  POSTGRESQL_UPGRADE_REV6_TO_REV10  ${CMAKE_SOURCE_DIR}/Plugins/SQL/Upgrades/Rev6ToRev10.sql
   )
 
 
--- a/PostgreSQL/NEWS	Wed Nov 26 14:20:16 2025 +0100
+++ b/PostgreSQL/NEWS	Fri Nov 28 15:38:54 2025 +0100
@@ -1,15 +1,17 @@
 Pending changes in the mainline
 ===============================
 
-DB schema revision: 6
+DB schema revision: 10
 Minimum plugin SDK (for build): 1.12.5
-Optimal plugin SDK (for build): 1.12.9
+Optimal plugin SDK (for build): 1.12.10   (TODO: update once released !)
 Minimum Orthanc runtime: 1.12.5
-Optimal Orthanc runtime: 1.12.9
+Optimal Orthanc runtime: 1.12.10
 
 Minimal Postgresql Server version: 9
 Optimal Postgresql Server version: 11+
 
+TODO before release: update SDK to 1.12.10
+
 Changes:
 * New configuration "Schema" (default value: 'public') to allow Orthanc
   to use another schema.  Note that, if you are not using the default 'public'
@@ -23,11 +25,14 @@
 * New configuration "ApplicationName" (default value is empty) that is copied in 
   the application_name argument in the connection string.  This name is used to
   identify the origin of queries in statistics and logs in the PostgreSQL server.
-
+* SDK: Added support for ReserveQueueValue and AcknowledgeQueueValue (new in SDK 1.12.10)
 
 Maintenance:
-* Now verifying the DatabasePatchLevel (revision) in another transaction than
-  the one that upgrades the schema.
+* Added a new primary key column in the InvalidChildCounts and GlobalIntegersChanges
+  tables.  This new column is required for pg_repack to be able to reclaim space on
+  these tables.
+* Upgraded dependencies for static builds (notably on Windows and LSB):
+  - libpq 18.1 (replacing libpq 13.1 - except for macOS universal binaries that still uses 13.1)
 
 
 Release 9.0 (2025-08-13)
--- a/PostgreSQL/Plugins/PostgreSQLIndex.cpp	Wed Nov 26 14:20:16 2025 +0100
+++ b/PostgreSQL/Plugins/PostgreSQLIndex.cpp	Fri Nov 28 15:38:54 2025 +0100
@@ -49,7 +49,7 @@
   static const GlobalProperty GlobalProperty_HasComputeStatisticsReadOnly = GlobalProperty_DatabaseInternal4;
 }
 
-#define CURRENT_DB_REVISION 6
+#define CURRENT_DB_REVISION 10
 
 namespace OrthancDatabases
 {
@@ -268,6 +268,19 @@
             currentRevision = 6;
           }
 
+          if (currentRevision == 6)
+          {
+            LOG(WARNING) << "Upgrading DB schema from revision 6 to revision 10 (there are no versions 7, 8 and 9 !)";
+
+            std::string query;
+
+            Orthanc::EmbeddedResources::GetFileResource
+              (query, Orthanc::EmbeddedResources::POSTGRESQL_UPGRADE_REV6_TO_REV10);
+            t.GetDatabaseTransaction().ExecuteMultiLines(query);
+            hasAppliedAnUpgrade = true;
+            currentRevision = 10;
+          }
+
           if (hasAppliedAnUpgrade)
           {
             LOG(WARNING) << "Upgrading DB schema by applying PrepareIndex.sql";
--- a/PostgreSQL/Plugins/PostgreSQLIndex.h	Wed Nov 26 14:20:16 2025 +0100
+++ b/PostgreSQL/Plugins/PostgreSQLIndex.h	Fri Nov 28 15:38:54 2025 +0100
@@ -87,6 +87,11 @@
       return true;
     }
 
+    virtual bool HasReserveQueueValue() const ORTHANC_OVERRIDE
+    {
+      return true;
+    }
+
     virtual bool HasAuditLogs() const ORTHANC_OVERRIDE
     {
       return true;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/PostgreSQL/Plugins/SQL/Downgrades/Rev699ToRev6.sql	Fri Nov 28 15:38:54 2025 +0100
@@ -0,0 +1,8 @@
+ALTER TABLE InvalidChildCounts DROP COLUMN pk;
+ALTER TABLE GlobalIntegersChanges DROP COLUMN pk;
+----------
+
+-- set the global properties that actually documents the DB version, revision and some of the capabilities
+-- modify only the ones that have changed
+DELETE FROM GlobalProperties WHERE property IN (4);
+INSERT INTO GlobalProperties VALUES (4, 6); -- GlobalProperty_DatabasePatchLevel
--- a/PostgreSQL/Plugins/SQL/PrepareIndex.sql	Wed Nov 26 14:20:16 2025 +0100
+++ b/PostgreSQL/Plugins/SQL/PrepareIndex.sql	Fri Nov 28 15:38:54 2025 +0100
@@ -400,6 +400,7 @@
 -- These changes will be applied at regular interval by an external thread or when someone
 -- requests the statistics
 CREATE TABLE IF NOT EXISTS GlobalIntegersChanges(
+    pk BIGSERIAL PRIMARY KEY,   -- new in rev10 required for pg_repack to be able to reclaim space
     key INTEGER,
     value BIGINT);
 
@@ -458,7 +459,7 @@
 CREATE OR REPLACE FUNCTION IncrementResourcesTrackerFunc()
 RETURNS TRIGGER AS $$
 BEGIN
-  INSERT INTO GlobalIntegersChanges VALUES(new.resourceType + 2, 1);
+  INSERT INTO GlobalIntegersChanges (key, value) VALUES(new.resourceType + 2, 1);
   RETURN NULL;
 END;
 $$ LANGUAGE plpgsql;
@@ -466,7 +467,7 @@
 CREATE OR REPLACE FUNCTION DecrementResourcesTrackerFunc()
 RETURNS TRIGGER AS $$
 BEGIN
-  INSERT INTO GlobalIntegersChanges VALUES(old.resourceType + 2, -1);
+  INSERT INTO GlobalIntegersChanges (key, value) VALUES(old.resourceType + 2, -1);
   RETURN NULL;
 END;
 $$ LANGUAGE plpgsql;
@@ -475,8 +476,8 @@
 CREATE OR REPLACE FUNCTION AttachedFileIncrementSizeFunc()
 RETURNS TRIGGER AS $body$
 BEGIN
-  INSERT INTO GlobalIntegersChanges VALUES(0, new.compressedSize);
-  INSERT INTO GlobalIntegersChanges VALUES(1, new.uncompressedSize);
+  INSERT INTO GlobalIntegersChanges (key, value) VALUES(0, new.compressedSize);
+  INSERT INTO GlobalIntegersChanges (key, value) VALUES(1, new.uncompressedSize);
   RETURN NULL;
 END;
 $body$ LANGUAGE plpgsql;
@@ -484,8 +485,8 @@
 CREATE OR REPLACE FUNCTION AttachedFileDecrementSizeFunc() 
 RETURNS TRIGGER AS $body$
 BEGIN
-  INSERT INTO GlobalIntegersChanges VALUES(0, -old.compressedSize);
-  INSERT INTO GlobalIntegersChanges VALUES(1, -old.uncompressedSize);
+  INSERT INTO GlobalIntegersChanges (key, value) VALUES(0, -old.compressedSize);
+  INSERT INTO GlobalIntegersChanges (key, value) VALUES(1, -old.uncompressedSize);
   RETURN NULL;
 END;
 $body$ LANGUAGE plpgsql;
@@ -704,6 +705,7 @@
 -- At regular interval, the DB housekeeping thread updates the childCount column of
 -- resources with an entry in this table.
 CREATE TABLE IF NOT EXISTS InvalidChildCounts(
+    pk BIGSERIAL PRIMARY KEY,   -- new in rev10 required for pg_repack to be able to reclaim space
     id BIGINT REFERENCES Resources(internalId) ON DELETE CASCADE,
     updatedAt TIMESTAMP DEFAULT NOW());
 
@@ -756,7 +758,7 @@
     IF TG_OP = 'INSERT' THEN
 		IF new.parentId IS NOT NULL THEN
             -- mark the parent's childCount as invalid
-			INSERT INTO InvalidChildCounts VALUES(new.parentId);
+			INSERT INTO InvalidChildCounts (id) VALUES(new.parentId);
         END IF;
 	
     ELSIF TG_OP = 'DELETE' THEN
@@ -764,7 +766,7 @@
 		IF old.parentId IS NOT NULL THEN
             BEGIN
                 -- mark the parent's childCount as invalid
-                INSERT INTO InvalidChildCounts VALUES(old.parentId);
+                INSERT INTO InvalidChildCounts (id) VALUES(old.parentId);
             EXCEPTION
                 -- when deleting the last child of a parent, the insert will fail (this is expected)
                 WHEN foreign_key_violation THEN NULL;
@@ -800,7 +802,8 @@
 CREATE TABLE IF NOT EXISTS Queues (
        id BIGSERIAL NOT NULL PRIMARY KEY,
        queueId TEXT NOT NULL,
-       value BYTEA NOT NULL
+       value BYTEA NOT NULL,
+       reservedUntil BIGINT DEFAULT NULL -- new in rev 10
 );
 
 CREATE INDEX IF NOT EXISTS QueuesIndex ON Queues (queueId, id);
@@ -854,7 +857,7 @@
 -- set the global properties that actually documents the DB version, revision and some of the capabilities
 DELETE FROM GlobalProperties WHERE property IN (1, 4, 6, 10, 11, 12, 13, 14);
 INSERT INTO GlobalProperties VALUES (1, 6); -- GlobalProperty_DatabaseSchemaVersion
-INSERT INTO GlobalProperties VALUES (4, 6); -- GlobalProperty_DatabasePatchLevel
+INSERT INTO GlobalProperties VALUES (4, 10); -- GlobalProperty_DatabasePatchLevel
 INSERT INTO GlobalProperties VALUES (6, 1); -- GlobalProperty_GetTotalSizeIsFast
 INSERT INTO GlobalProperties VALUES (10, 1); -- GlobalProperty_HasTrigramIndex
 INSERT INTO GlobalProperties VALUES (11, 3); -- GlobalProperty_HasCreateInstance  -- this is actually the 3rd version of HasCreateInstance
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/PostgreSQL/Plugins/SQL/Upgrades/Rev6ToRev10.sql	Fri Nov 28 15:38:54 2025 +0100
@@ -0,0 +1,7 @@
+-- Adding a PK to these 2 table to allow pg_repack to process these tables, enabling reclaiming disk space and defragmenting the tables.
+
+ALTER TABLE InvalidChildCounts ADD COLUMN pk BIGSERIAL PRIMARY KEY;
+ALTER TABLE GlobalIntegersChanges ADD COLUMN pk BIGSERIAL PRIMARY KEY;
+
+-- Adding the queues timeout
+ALTER TABLE Queues ADD COLUMN reservedUntil BIGINT DEFAULT NULL;
\ No newline at end of file
--- a/PostgreSQL/UnitTests/UnitTestsMain.cpp	Wed Nov 26 14:20:16 2025 +0100
+++ b/PostgreSQL/UnitTests/UnitTestsMain.cpp	Fri Nov 28 15:38:54 2025 +0100
@@ -38,7 +38,7 @@
 
 TEST(PostgreSQL, Version)
 {
-  ASSERT_STREQ("13.1", PG_VERSION);
+  ASSERT_STREQ("18.1", PG_VERSION);
 }
 #endif
 
--- a/Resources/CMake/PostgreSQLConfiguration.cmake	Wed Nov 26 14:20:16 2025 +0100
+++ b/Resources/CMake/PostgreSQLConfiguration.cmake	Fri Nov 28 15:38:54 2025 +0100
@@ -48,13 +48,21 @@
 if (STATIC_BUILD OR NOT USE_SYSTEM_LIBPQ)
   add_definitions(-DORTHANC_POSTGRESQL_STATIC=1)
 
-  SET(LIBPQ_MAJOR 13)
-  SET(LIBPQ_MINOR 1)
+  if (CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang")
+    SET(LIBPQ_MAJOR 13)
+    SET(LIBPQ_MINOR 23)
+    SET(LIBPQ_MD5SUM 86f7b1ace0dc43e993f29a6739a264d8)
+  else()
+    SET(LIBPQ_MAJOR 18)
+    SET(LIBPQ_MINOR 1)
+    SET(LIBPQ_MD5SUM 523b5e7f7f64d331004fd93d37109aa0)
+  endif()
+
   SET(LIBPQ_VERSION ${LIBPQ_MAJOR}.${LIBPQ_MINOR})
 
   SET(LIBPQ_SOURCES_DIR ${CMAKE_BINARY_DIR}/postgresql-${LIBPQ_VERSION})
   DownloadPackage(
-    "551302a823a1ab48b4ed14166beebba9"
+    "${LIBPQ_MD5SUM}"
     "https://orthanc.uclouvain.be/downloads/third-party-downloads/postgresql-${LIBPQ_VERSION}.tar.gz"
     "${LIBPQ_SOURCES_DIR}")
 
@@ -103,11 +111,15 @@
 
   elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin")
     add_definitions(
-      -D_GNU_SOURCE
       -D_THREAD_SAFE
       -D_POSIX_PTHREAD_SEMANTICS
       )
 
+    # this has been included in the OrthancFramework from 1.12.10+
+    if (CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang")
+      SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DSTRERROR_R_INT=1 -D_POSIX_C_SOURCE=200112L")
+    endif()
+
     configure_file(
       ${LIBPQ_SOURCES_DIR}/src/include/port/darwin.h
       ${AUTOGENERATED_DIR}/pg_config_os.h
@@ -181,6 +193,8 @@
     set(PG_INT64_TYPE "long int")
   endif()
   
+  check_type_size("long long" SIZEOF_LONG_LONG)
+
   if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
     set(ALIGNOF_DOUBLE 8)
     set(ALIGNOF_INT 4)
@@ -307,23 +321,24 @@
   set(USE_OPENSSL 1)
   set(USE_OPENSSL_RANDOM 1)
   
-  PrepareCMakeConfigurationFile(
-    ${LIBPQ_SOURCES_DIR}/src/include/pg_config_ext.h.in
-    ${AUTOGENERATED_DIR}/pg_config_ext.h.in)
-  
+ 
   PrepareCMakeConfigurationFile(
     ${LIBPQ_SOURCES_DIR}/src/include/pg_config.h.in
     ${AUTOGENERATED_DIR}/pg_config.h.in)
   
   configure_file(
-    ${AUTOGENERATED_DIR}/pg_config_ext.h.in
-    ${AUTOGENERATED_DIR}/pg_config_ext.h)
-
-  configure_file(
     ${AUTOGENERATED_DIR}/pg_config.h.in
     ${AUTOGENERATED_DIR}/pg_config.h)
 
-
+  if (LIBPQ_MAJOR STREQUAL "13")
+    PrepareCMakeConfigurationFile(
+      ${LIBPQ_SOURCES_DIR}/src/include/pg_config_ext.h.in
+      ${AUTOGENERATED_DIR}/pg_config_ext.h.in)
+   
+    configure_file(
+      ${AUTOGENERATED_DIR}/pg_config_ext.h.in
+      ${AUTOGENERATED_DIR}/pg_config_ext.h)
+  endif()
 
   ##
   ## Generic configuration
@@ -350,45 +365,96 @@
     ${LIBPQ_SOURCES_DIR}/src/interfaces/libpq
     )
 
-  set(LIBPQ_SOURCES
-    # Don't use files from the "src/backend/" folder
-    ${LIBPQ_SOURCES_DIR}/src/common/base64.c
-    ${LIBPQ_SOURCES_DIR}/src/common/encnames.c
-    ${LIBPQ_SOURCES_DIR}/src/common/ip.c
-    ${LIBPQ_SOURCES_DIR}/src/common/link-canary.c
-    ${LIBPQ_SOURCES_DIR}/src/common/md5.c
-    ${LIBPQ_SOURCES_DIR}/src/common/saslprep.c
-    ${LIBPQ_SOURCES_DIR}/src/common/scram-common.c
-    ${LIBPQ_SOURCES_DIR}/src/common/sha2_openssl.c
-    ${LIBPQ_SOURCES_DIR}/src/common/string.c
-    ${LIBPQ_SOURCES_DIR}/src/common/unicode_norm.c
-    ${LIBPQ_SOURCES_DIR}/src/common/wchar.c
-    ${LIBPQ_SOURCES_DIR}/src/interfaces/libpq/fe-auth-scram.c
-    ${LIBPQ_SOURCES_DIR}/src/interfaces/libpq/fe-auth.c
-    ${LIBPQ_SOURCES_DIR}/src/interfaces/libpq/fe-connect.c
-    ${LIBPQ_SOURCES_DIR}/src/interfaces/libpq/fe-exec.c
-    ${LIBPQ_SOURCES_DIR}/src/interfaces/libpq/fe-lobj.c
-    ${LIBPQ_SOURCES_DIR}/src/interfaces/libpq/fe-misc.c
-    ${LIBPQ_SOURCES_DIR}/src/interfaces/libpq/fe-print.c
-    ${LIBPQ_SOURCES_DIR}/src/interfaces/libpq/fe-protocol2.c
-    ${LIBPQ_SOURCES_DIR}/src/interfaces/libpq/fe-protocol3.c
-    ${LIBPQ_SOURCES_DIR}/src/interfaces/libpq/fe-secure-common.c
-    ${LIBPQ_SOURCES_DIR}/src/interfaces/libpq/fe-secure-openssl.c
-    ${LIBPQ_SOURCES_DIR}/src/interfaces/libpq/fe-secure.c
-    ${LIBPQ_SOURCES_DIR}/src/interfaces/libpq/libpq-events.c
-    ${LIBPQ_SOURCES_DIR}/src/interfaces/libpq/pqexpbuffer.c
-    ${LIBPQ_SOURCES_DIR}/src/port/chklocale.c
-    ${LIBPQ_SOURCES_DIR}/src/port/explicit_bzero.c
-    ${LIBPQ_SOURCES_DIR}/src/port/getaddrinfo.c
-    ${LIBPQ_SOURCES_DIR}/src/port/inet_net_ntop.c
-    ${LIBPQ_SOURCES_DIR}/src/port/noblock.c
-    ${LIBPQ_SOURCES_DIR}/src/port/pg_strong_random.c
-    ${LIBPQ_SOURCES_DIR}/src/port/pgstrcasecmp.c
-    ${LIBPQ_SOURCES_DIR}/src/port/pqsignal.c
-    ${LIBPQ_SOURCES_DIR}/src/port/snprintf.c
-    ${LIBPQ_SOURCES_DIR}/src/port/strerror.c
-    ${LIBPQ_SOURCES_DIR}/src/port/thread.c
-    )
+  if (LIBPQ_MAJOR STREQUAL "18")
+
+    set(LIBPQ_SOURCES
+      # Don't use files from the "src/backend/" folder
+      ${LIBPQ_SOURCES_DIR}/src/common/base64.c
+      ${LIBPQ_SOURCES_DIR}/src/common/cryptohash_openssl.c
+      ${LIBPQ_SOURCES_DIR}/src/common/encnames.c
+      ${LIBPQ_SOURCES_DIR}/src/common/fe_memutils.c
+      ${LIBPQ_SOURCES_DIR}/src/common/ip.c
+      ${LIBPQ_SOURCES_DIR}/src/common/link-canary.c
+      ${LIBPQ_SOURCES_DIR}/src/common/jsonapi.c
+      ${LIBPQ_SOURCES_DIR}/src/common/md5.c
+      ${LIBPQ_SOURCES_DIR}/src/common/md5_common.c
+      ${LIBPQ_SOURCES_DIR}/src/common/pg_prng.c
+      ${LIBPQ_SOURCES_DIR}/src/common/psprintf.c
+      ${LIBPQ_SOURCES_DIR}/src/common/saslprep.c
+      ${LIBPQ_SOURCES_DIR}/src/common/scram-common.c
+      ${LIBPQ_SOURCES_DIR}/src/common/hmac_openssl.c
+      ${LIBPQ_SOURCES_DIR}/src/common/string.c
+      ${LIBPQ_SOURCES_DIR}/src/common/stringinfo.c
+      ${LIBPQ_SOURCES_DIR}/src/common/unicode_norm.c
+      ${LIBPQ_SOURCES_DIR}/src/common/wchar.c
+      ${LIBPQ_SOURCES_DIR}/src/interfaces/libpq/fe-auth-oauth.c
+      ${LIBPQ_SOURCES_DIR}/src/interfaces/libpq/fe-auth-scram.c
+      ${LIBPQ_SOURCES_DIR}/src/interfaces/libpq/fe-auth.c
+      ${LIBPQ_SOURCES_DIR}/src/interfaces/libpq/fe-cancel.c
+      ${LIBPQ_SOURCES_DIR}/src/interfaces/libpq/fe-connect.c
+      ${LIBPQ_SOURCES_DIR}/src/interfaces/libpq/fe-exec.c
+      ${LIBPQ_SOURCES_DIR}/src/interfaces/libpq/fe-lobj.c
+      ${LIBPQ_SOURCES_DIR}/src/interfaces/libpq/fe-misc.c
+      ${LIBPQ_SOURCES_DIR}/src/interfaces/libpq/fe-print.c
+      ${LIBPQ_SOURCES_DIR}/src/interfaces/libpq/fe-protocol3.c
+      ${LIBPQ_SOURCES_DIR}/src/interfaces/libpq/fe-secure-common.c
+      ${LIBPQ_SOURCES_DIR}/src/interfaces/libpq/fe-secure-openssl.c
+      ${LIBPQ_SOURCES_DIR}/src/interfaces/libpq/fe-secure.c
+      ${LIBPQ_SOURCES_DIR}/src/interfaces/libpq/fe-trace.c
+      ${LIBPQ_SOURCES_DIR}/src/interfaces/libpq/libpq-events.c
+      ${LIBPQ_SOURCES_DIR}/src/interfaces/libpq/pqexpbuffer.c
+      ${LIBPQ_SOURCES_DIR}/src/port/chklocale.c
+      ${LIBPQ_SOURCES_DIR}/src/port/explicit_bzero.c
+      ${LIBPQ_SOURCES_DIR}/src/port/inet_net_ntop.c
+      ${LIBPQ_SOURCES_DIR}/src/port/noblock.c
+      ${LIBPQ_SOURCES_DIR}/src/port/pg_strong_random.c
+      ${LIBPQ_SOURCES_DIR}/src/port/pg_bitutils.c
+      ${LIBPQ_SOURCES_DIR}/src/port/pgstrcasecmp.c
+      ${LIBPQ_SOURCES_DIR}/src/port/pqsignal.c
+      ${LIBPQ_SOURCES_DIR}/src/port/snprintf.c
+      ${LIBPQ_SOURCES_DIR}/src/port/strerror.c
+      )
+  else()
+    set(LIBPQ_SOURCES
+      # Don't use files from the "src/backend/" folder
+      ${LIBPQ_SOURCES_DIR}/src/common/base64.c
+      ${LIBPQ_SOURCES_DIR}/src/common/encnames.c
+      ${LIBPQ_SOURCES_DIR}/src/common/ip.c
+      ${LIBPQ_SOURCES_DIR}/src/common/link-canary.c
+      ${LIBPQ_SOURCES_DIR}/src/common/md5.c
+      ${LIBPQ_SOURCES_DIR}/src/common/saslprep.c
+      ${LIBPQ_SOURCES_DIR}/src/common/scram-common.c
+      ${LIBPQ_SOURCES_DIR}/src/common/sha2_openssl.c
+      ${LIBPQ_SOURCES_DIR}/src/common/string.c
+      ${LIBPQ_SOURCES_DIR}/src/common/unicode_norm.c
+      ${LIBPQ_SOURCES_DIR}/src/common/wchar.c
+      ${LIBPQ_SOURCES_DIR}/src/interfaces/libpq/fe-auth-scram.c
+      ${LIBPQ_SOURCES_DIR}/src/interfaces/libpq/fe-auth.c
+      ${LIBPQ_SOURCES_DIR}/src/interfaces/libpq/fe-connect.c
+      ${LIBPQ_SOURCES_DIR}/src/interfaces/libpq/fe-exec.c
+      ${LIBPQ_SOURCES_DIR}/src/interfaces/libpq/fe-lobj.c
+      ${LIBPQ_SOURCES_DIR}/src/interfaces/libpq/fe-misc.c
+      ${LIBPQ_SOURCES_DIR}/src/interfaces/libpq/fe-print.c
+      ${LIBPQ_SOURCES_DIR}/src/interfaces/libpq/fe-protocol2.c
+      ${LIBPQ_SOURCES_DIR}/src/interfaces/libpq/fe-protocol3.c
+      ${LIBPQ_SOURCES_DIR}/src/interfaces/libpq/fe-secure-common.c
+      ${LIBPQ_SOURCES_DIR}/src/interfaces/libpq/fe-secure-openssl.c
+      ${LIBPQ_SOURCES_DIR}/src/interfaces/libpq/fe-secure.c
+      ${LIBPQ_SOURCES_DIR}/src/interfaces/libpq/libpq-events.c
+      ${LIBPQ_SOURCES_DIR}/src/interfaces/libpq/pqexpbuffer.c
+      ${LIBPQ_SOURCES_DIR}/src/port/chklocale.c
+      ${LIBPQ_SOURCES_DIR}/src/port/explicit_bzero.c
+      ${LIBPQ_SOURCES_DIR}/src/port/getaddrinfo.c
+      ${LIBPQ_SOURCES_DIR}/src/port/inet_net_ntop.c
+      ${LIBPQ_SOURCES_DIR}/src/port/noblock.c
+      ${LIBPQ_SOURCES_DIR}/src/port/pg_strong_random.c
+      ${LIBPQ_SOURCES_DIR}/src/port/pgstrcasecmp.c
+      ${LIBPQ_SOURCES_DIR}/src/port/pqsignal.c
+      ${LIBPQ_SOURCES_DIR}/src/port/snprintf.c
+      ${LIBPQ_SOURCES_DIR}/src/port/strerror.c
+      ${LIBPQ_SOURCES_DIR}/src/port/thread.c
+      )
+  endif()
 
   if (NOT HAVE_STRLCPY)
     LIST(APPEND LIBPQ_SOURCES
--- a/SQLite/Plugins/SQLiteIndex.cpp	Wed Nov 26 14:20:16 2025 +0100
+++ b/SQLite/Plugins/SQLiteIndex.cpp	Fri Nov 28 15:38:54 2025 +0100
@@ -299,4 +299,61 @@
     throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
   }
 #endif
+
+#if ORTHANC_PLUGINS_HAS_QUEUES == 1
+  bool SQLiteIndex::DequeueValue(std::string& value,
+                                 DatabaseManager& manager,
+                                 const std::string& queueId,
+                                 bool fromFront)
+  {
+    assert(manager.GetDialect() == Dialect_SQLite);
+
+    std::unique_ptr<ISqlLookupFormatter> formatter(CreateLookupFormatter(manager.GetDialect()));
+
+    std::unique_ptr<DatabaseManager::CachedStatement> statement;
+
+    std::string queueIdParameter = formatter->GenerateParameter(queueId);
+
+    if (fromFront)
+    {
+      statement.reset(new DatabaseManager::CachedStatement(
+                        STATEMENT_FROM_HERE, manager,
+                        "SELECT id, value FROM Queues WHERE queueId=" + queueIdParameter + " ORDER BY id ASC LIMIT 1"));
+    }
+    else
+    {
+      statement.reset(new DatabaseManager::CachedStatement(
+                        STATEMENT_FROM_HERE, manager,
+                        "SELECT id, value FROM Queues WHERE queueId=" + queueIdParameter + " ORDER BY id DESC LIMIT 1"));
+    }
+
+    statement->Execute(formatter->GetDictionary());
+
+    if (statement->IsDone())
+    {
+      return false;
+    }
+    else
+    {
+      statement->SetResultFieldType(0, ValueType_Integer64);
+      statement->SetResultFieldType(1, ValueType_BinaryString);
+
+      value = statement->ReadString(1);
+
+      {
+        DatabaseManager::CachedStatement s2(STATEMENT_FROM_HERE, manager,
+                                            "DELETE FROM Queues WHERE id=${id}");
+
+        s2.SetParameterType("id", ValueType_Integer64);
+
+        Dictionary args;
+        args.SetIntegerValue("id", statement->ReadInteger64(0));
+
+        s2.Execute(args);
+      }
+
+      return true;
+    }
+  }
+#endif
 }
--- a/SQLite/Plugins/SQLiteIndex.h	Wed Nov 26 14:20:16 2025 +0100
+++ b/SQLite/Plugins/SQLiteIndex.h	Fri Nov 28 15:38:54 2025 +0100
@@ -70,6 +70,11 @@
       return true;
     }
 
+    virtual bool HasReserveQueueValue() const ORTHANC_OVERRIDE
+    {
+      return false;
+    }
+
     virtual bool HasAuditLogs() const ORTHANC_OVERRIDE
     {
       return false;
@@ -108,5 +113,12 @@
     {
       return false;
     }
+
+#if ORTHANC_PLUGINS_HAS_QUEUES == 1
+    virtual bool DequeueValue(std::string& value,
+                              DatabaseManager& manager,
+                              const std::string& queueId,
+                              bool fromFront) ORTHANC_OVERRIDE;
+#endif
   };
 }
--- a/TODO	Wed Nov 26 14:20:16 2025 +0100
+++ b/TODO	Fri Nov 28 15:38:54 2025 +0100
@@ -7,10 +7,6 @@
 Common - Database index
 -----------------------
 
-* Try to avoid the use of temporary tables:
-  https://discourse.orthanc-server.org/t/image-insert-are-too-slow-databse-performance-too-poor/3820
-
-
 * Implement "large queries" for:
   - updating all metadata of a resource at once
   - update all maindicomtags of 4 resource levels at once
@@ -31,19 +27,7 @@
 PostgreSQL
 ----------
 
-* Check if we can force the schema that is used.  By default, Orthanc
-  is using the 'public' schema but, after a wrong command, we have seen
-  a DB where there was a 'AttachedFiles' table in the public schema and another one in a
-  'MyPacs' schema and Orthanc was actually using the 'MyPacs.AttachedFiles' table !!!
-  Orthanc was then seeing only the most recent attached files !!!
-  We should also be able to use other schemas:
-  https://discourse.orthanc-server.org/t/orthanc-container-unable-to-connect-to-specified-postgresql-database/5471
-
-
-* Seems Orthanc might deadlock when there are plenty of conflicting transactions:
-  https://groups.google.com/g/orthanc-users/c/xQelEcKqL9U/m/HsvxwlkvAQAJ
-  https://groups.google.com/g/orthanc-users/c/1bkClfZ0KBA/m/s4AlwVh3CQAJ 
-
+* upgrade to libpq 18.1
 
 -----
 MySQL