changeset 674:fc78f08ee019 attach-custom-data

many fixes, added unit tests
author Sebastien Jodogne <s.jodogne@gmail.com>
date Fri, 30 May 2025 21:42:13 +0200 (2 months ago)
parents 3d1faa34233f
children 0d2ccb51c70b
files Framework/Common/BinaryStringValue.h Framework/Plugins/DatabaseBackendAdapterV4.cpp Framework/Plugins/IDatabaseBackend.h Framework/Plugins/IndexBackend.cpp Framework/Plugins/IndexBackend.h Framework/Plugins/IndexUnitTests.h Framework/PostgreSQL/PostgreSQLResult.cpp Framework/PostgreSQL/PostgreSQLResult.h Framework/PostgreSQL/PostgreSQLStatement.cpp PostgreSQL/Plugins/SQL/PrepareIndex.sql SQLite/Plugins/PrepareIndex.sql
diffstat 11 files changed, 372 insertions(+), 94 deletions(-) [+]
line wrap: on
line diff
--- a/Framework/Common/BinaryStringValue.h	Fri May 30 15:46:56 2025 +0200
+++ b/Framework/Common/BinaryStringValue.h	Fri May 30 21:42:13 2025 +0200
@@ -35,6 +35,10 @@
     std::string  content_;
 
   public:
+    BinaryStringValue()
+    {
+    }
+
     explicit BinaryStringValue(const std::string& content) :
       content_(content)
     {
@@ -55,6 +59,11 @@
       return content_.size();
     }
 
+    void Swap(std::string& other)
+    {
+      content_.swap(other);
+    }
+
     virtual ValueType GetType() const ORTHANC_OVERRIDE
     {
       return ValueType_BinaryString;
--- a/Framework/Plugins/DatabaseBackendAdapterV4.cpp	Fri May 30 15:46:56 2025 +0200
+++ b/Framework/Plugins/DatabaseBackendAdapterV4.cpp	Fri May 30 21:42:13 2025 +0200
@@ -1370,8 +1370,7 @@
       case Orthanc::DatabasePluginMessages::OPERATION_GET_KEY_VALUE:
       {
         std::string value;
-        bool found = backend.GetKeyValue(manager, 
-                                         value,
+        bool found = backend.GetKeyValue(value, manager,
                                          request.get_key_value().store_id(),
                                          request.get_key_value().key());
         response.mutable_get_key_value()->set_found(found);
@@ -1409,8 +1408,7 @@
       case Orthanc::DatabasePluginMessages::OPERATION_DEQUEUE_VALUE:
       {
         std::string value;
-        bool found = backend.DequeueValue(manager,
-                                          value,
+        bool found = backend.DequeueValue(value, manager,
                                           request.dequeue_value().queue_id(),
                                           request.dequeue_value().origin() == Orthanc::DatabasePluginMessages::QUEUE_ORIGIN_FRONT);
         response.mutable_dequeue_value()->set_found(found);
--- a/Framework/Plugins/IDatabaseBackend.h	Fri May 30 15:46:56 2025 +0200
+++ b/Framework/Plugins/IDatabaseBackend.h	Fri May 30 21:42:13 2025 +0200
@@ -415,18 +415,18 @@
 
 #if ORTHANC_PLUGINS_HAS_KEY_VALUE_STORES == 1
     virtual void StoreKeyValue(DatabaseManager& manager,
-                                const std::string& storeId,
-                                const std::string& key,
-                                const std::string& value) = 0;
+                               const std::string& storeId,
+                               const std::string& key,
+                               const std::string& value) = 0;
 
     virtual void DeleteKeyValue(DatabaseManager& manager,
                                 const std::string& storeId,
                                 const std::string& key) = 0;
 
-    virtual bool GetKeyValue(DatabaseManager& manager,
-                              std::string& value,
-                              const std::string& storeId,
-                              const std::string& key) = 0;
+    virtual bool GetKeyValue(std::string& value,
+                             DatabaseManager& manager,
+                             const std::string& storeId,
+                             const std::string& key) = 0;
 
     virtual void ListKeysValues(Orthanc::DatabasePluginMessages::TransactionResponse& response,
                                 DatabaseManager& manager,
@@ -438,8 +438,8 @@
                               const std::string& queueId,
                               const std::string& value) = 0;
 
-    virtual bool DequeueValue(DatabaseManager& manager,
-                              std::string& value,
+    virtual bool DequeueValue(std::string& value,
+                              DatabaseManager& manager,
                               const std::string& queueId,
                               bool fromFront) = 0;
 
--- a/Framework/Plugins/IndexBackend.cpp	Fri May 30 15:46:56 2025 +0200
+++ b/Framework/Plugins/IndexBackend.cpp	Fri May 30 21:42:13 2025 +0200
@@ -23,8 +23,6 @@
 
 #include "IndexBackend.h"
 
-#include "../Common/BinaryStringValue.h"
-#include "../Common/Integer64Value.h"
 #include "../Common/Utf8StringValue.h"
 #include "DatabaseBackendAdapterV2.h"
 #include "DatabaseBackendAdapterV3.h"
@@ -4385,15 +4383,14 @@
         STATEMENT_FROM_HERE, manager,
         "INSERT INTO KeyValueStores VALUES(${storeId}, ${key}, ${value}) ON CONFLICT (storeId, key) DO UPDATE SET value = EXCLUDED.value;");
 
-      Dictionary args;
-
       statement.SetParameterType("storeId", ValueType_Utf8String);
       statement.SetParameterType("key", ValueType_Utf8String);
-      statement.SetParameterType("value", ValueType_Utf8String);
-
+      statement.SetParameterType("value", ValueType_BinaryString);
+
+      Dictionary args;
       args.SetUtf8Value("storeId", storeId);
       args.SetUtf8Value("key", key);
-      args.SetUtf8Value("value", value);
+      args.SetBinaryValue("value", value);
 
       statement.Execute(args);
     }
@@ -4406,36 +4403,35 @@
         STATEMENT_FROM_HERE, manager,
         "DELETE FROM KeyValueStores WHERE storeId = ${storeId} AND key = ${key}");
 
-      Dictionary args;
-
       statement.SetParameterType("storeId", ValueType_Utf8String);
       statement.SetParameterType("key", ValueType_Utf8String);
 
+      Dictionary args;
       args.SetUtf8Value("storeId", storeId);
       args.SetUtf8Value("key", key);
 
       statement.Execute(args);
     }
 
-    bool IndexBackend::GetKeyValue(DatabaseManager& manager,
-                                   std::string& value,
+    bool IndexBackend::GetKeyValue(std::string& value,
+                                   DatabaseManager& manager,
                                    const std::string& storeId,
-                                  const std::string& key)
+                                   const std::string& key)
     {
       DatabaseManager::CachedStatement statement(
         STATEMENT_FROM_HERE, manager,
         "SELECT value FROM KeyValueStores WHERE storeId = ${storeId} AND key = ${key}");
         
       statement.SetReadOnly(true);
-      
-      Dictionary args;
       statement.SetParameterType("storeId", ValueType_Utf8String);
       statement.SetParameterType("key", ValueType_Utf8String);
 
+      Dictionary args;
       args.SetUtf8Value("storeId", storeId);
       args.SetUtf8Value("key", key);
 
       statement.Execute(args);
+      statement.SetResultFieldType(0, ValueType_BinaryString);
 
       if (statement.IsDone())
       {
@@ -4452,6 +4448,8 @@
                                       DatabaseManager& manager,
                                       const Orthanc::DatabasePluginMessages::ListKeysValues_Request& request)
     {
+      response.mutable_list_keys_values()->Clear();
+
       LookupFormatter formatter(manager.GetDialect());
 
       std::unique_ptr<DatabaseManager::CachedStatement> statement;
@@ -4467,14 +4465,13 @@
       else
       {
         std::string fromKeyParameter = formatter.GenerateParameter(request.from_key());
-
         statement.reset(new DatabaseManager::CachedStatement(
                         STATEMENT_FROM_HERE, manager,
                         "SELECT key, value FROM KeyValueStores WHERE storeId= " + storeIdParameter + " AND key > " + fromKeyParameter + " ORDER BY key ASC " + formatter.FormatLimits(0, request.limit())));
       }
-        
+
       statement->Execute(formatter.GetDictionary());
-        
+
       if (!statement->IsDone())
       {
         if (statement->GetResultFieldsCount() != 2)
@@ -4483,7 +4480,7 @@
         }
         
         statement->SetResultFieldType(0, ValueType_Utf8String);
-        statement->SetResultFieldType(1, ValueType_Utf8String);
+        statement->SetResultFieldType(1, ValueType_BinaryString);
 
         while (!statement->IsDone())
         {
@@ -4507,19 +4504,18 @@
         STATEMENT_FROM_HERE, manager,
         "INSERT INTO Queues VALUES(${AUTOINCREMENT} ${queueId}, ${value})");
 
+      statement.SetParameterType("queueId", ValueType_Utf8String);
+      statement.SetParameterType("value", ValueType_BinaryString);
+
       Dictionary args;
-
-      statement.SetParameterType("queueId", ValueType_Utf8String);
-      statement.SetParameterType("value", ValueType_Utf8String);
-
       args.SetUtf8Value("queueId", queueId);
-      args.SetUtf8Value("value", value);
+      args.SetBinaryValue("value", value);
 
       statement.Execute(args);
     }
 
-    bool IndexBackend::DequeueValue(DatabaseManager& manager,
-                                    std::string& value,
+    bool IndexBackend::DequeueValue(std::string& value,
+                                    DatabaseManager& manager,
                                     const std::string& queueId,
                                     bool fromFront)
     {
@@ -4529,21 +4525,44 @@
       
       std::string queueIdParameter = formatter.GenerateParameter(queueId);
 
-      if (fromFront)
+      switch (manager.GetDialect())
       {
-        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"));
+        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;
+
+        case Dialect_SQLite:
+          if (fromFront)
+          {
+            statement.reset(new DatabaseManager::CachedStatement(
+                              STATEMENT_FROM_HERE, manager,
+                              "DELETE FROM Queues WHERE id = (SELECT id FROM Queues WHERE queueId=" + queueIdParameter + " ORDER BY id ASC LIMIT 1) RETURNING value"));
+          }
+          else
+          {
+            statement.reset(new DatabaseManager::CachedStatement(
+                              STATEMENT_FROM_HERE, manager,
+                              "DELETE FROM Queues WHERE id = (SELECT id FROM Queues WHERE queueId=" + queueIdParameter + " ORDER BY id DESC LIMIT 1) RETURNING value"));
+          }
+          break;
+
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
       }
-      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"));
-      }
-        
+
       statement->Execute(formatter.GetDictionary());
 
       if (statement->IsDone())
@@ -4552,9 +4571,8 @@
       }        
       else
       {
-        statement->SetResultFieldType(0, ValueType_Utf8String);
+        statement->SetResultFieldType(0, ValueType_BinaryString);
         value = statement->ReadString(0);
-
         return true;
       }
     }
@@ -4567,12 +4585,13 @@
         "SELECT COUNT(*) FROM Queues WHERE queueId = ${queueId}");
         
       statement.SetReadOnly(true);
-      
+      statement.SetParameterType("queueId", ValueType_Utf8String);
+
       Dictionary args;
-      statement.SetParameterType("queueId", ValueType_Utf8String);
       args.SetUtf8Value("queueId", queueId);
 
       statement.Execute(args);
+      statement.SetResultFieldType(0, ValueType_Integer64);
 
       return statement.ReadInteger64(0);
     }
@@ -4589,9 +4608,9 @@
         "compressedSize, compressedHash, revision, customData FROM AttachedFiles WHERE uuid = ${uuid}");
 
       statement.SetReadOnly(true);
+      statement.SetParameterType("uuid", ValueType_Utf8String);
 
       Dictionary args;
-      statement.SetParameterType("uuid", ValueType_Utf8String);
       args.SetUtf8Value("uuid", request.uuid());
 
       statement.Execute(args);
@@ -4630,6 +4649,7 @@
 
       Dictionary args;
       args.SetUtf8Value("uuid", attachmentUuid);
+
       if (customData.empty())
       {
         args.SetUtf8NullValue("customData");
--- a/Framework/Plugins/IndexBackend.h	Fri May 30 15:46:56 2025 +0200
+++ b/Framework/Plugins/IndexBackend.h	Fri May 30 21:42:13 2025 +0200
@@ -468,18 +468,18 @@
 
 #if ORTHANC_PLUGINS_HAS_KEY_VALUE_STORES == 1
     virtual void StoreKeyValue(DatabaseManager& manager,
-                                const std::string& storeId,
-                                const std::string& key,
-                                const std::string& value) ORTHANC_OVERRIDE;
+                               const std::string& storeId,
+                               const std::string& key,
+                               const std::string& value) ORTHANC_OVERRIDE;
 
     virtual void DeleteKeyValue(DatabaseManager& manager,
                                 const std::string& storeId,
                                 const std::string& key) ORTHANC_OVERRIDE;
 
-    virtual bool GetKeyValue(DatabaseManager& manager,
-                              std::string& value,
-                              const std::string& storeId,
-                              const std::string& key) ORTHANC_OVERRIDE;
+    virtual bool GetKeyValue(std::string& value,
+                             DatabaseManager& manager,
+                             const std::string& storeId,
+                             const std::string& key) ORTHANC_OVERRIDE;
 
     virtual void ListKeysValues(Orthanc::DatabasePluginMessages::TransactionResponse& response,
                                 DatabaseManager& manager,
@@ -491,8 +491,8 @@
                               const std::string& queueId,
                               const std::string& value) ORTHANC_OVERRIDE;
 
-    virtual bool DequeueValue(DatabaseManager& manager,
-                              std::string& value,
+    virtual bool DequeueValue(std::string& value,
+                              DatabaseManager& manager,
                               const std::string& queueId,
                               bool fromFront) ORTHANC_OVERRIDE;
 
--- a/Framework/Plugins/IndexUnitTests.h	Fri May 30 15:46:56 2025 +0200
+++ b/Framework/Plugins/IndexUnitTests.h	Fri May 30 21:42:13 2025 +0200
@@ -217,6 +217,80 @@
 }
 
 
+static void ListKeys(std::set<std::string>& keys,
+                     OrthancDatabases::IndexBackend& db,
+                     OrthancDatabases::DatabaseManager& manager,
+                     const std::string& storeId)
+{
+  {
+    Orthanc::DatabasePluginMessages::ListKeysValues_Request request;
+    request.set_store_id(storeId);
+    request.set_from_first(true);
+    request.set_limit(0);
+
+    Orthanc::DatabasePluginMessages::TransactionResponse response;
+    db.ListKeysValues(response, manager, request);
+
+    keys.clear();
+
+    for (int i = 0; i < response.list_keys_values().keys_values_size(); i++)
+    {
+      const Orthanc::DatabasePluginMessages::ListKeysValues_Response_KeyValue& item = response.list_keys_values().keys_values(i);
+      keys.insert(item.key());
+
+      std::string value;
+      if (!db.GetKeyValue(value, manager, storeId, item.key()) ||
+          value != item.value())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+    }
+  }
+
+  {
+    std::set<std::string> keys2;
+
+    // Alternative implementation using an iterator
+    Orthanc::DatabasePluginMessages::ListKeysValues_Request request;
+    request.set_store_id(storeId);
+    request.set_from_first(true);
+    request.set_limit(1);
+
+    Orthanc::DatabasePluginMessages::TransactionResponse response;
+    db.ListKeysValues(response, manager, request);
+
+    while (response.list_keys_values().keys_values_size() > 0)
+    {
+      int count = response.list_keys_values().keys_values_size();
+
+      for (int i = 0; i < count; i++)
+      {
+        keys2.insert(response.list_keys_values().keys_values(i).key());
+      }
+
+      request.set_from_first(false);
+      request.set_from_key(response.list_keys_values().keys_values(count - 1).key());
+      db.ListKeysValues(response, manager, request);
+    }
+
+    if (keys.size() != keys2.size())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+    else
+    {
+      for (std::set<std::string>::const_iterator it = keys.begin(); it != keys.end(); ++it)
+      {
+        if (keys2.find(*it) == keys2.end())
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+        }
+      }
+    }
+  }
+}
+
+
 TEST(IndexBackend, Basic)
 {
   using namespace OrthancDatabases;
@@ -238,7 +312,7 @@
 #elif ORTHANC_ENABLE_ODBC == 1
   OdbcIndex db(&context, connectionString_, false);
 #elif ORTHANC_ENABLE_SQLITE == 1  // Must be the last one
-  SQLiteIndex db(&context);  // Open in memory
+  SQLiteIndex db(&context, "tutu.db");  // Open in memory
 #else
 #  error Unsupported database backend
 #endif
@@ -803,5 +877,133 @@
   }
 #endif
 
+  {
+    manager->StartTransaction(TransactionType_ReadWrite);
+
+    std::set<std::string> keys;
+    ListKeys(keys, db, *manager, "test");
+    ASSERT_EQ(0u, keys.size());
+
+    std::string s;
+    ASSERT_FALSE(db.GetKeyValue(s, *manager, "test", "hello"));
+    db.DeleteKeyValue(*manager, s, "test");
+
+    db.StoreKeyValue(*manager, "test", "hello", "world");
+    db.StoreKeyValue(*manager, "another", "hello", "world");
+    ListKeys(keys, db, *manager, "test");
+    ASSERT_EQ(1u, keys.size());
+    ASSERT_EQ("hello", *keys.begin());
+    ASSERT_TRUE(db.GetKeyValue(s, *manager, "test", "hello"));  ASSERT_EQ("world", s);
+
+    db.StoreKeyValue(*manager, "test", "hello", "overwritten");
+    ListKeys(keys, db, *manager, "test");
+    ASSERT_EQ(1u, keys.size());
+    ASSERT_EQ("hello", *keys.begin());
+    ASSERT_TRUE(db.GetKeyValue(s, *manager, "test", "hello"));  ASSERT_EQ("overwritten", s);
+
+    db.StoreKeyValue(*manager, "test", "hello2", "world2");
+    db.StoreKeyValue(*manager, "test", "hello3", "world3");
+
+    ListKeys(keys, db, *manager, "test");
+    ASSERT_EQ(3u, keys.size());
+    ASSERT_TRUE(keys.find("hello") != keys.end());
+    ASSERT_TRUE(keys.find("hello2") != keys.end());
+    ASSERT_TRUE(keys.find("hello3") != keys.end());
+    ASSERT_TRUE(db.GetKeyValue(s, *manager, "test", "hello"));   ASSERT_EQ("overwritten", s);
+    ASSERT_TRUE(db.GetKeyValue(s, *manager, "test", "hello2"));  ASSERT_EQ("world2", s);
+    ASSERT_TRUE(db.GetKeyValue(s, *manager, "test", "hello3"));  ASSERT_EQ("world3", s);
+
+    db.DeleteKeyValue(*manager, "test", "hello2");
+
+    ListKeys(keys, db, *manager, "test");
+    ASSERT_EQ(2u, keys.size());
+    ASSERT_TRUE(keys.find("hello") != keys.end());
+    ASSERT_TRUE(keys.find("hello3") != keys.end());
+    ASSERT_TRUE(db.GetKeyValue(s, *manager, "test", "hello"));   ASSERT_EQ("overwritten", s);
+    ASSERT_FALSE(db.GetKeyValue(s, *manager, "test", "hello2"));
+    ASSERT_TRUE(db.GetKeyValue(s, *manager, "test", "hello3"));  ASSERT_EQ("world3", s);
+
+    db.DeleteKeyValue(*manager, "test", "nope");
+    db.DeleteKeyValue(*manager, "test", "hello");
+    db.DeleteKeyValue(*manager, "test", "hello3");
+
+    ListKeys(keys, db, *manager, "test");
+    ASSERT_EQ(0u, keys.size());
+
+    {
+      std::string blob;
+      blob.push_back(0);
+      blob.push_back(1);
+      blob.push_back(0);
+      blob.push_back(2);
+      db.StoreKeyValue(*manager, "test", "blob", blob); // Storing binary values
+    }
+
+    ASSERT_TRUE(db.GetKeyValue(s, *manager, "test", "blob"));
+    ASSERT_EQ(4u, s.size());
+    ASSERT_EQ(0u, static_cast<uint8_t>(s[0]));
+    ASSERT_EQ(1u, static_cast<uint8_t>(s[1]));
+    ASSERT_EQ(0u, static_cast<uint8_t>(s[2]));
+    ASSERT_EQ(2u, static_cast<uint8_t>(s[3]));
+    db.DeleteKeyValue(*manager, "test", "blob");
+    ASSERT_FALSE(db.GetKeyValue(s, *manager, "test", "blob"));
+
+    manager->CommitTransaction();
+  }
+
+  {
+    manager->StartTransaction(TransactionType_ReadWrite);
+
+    ASSERT_EQ(0u, db.GetQueueSize(*manager, "test"));
+    db.EnqueueValue(*manager, "test", "a");
+    db.EnqueueValue(*manager, "another", "hello");
+    ASSERT_EQ(1u, db.GetQueueSize(*manager, "test"));
+    db.EnqueueValue(*manager, "test", "b");
+    ASSERT_EQ(2u, db.GetQueueSize(*manager, "test"));
+    db.EnqueueValue(*manager, "test", "c");
+    ASSERT_EQ(3u, db.GetQueueSize(*manager, "test"));
+
+    std::string s;
+    ASSERT_FALSE(db.DequeueValue(s, *manager, "nope", false));
+    ASSERT_TRUE(db.DequeueValue(s, *manager, "test", true));  ASSERT_EQ("a", s);
+    ASSERT_EQ(2u, db.GetQueueSize(*manager, "test"));
+    ASSERT_TRUE(db.DequeueValue(s, *manager, "test", true));  ASSERT_EQ("b", s);
+    ASSERT_EQ(1u, db.GetQueueSize(*manager, "test"));
+    ASSERT_TRUE(db.DequeueValue(s, *manager, "test", true));  ASSERT_EQ("c", s);
+    ASSERT_EQ(0u, db.GetQueueSize(*manager, "test"));
+    ASSERT_FALSE(db.DequeueValue(s, *manager, "test", true));
+
+    db.EnqueueValue(*manager, "test", "a");
+    db.EnqueueValue(*manager, "test", "b");
+    db.EnqueueValue(*manager, "test", "c");
+
+    ASSERT_TRUE(db.DequeueValue(s, *manager, "test", false));  ASSERT_EQ("c", s);
+    ASSERT_TRUE(db.DequeueValue(s, *manager, "test", false));  ASSERT_EQ("b", s);
+    ASSERT_TRUE(db.DequeueValue(s, *manager, "test", false));  ASSERT_EQ("a", s);
+    ASSERT_FALSE(db.DequeueValue(s, *manager, "test", false));
+
+    {
+      std::string blob;
+      blob.push_back(0);
+      blob.push_back(1);
+      blob.push_back(0);
+      blob.push_back(2);
+      db.EnqueueValue(*manager, "test", blob); // Storing binary values
+    }
+
+    ASSERT_TRUE(db.DequeueValue(s, *manager, "test", true));
+    ASSERT_EQ(4u, s.size());
+    ASSERT_EQ(0u, static_cast<uint8_t>(s[0]));
+    ASSERT_EQ(1u, static_cast<uint8_t>(s[1]));
+    ASSERT_EQ(0u, static_cast<uint8_t>(s[2]));
+    ASSERT_EQ(2u, static_cast<uint8_t>(s[3]));
+
+    ASSERT_FALSE(db.DequeueValue(s, *manager, "test", true));
+
+    ASSERT_EQ(1u, db.GetQueueSize(*manager, "another"));
+
+    manager->CommitTransaction();
+  }
+
   manager->Close();
 }
--- a/Framework/PostgreSQL/PostgreSQLResult.cpp	Fri May 30 15:46:56 2025 +0200
+++ b/Framework/PostgreSQL/PostgreSQLResult.cpp	Fri May 30 21:42:13 2025 +0200
@@ -168,7 +168,7 @@
     CheckColumn(column, 0);
 
     Oid oid = PQftype(reinterpret_cast<PGresult*>(result_), column);
-    if (oid != TEXTOID && oid != VARCHAROID && oid != BYTEAOID)
+    if (oid != TEXTOID && oid != VARCHAROID)
     {
       throw Orthanc::OrthancException(Orthanc::ErrorCode_BadParameterType);
     }
@@ -177,6 +177,22 @@
   }
 
 
+  void PostgreSQLResult::GetBinaryString(std::string& target,
+                                         unsigned int column) const
+  {
+    CheckColumn(column, 0);
+
+    Oid oid = PQftype(reinterpret_cast<PGresult*>(result_), column);
+    if (oid != BYTEAOID)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadParameterType);
+    }
+
+    target.assign(PQgetvalue(reinterpret_cast<PGresult*>(result_), position_, column),
+                  PQgetlength(reinterpret_cast<PGresult*>(result_), position_, column));
+  }
+
+
   std::string PostgreSQLResult::GetLargeObjectOid(unsigned int column) const
   {
     CheckColumn(column, OIDOID);
@@ -256,7 +272,14 @@
         return new Utf8StringValue(GetString(column));
 
       case BYTEAOID:
-        return new BinaryStringValue(GetString(column));
+      {
+        std::string s;
+        GetBinaryString(s, column);
+
+        std::unique_ptr<BinaryStringValue> value(new BinaryStringValue);
+        value->Swap(s);
+        return value.release();
+      }
 
       case OIDOID:
         return new LargeObjectResult(database_, GetLargeObjectOid(column));
--- a/Framework/PostgreSQL/PostgreSQLResult.h	Fri May 30 15:46:56 2025 +0200
+++ b/Framework/PostgreSQL/PostgreSQLResult.h	Fri May 30 21:42:13 2025 +0200
@@ -74,6 +74,9 @@
 
     std::string GetString(unsigned int column) const;
 
+    void GetBinaryString(std::string& target,
+                         unsigned int column) const;
+
     std::string GetLargeObjectOid(unsigned int column) const;
 
     void GetLargeObjectContent(std::string& content,
--- a/Framework/PostgreSQL/PostgreSQLStatement.cpp	Fri May 30 15:46:56 2025 +0200
+++ b/Framework/PostgreSQL/PostgreSQLStatement.cpp	Fri May 30 21:42:13 2025 +0200
@@ -223,7 +223,7 @@
     }
 
     oids_[param] = type;
-    binary_[param] = (type == TEXTOID || type == BYTEAOID || type == OIDOID) ? 0 : 1;
+    binary_[param] = (type == TEXTOID || type == OIDOID) ? 0 : 1;
   }
 
 
@@ -283,16 +283,23 @@
 
     if (PQtransactionStatus(reinterpret_cast<PGconn*>(database_.pg_)) == PQTRANS_INERROR)
     {
+      std::string message;
+
       if (result != NULL)
       {
-        PQclear(result);
+        message.assign(PQresultErrorMessage(result));
+        PQclear(result);  // Frees the memory allocated by "PQresultErrorMessage()"
       }
-      
+
+      if (message.empty())
+      {
+        message = "Collision between multiple writers";
+      }
+
 #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 9, 2)
-      std::string errorString(PQresultErrorMessage(result));
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_DatabaseCannotSerialize, errorString, false); // don't log here, it is handled at higher level
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_DatabaseCannotSerialize, message, false); // don't log here, it is handled #else
 #else
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_Database, "Collision between multiple writers");
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_Database, message);
 #endif
     }
     else if (result == NULL)
@@ -454,19 +461,21 @@
       throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
     }
 
-    if (oids_[param] != TEXTOID && oids_[param] != BYTEAOID)
-    {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadParameterType);
-    }
-
-    if (value.size() == 0)
+    switch (oids_[param])
     {
-      inputs_->SetItem(param, "", 1 /* end-of-string character */);
-    }
-    else
-    {
-      inputs_->SetItem(param, value.c_str(), 
-                       value.size() + 1);  // "+1" for end-of-string character
+      case TEXTOID:
+      {
+        std::string s = value + '\0';  // Make sure that there is an end-of-string character
+        inputs_->SetItem(param, s.c_str(), s.size());
+        break;
+      }
+
+      case BYTEAOID:
+        inputs_->SetItem(param, value.c_str(), value.size());
+        break;
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadParameterType);
     }
   }
 
@@ -547,19 +556,16 @@
             break;
 
           case ValueType_Utf8String:
-            BindString(i, dynamic_cast<const Utf8StringValue&>
-                      (value).GetContent());
+            BindString(i, dynamic_cast<const Utf8StringValue&>(value).GetContent());
             break;
 
           case ValueType_BinaryString:
-            BindString(i, dynamic_cast<const BinaryStringValue&>
-                      (value).GetContent());
+            BindString(i, dynamic_cast<const BinaryStringValue&>(value).GetContent());
             break;
 
           case ValueType_InputFile:
           {
-            const InputFileValue& blob =
-              dynamic_cast<const InputFileValue&>(value);
+            const InputFileValue& blob = dynamic_cast<const InputFileValue&>(value);
 
             PostgreSQLLargeObject largeObject(database_, blob.GetContent());
             BindLargeObject(i, largeObject);
--- a/PostgreSQL/Plugins/SQL/PrepareIndex.sql	Fri May 30 15:46:56 2025 +0200
+++ b/PostgreSQL/Plugins/SQL/PrepareIndex.sql	Fri May 30 21:42:13 2025 +0200
@@ -726,14 +726,14 @@
 CREATE TABLE KeyValueStores(
        storeId TEXT NOT NULL,
        key TEXT NOT NULL,
-       value TEXT NOT NULL,
+       value BYTEA NOT NULL,
        PRIMARY KEY(storeId, key)  -- Prevents duplicates
        );
 
 CREATE TABLE Queues (
        id BIGSERIAL NOT NULL PRIMARY KEY,
        queueId TEXT NOT NULL,
-       value TEXT
+       value BYTEA
 );
 
 CREATE INDEX QueuesIndex ON Queues (queueId, id);
--- a/SQLite/Plugins/PrepareIndex.sql	Fri May 30 15:46:56 2025 +0200
+++ b/SQLite/Plugins/PrepareIndex.sql	Fri May 30 21:42:13 2025 +0200
@@ -156,3 +156,20 @@
 BEGIN
   INSERT INTO PatientRecyclingOrder VALUES (NULL, new.internalId);
 END;
+
+
+-- New in Orthanc 1.12.8
+CREATE TABLE KeyValueStores(
+       storeId TEXT NOT NULL,
+       key TEXT NOT NULL,
+       value BLOB NOT NULL,
+       PRIMARY KEY(storeId, key)  -- Prevents duplicates
+       );
+
+CREATE TABLE Queues (
+       id INTEGER PRIMARY KEY AUTOINCREMENT,
+       queueId TEXT NOT NULL,
+       value BLOB
+);
+
+CREATE INDEX QueuesIndex ON Queues (queueId, id);