changeset 6102:b1764e7248e0 attach-custom-data

wip: key-value store for plugins
author Alain Mazy <am@orthanc.team>
date Wed, 07 May 2025 19:27:06 +0200
parents 090ef6a37882
children 91527f33f3bf
files NEWS OrthancServer/CMakeLists.txt OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp OrthancServer/Plugins/Engine/OrthancPlugins.cpp OrthancServer/Plugins/Engine/OrthancPlugins.h OrthancServer/Plugins/Engine/PluginsEnumerations.cpp OrthancServer/Plugins/Engine/PluginsEnumerations.h OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto OrthancServer/Sources/Database/IDatabaseWrapper.h OrthancServer/Sources/Database/InstallKeyValueStore.sql OrthancServer/Sources/Database/PrepareDatabase.sql OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp OrthancServer/Sources/Database/StatelessDatabaseOperations.h OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp
diffstat 18 files changed, 617 insertions(+), 11 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Mon Apr 07 13:23:11 2025 +0200
+++ b/NEWS	Wed May 07 19:27:06 2025 +0200
@@ -13,7 +13,7 @@
 Plugins
 -------
 
-* New database plugin SDK (vX) to handle customData for attachments.
+* New database plugin SDK (vX) to handle customData for attachments and key-value store.
 * New storage plugin SDK (v3) to handle customData for attachments.
 
 
--- a/OrthancServer/CMakeLists.txt	Mon Apr 07 13:23:11 2025 +0200
+++ b/OrthancServer/CMakeLists.txt	Wed May 07 19:27:06 2025 +0200
@@ -253,6 +253,7 @@
   INSTALL_TRACK_ATTACHMENTS_SIZE    ${CMAKE_SOURCE_DIR}/Sources/Database/InstallTrackAttachmentsSize.sql
   INSTALL_LABELS_TABLE              ${CMAKE_SOURCE_DIR}/Sources/Database/InstallLabelsTable.sql
   INSTALL_REVISION_AND_CUSTOM_DATA  ${CMAKE_SOURCE_DIR}/Sources/Database/InstallRevisionAndCustomData.sql  
+  INSTALL_KEY_VALUE_STORE           ${CMAKE_SOURCE_DIR}/Sources/Database/InstallKeyValueStore.sql
   )
 
 if (STANDALONE_BUILD)
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp	Mon Apr 07 13:23:11 2025 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp	Wed May 07 19:27:06 2025 +0200
@@ -1450,6 +1450,27 @@
     {
       throw OrthancException(ErrorCode_InternalError);  // Not supported
     }
+
+    virtual void StoreKeyValue(const std::string& pluginId,
+                               const std::string& key,
+                               const std::string& value) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_InternalError);  // Not supported
+    }
+
+    virtual void DeleteKeyValue(const std::string& pluginId,
+                                const std::string& key) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_InternalError);  // Not supported
+    }
+
+    virtual bool GetKeyValue(std::string& value,
+                             const std::string& pluginId,
+                             const std::string& key) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_InternalError);  // Not supported
+    }
+
   };
 
 
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp	Mon Apr 07 13:23:11 2025 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp	Wed May 07 19:27:06 2025 +0200
@@ -1063,6 +1063,27 @@
     {
       throw OrthancException(ErrorCode_InternalError);  // Not supported
     }
+
+    virtual void StoreKeyValue(const std::string& pluginId,
+                               const std::string& key,
+                               const std::string& value) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_InternalError);  // Not supported
+    }
+
+    virtual void DeleteKeyValue(const std::string& pluginId,
+                                const std::string& key) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_InternalError);  // Not supported
+    }
+
+    virtual bool GetKeyValue(std::string& value,
+                             const std::string& pluginId,
+                             const std::string& key) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_InternalError);  // Not supported
+    }
+
   };
 
   
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp	Mon Apr 07 13:23:11 2025 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp	Wed May 07 19:27:06 2025 +0200
@@ -1807,6 +1807,27 @@
         find.ExecuteExpand(response, capabilities, request, identifier);
       }
     }
+
+    virtual void StoreKeyValue(const std::string& pluginId,
+                               const std::string& key,
+                               const std::string& value) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_NotImplemented);  // TODO_ATTACH_CUSTOM_DATA
+    }
+
+    virtual void DeleteKeyValue(const std::string& pluginId,
+                                const std::string& key) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_NotImplemented);  // TODO_ATTACH_CUSTOM_DATA
+    }
+
+    virtual bool GetKeyValue(std::string& value,
+                             const std::string& pluginId,
+                             const std::string& key) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_NotImplemented);  // TODO_ATTACH_CUSTOM_DATA
+    }
+
   };
 
 
@@ -1897,6 +1918,7 @@
       dbCapabilities_.SetMeasureLatency(systemInfo.has_measure_latency());
       dbCapabilities_.SetHasExtendedChanges(systemInfo.has_extended_changes());
       dbCapabilities_.SetHasFindSupport(systemInfo.supports_find());
+      dbCapabilities_.SetHasKeyValueStore(systemInfo.has_key_value_store());
     }
 
     open_ = true;
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Mon Apr 07 13:23:11 2025 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Wed May 07 19:27:06 2025 +0200
@@ -4675,7 +4675,48 @@
 
       ServerContext::StoreResult result = lock.GetContext().AdoptAttachment(resultPublicId, *dicom, StoreInstanceMode_Default, adoptedFile);
 
-      // TODO_ATTACH_CUSTOM_DATA: handle result
+      CopyToMemoryBuffer(*parameters.attachmentUuid, adoptedFile.GetUuid().size() > 0 ? adoptedFile.GetUuid().c_str() : NULL, adoptedFile.GetUuid().size());
+      CopyToMemoryBuffer(*parameters.createdResourceId, resultPublicId.size() > 0 ? resultPublicId.c_str() : NULL, resultPublicId.size());
+      *(parameters.storeStatus) = Plugins::Convert(result.GetStatus());
+    }
+  }
+
+  bool OrthancPlugins::HasKeyValueStore()
+  {
+    PImpl::ServerContextReference lock(*pimpl_);
+
+    return lock.GetContext().GetIndex().HasKeyValueStore();
+  }
+
+  void OrthancPlugins::ApplyStoreKeyValue(const _OrthancPluginStoreKeyValue& parameters)
+  {
+    PImpl::ServerContextReference lock(*pimpl_);
+    std::string value(parameters.value, parameters.valueSize);
+
+    lock.GetContext().GetIndex().StoreKeyValue(parameters.pluginIdentifier, parameters.key, value);
+  }
+
+  void OrthancPlugins::ApplyDeleteKeyValue(const _OrthancPluginDeleteKeyValue& parameters)
+  {
+    PImpl::ServerContextReference lock(*pimpl_);
+
+    lock.GetContext().GetIndex().DeleteKeyValue(parameters.pluginIdentifier, parameters.key);
+  }
+
+  bool OrthancPlugins::ApplyGetKeyValue(const _OrthancPluginGetKeyValue& parameters)
+  {
+    PImpl::ServerContextReference lock(*pimpl_);
+
+    std::string value;
+
+    if (lock.GetContext().GetIndex().GetKeyValue(value, parameters.pluginIdentifier, parameters.key))
+    {
+      CopyToMemoryBuffer(*parameters.value, value.size() > 0 ? value.c_str() : NULL, value.size());
+      return true;
+    }
+    else
+    {
+      return false;
     }
   }
 
@@ -5755,6 +5796,54 @@
         return true;
       }
 
+      case _OrthancPluginService_StoreKeyValue:
+      {
+        if (!HasKeyValueStore())
+        {
+          LOG(ERROR) << "The DB engine does not support Key Value Store";
+          return false;
+        }
+
+        const _OrthancPluginStoreKeyValue& p =
+          *reinterpret_cast<const _OrthancPluginStoreKeyValue*>(parameters);
+        ApplyStoreKeyValue(p);
+        return true;
+      }
+
+      case _OrthancPluginService_DeleteKeyValue:
+      {
+        if (!HasKeyValueStore())
+        {
+          LOG(ERROR) << "The DB engine does not support Key Value Store";
+          return false;
+        }
+
+        const _OrthancPluginDeleteKeyValue& p =
+          *reinterpret_cast<const _OrthancPluginDeleteKeyValue*>(parameters);
+        ApplyDeleteKeyValue(p);
+        return true;
+      }
+
+      case _OrthancPluginService_GetKeyValue:
+      {
+        if (!HasKeyValueStore())
+        {
+          LOG(ERROR) << "The DB engine does not support Key Value Store";
+          return false;
+        }
+
+        const _OrthancPluginGetKeyValue& p =
+          *reinterpret_cast<const _OrthancPluginGetKeyValue*>(parameters);
+        if (ApplyGetKeyValue(p))
+        {
+          return true;
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_UnknownResource);
+        }
+      }
+
       default:
         return false;
     }
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.h	Mon Apr 07 13:23:11 2025 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPlugins.h	Wed May 07 19:27:06 2025 +0200
@@ -225,6 +225,14 @@
 
     void ApplyAdoptAttachment(const _OrthancPluginAdoptAttachment& parameters);
 
+    bool HasKeyValueStore();
+
+    void ApplyStoreKeyValue(const _OrthancPluginStoreKeyValue& parameters);
+
+    void ApplyDeleteKeyValue(const _OrthancPluginDeleteKeyValue& parameters);
+
+    bool ApplyGetKeyValue(const _OrthancPluginGetKeyValue& parameters);
+
     void ComputeHash(_OrthancPluginService service,
                      const void* parameters);
 
--- a/OrthancServer/Plugins/Engine/PluginsEnumerations.cpp	Mon Apr 07 13:23:11 2025 +0200
+++ b/OrthancServer/Plugins/Engine/PluginsEnumerations.cpp	Wed May 07 19:27:06 2025 +0200
@@ -696,5 +696,54 @@
       }
     }
 
+    OrthancPluginStoreStatus Convert(StoreStatus status)
+    {
+      switch (status)
+      {
+        case StoreStatus_Success:
+          return OrthancPluginStoreStatus_Success;
+
+        case StoreStatus_AlreadyStored:
+          return OrthancPluginStoreStatus_AlreadyStored;
+
+        case StoreStatus_Failure:
+          return OrthancPluginStoreStatus_Failure;
+
+        case StoreStatus_FilteredOut:
+          return OrthancPluginStoreStatus_FilteredOut;
+
+        case StoreStatus_StorageFull:
+          return OrthancPluginStoreStatus_StorageFull;
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+
+
+    StoreStatus Convert(OrthancPluginStoreStatus status)
+    {
+      switch (status)
+      {
+        case OrthancPluginStoreStatus_Success:
+          return StoreStatus_Success;
+
+        case OrthancPluginStoreStatus_AlreadyStored:
+          return StoreStatus_AlreadyStored;
+
+        case OrthancPluginStoreStatus_Failure:
+          return StoreStatus_Failure;
+
+        case OrthancPluginStoreStatus_FilteredOut:
+          return StoreStatus_FilteredOut;
+
+        case OrthancPluginStoreStatus_StorageFull:
+          return StoreStatus_StorageFull;
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+
   }
 }
--- a/OrthancServer/Plugins/Engine/PluginsEnumerations.h	Mon Apr 07 13:23:11 2025 +0200
+++ b/OrthancServer/Plugins/Engine/PluginsEnumerations.h	Wed May 07 19:27:06 2025 +0200
@@ -77,6 +77,10 @@
     OrthancPluginCompressionType Convert(CompressionType type);
 
     CompressionType Convert(OrthancPluginCompressionType type);
+
+    OrthancPluginStoreStatus Convert(StoreStatus type);
+
+    StoreStatus Convert(OrthancPluginStoreStatus type);
   }
 }
 
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Mon Apr 07 13:23:11 2025 +0200
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Wed May 07 19:27:06 2025 +0200
@@ -469,7 +469,10 @@
     _OrthancPluginService_SetMetricsIntegerValue = 43,              /* New in Orthanc 1.12.1 */
     _OrthancPluginService_SetCurrentThreadName = 44,                /* New in Orthanc 1.12.2 */
     _OrthancPluginService_LogMessage = 45,                          /* New in Orthanc 1.12.4 */
-    _OrthancPluginService_AdoptAttachment = 46,                     /* New in Orthanc 99 */
+    _OrthancPluginService_AdoptAttachment = 46,                     /* New in Orthanc 1.12.99 */
+    _OrthancPluginService_StoreKeyValue = 47,                       /* New in Orthanc 1.12.99 */
+    _OrthancPluginService_DeleteKeyValue = 48,                      /* New in Orthanc 1.12.99 */
+    _OrthancPluginService_GetKeyValue = 49,                         /* New in Orthanc 1.12.99 */
 
 
     /* Registration of callbacks */
@@ -1133,6 +1136,18 @@
 
 
   /**
+   * The store status response to AdoptAttachment.
+   **/
+  typedef enum
+  {
+    OrthancPluginStoreStatus_Success = 0,         /*!< The file has been stored/adopted */
+    OrthancPluginStoreStatus_AlreadyStored = 1,   /*!< The file has already been stored/adopted (only if OverwriteInstances is set to false)*/
+    OrthancPluginStoreStatus_Failure = 2,         /*!< The file could not be stored/adopted */
+    OrthancPluginStoreStatus_FilteredOut = 3,     /*!< The file has been filtered out by a lua script or a plugin */
+    OrthancPluginStoreStatus_StorageFull = 4      /*!< The storage is full (only if MaximumStorageSize/MaximumPatientCount is set and MaximumStorageMode is Reject)*/
+  } OrthancPluginStoreStatus;
+
+  /**
    * @brief A 32-bit memory buffer allocated by the core system of Orthanc.
    *
    * A memory buffer allocated by the core system of Orthanc. When the
@@ -9772,6 +9787,7 @@
     const char*                   attachToResourceId; /* in */
     OrthancPluginMemoryBuffer*    createdResourceId; /* out */
     OrthancPluginMemoryBuffer*    attachmentUuid;    /* out */
+    OrthancPluginStoreStatus*     storeStatus;       /* out */
   } _OrthancPluginAdoptAttachment;
   
   /**
@@ -9789,7 +9805,9 @@
     OrthancPluginResourceType     attachToResourceType,
     const char*                   attachToResourceId,
     OrthancPluginMemoryBuffer*    createdResourceId, /* out */
-    OrthancPluginMemoryBuffer*    attachmentUuid) /* out */
+    OrthancPluginMemoryBuffer*    attachmentUuid, /* out */
+    OrthancPluginStoreStatus*     storeStatus /* out */
+  ) 
   {
     _OrthancPluginAdoptAttachment params;
     params.buffer = buffer;
@@ -9800,10 +9818,92 @@
     params.attachToResourceId = attachToResourceId;
     params.createdResourceId = createdResourceId;
     params.attachmentUuid = attachmentUuid;
+    params.storeStatus = storeStatus;
 
     return context->InvokeService(context, _OrthancPluginService_AdoptAttachment, &params);
   }
 
+  typedef struct
+  {
+    const char*                   pluginIdentifier;
+    const char*                   key;
+    const char*                   value;
+    uint64_t                      valueSize;
+  } _OrthancPluginStoreKeyValue;
+  
+  /**
+   * @brief Tell Orthanc to store a key-value in its store.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+TODO_ATTACH_CUSTOM_DATA TODO TODO
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginStoreKeyValue(
+    OrthancPluginContext*         context,
+    const char*                   pluginIdentifier, /* in */
+    const char*                   key, /* in */
+    const char*                   value, /* in */
+    uint64_t                      valueSize /* in */)
+  {
+    _OrthancPluginStoreKeyValue params;
+    params.pluginIdentifier = pluginIdentifier;
+    params.key = key;
+    params.value = value;
+    params.valueSize = valueSize;
+
+    return context->InvokeService(context, _OrthancPluginService_StoreKeyValue, &params);
+  }
+
+  typedef struct
+  {
+    const char*                   pluginIdentifier;
+    const char*                   key;
+  } _OrthancPluginDeleteKeyValue;
+  
+  /**
+   * @brief Tell Orthanc to delete a key-value from its store.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+TODO_ATTACH_CUSTOM_DATA TODO TODO
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginDeleteKeyValue(
+    OrthancPluginContext*         context,
+    const char*                   pluginIdentifier, /* in */
+    const char*                   key /* in */)
+  {
+    _OrthancPluginDeleteKeyValue params;
+    params.pluginIdentifier = pluginIdentifier;
+    params.key = key;
+
+    return context->InvokeService(context, _OrthancPluginService_DeleteKeyValue, &params);
+  }
+
+  typedef struct
+  {
+    const char*                   pluginIdentifier;
+    const char*                   key;
+    OrthancPluginMemoryBuffer*    value;
+  } _OrthancPluginGetKeyValue;
+  
+  /**
+   * @brief Get the value associated to this key in the key-value store.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+TODO_ATTACH_CUSTOM_DATA TODO TODO
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginGetKeyValue(
+    OrthancPluginContext*         context,
+    const char*                   pluginIdentifier, /* in */
+    const char*                   key, /* in */
+    OrthancPluginMemoryBuffer*    value /* out */)
+  {
+    _OrthancPluginGetKeyValue params;
+    params.pluginIdentifier = pluginIdentifier;
+    params.key = key;
+    params.value = value;
+
+    return context->InvokeService(context, _OrthancPluginService_GetKeyValue, &params);
+  }
+
 #ifdef  __cplusplus
 }
 #endif
--- a/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto	Mon Apr 07 13:23:11 2025 +0200
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto	Wed May 07 19:27:06 2025 +0200
@@ -167,6 +167,7 @@
     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
+    bool has_key_value_store = 10;  // New in Orthanc 1.12.99
   }
 }
 
@@ -322,6 +323,9 @@
   OPERATION_FIND = 50;                        // New in Orthanc 1.12.5
   OPERATION_GET_CHANGES_EXTENDED = 51;        // New in Orthanc 1.12.5
   OPERATION_COUNT_RESOURCES = 52;             // New in Orthanc 1.12.5
+  OPERATION_STORE_KEY_VALUE = 53;             // New in Orthanc 1.12.99
+  OPERATION_DELETE_KEY_VALUE = 54;            // New in Orthanc 1.12.99
+  OPERATION_GET_KEY_VALUE = 55;               // New in Orthanc 1.12.99
 }
 
 message Rollback {
@@ -975,6 +979,39 @@
   }
 }
 
+message StoreKeyValue {
+  message Request {
+    string plugin_id = 1;
+    string key = 2;
+    string value = 3;
+  }
+
+  message Response {
+  }
+}
+
+message DeleteKeyValue {
+  message Request {
+    string plugin_id = 1;
+    string key = 2;
+  }
+
+  message Response {
+  }
+}
+
+message GetKeyValue {
+  message Request {
+    string plugin_id = 1;
+    string key = 2;
+  }
+
+  message Response {
+    string value = 1;
+  }
+}
+
+
 message TransactionRequest {
   sfixed64              transaction = 1;
   TransactionOperation  operation = 2;
@@ -1032,6 +1069,9 @@
   Find.Request                            find = 150;
   GetChangesExtended.Request              get_changes_extended = 151;
   Find.Request                            count_resources = 152;
+  StoreKeyValue.Request                   store_key_value = 153;
+  DeleteKeyValue.Request                  delete_key_value = 154;
+  GetKeyValue.Request                     get_key_value = 155;
 }
 
 message TransactionResponse {
@@ -1088,6 +1128,9 @@
   repeated Find.Response                   find = 150;   // One message per found resource
   GetChangesExtended.Response              get_changes_extended = 151;
   CountResources.Response                  count_resources = 152;
+  StoreKeyValue.Response                   store_key_value = 153;
+  DeleteKeyValue.Response                  delete_key_value = 154;
+  GetKeyValue.Response                     get_key_value = 155;
 }
 
 enum RequestType {
--- a/OrthancServer/Sources/Database/IDatabaseWrapper.h	Mon Apr 07 13:23:11 2025 +0200
+++ b/OrthancServer/Sources/Database/IDatabaseWrapper.h	Wed May 07 19:27:06 2025 +0200
@@ -57,6 +57,7 @@
       bool hasFindSupport_;
       bool hasExtendedChanges_;
       bool hasAttachmentCustomDataSupport_;
+      bool hasKeyValueStore_;
 
     public:
       Capabilities() :
@@ -68,7 +69,8 @@
         hasMeasureLatency_(false),
         hasFindSupport_(false),
         hasExtendedChanges_(false),
-        hasAttachmentCustomDataSupport_(false)
+        hasAttachmentCustomDataSupport_(false),
+        hasKeyValueStore_(false)
       {
       }
 
@@ -161,6 +163,16 @@
       {
         return hasFindSupport_;
       }
+
+      void SetHasKeyValueStore(bool value)
+      {
+        hasKeyValueStore_ = value;
+      }
+
+      bool HasKeyValueStore() const
+      {
+        return hasKeyValueStore_;
+      }
     };
 
 
@@ -402,6 +414,20 @@
                                       int64_t to,
                                       uint32_t limit,
                                       const std::set<ChangeType>& filterType) = 0;
+
+      // New in Orthanc 1.12.99
+      virtual void StoreKeyValue(const std::string& pluginId,
+                                 const std::string& key,
+                                 const std::string& value) = 0;
+
+      // New in Orthanc 1.12.99
+      virtual void DeleteKeyValue(const std::string& pluginId,
+                                  const std::string& key) = 0;
+
+      // New in Orthanc 1.12.99
+      virtual bool GetKeyValue(std::string& value,
+                               const std::string& pluginId,
+                               const std::string& key) = 0;
     };
 
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/InstallKeyValueStore.sql	Wed May 07 19:27:06 2025 +0200
@@ -0,0 +1,29 @@
+-- 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 General Public License as
+-- published by the Free Software Foundation, either version 3 of the
+-- License, or (at your option) any later version.
+--
+-- This program is distributed in the hope that it will be useful, but
+-- WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+-- General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+CREATE TABLE KeyValueStore(
+       pluginId TEXT NOT NULL,
+       key TEXT NOT NULL,
+       value TEXT NOT NULL,
+       PRIMARY KEY(pluginId, key)  -- Prevents duplicates
+       );
+
+CREATE INDEX KeyValueStoreIndex ON KeyValueStore (pluginId, key);
--- a/OrthancServer/Sources/Database/PrepareDatabase.sql	Mon Apr 07 13:23:11 2025 +0200
+++ b/OrthancServer/Sources/Database/PrepareDatabase.sql	Wed May 07 19:27:06 2025 +0200
@@ -159,6 +159,16 @@
   INSERT INTO PatientRecyclingOrder VALUES (NULL, new.internalId);
 END;
 
+-- new in Orthanc 1.12.99
+CREATE TABLE KeyValueStore(
+       pluginId TEXT NOT NULL,
+       key TEXT NOT NULL,
+       value TEXT NOT NULL,
+       PRIMARY KEY(pluginId, key)  -- Prevents duplicates
+       );
+
+-- new in Orthanc 1.12.99
+CREATE INDEX KeyValueStoreIndex ON KeyValueStore (pluginId, key);
 
 -- Set the version of the database schema
 -- The "1" corresponds to the "GlobalProperty_DatabaseSchemaVersion" enumeration
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp	Mon Apr 07 13:23:11 2025 +0200
+++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp	Wed May 07 19:27:06 2025 +0200
@@ -2102,6 +2102,48 @@
         target.insert(s.ColumnString(0));
       }
     }
+
+    virtual void StoreKeyValue(const std::string& pluginId,
+                               const std::string& key,
+                               const std::string& value) ORTHANC_OVERRIDE
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO KeyValueStore (pluginId, key, value) VALUES(?, ?, ?)");
+      s.BindString(0, pluginId);
+      s.BindString(1, key);
+      s.BindString(2, value);
+      s.Run();
+    }
+
+    virtual void DeleteKeyValue(const std::string& pluginId,
+                                const std::string& key) ORTHANC_OVERRIDE
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM KeyValueStore WHERE pluginId = ? AND key = ?");
+      s.BindString(0, pluginId);
+      s.BindString(1, key);
+      s.Run();
+    }
+
+    virtual bool GetKeyValue(std::string& value,
+                             const std::string& pluginId,
+                             const std::string& key) ORTHANC_OVERRIDE
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                          "SELECT value FROM KeyValueStore WHERE pluginId=? AND key=?");
+      s.BindString(0, pluginId);
+      s.BindString(1, key);
+
+      if (!s.Step())
+      {
+        // No value found
+        return false;
+      }
+      else
+      {
+        value = s.ColumnString(0);
+        return true;
+      }    
+    }
+
   };
 
 
@@ -2296,11 +2338,12 @@
     signalRemainingAncestor_(NULL),
     version_(0)
   {
-    // TODO: implement revisions in SQLite
+    dbCapabilities_.SetRevisionsSupport(true);
     dbCapabilities_.SetFlushToDisk(true);
     dbCapabilities_.SetLabelsSupport(true);
     dbCapabilities_.SetHasExtendedChanges(true);
     dbCapabilities_.SetHasFindSupport(HasIntegratedFind());
+    dbCapabilities_.SetHasKeyValueStore(true);
     db_.Open(path);
   }
 
@@ -2310,11 +2353,12 @@
     signalRemainingAncestor_(NULL),
     version_(0)
   {
-    // TODO: implement revisions in SQLite
+    dbCapabilities_.SetRevisionsSupport(true);
     dbCapabilities_.SetFlushToDisk(true);
     dbCapabilities_.SetLabelsSupport(true);
     dbCapabilities_.SetHasExtendedChanges(true);
     dbCapabilities_.SetHasFindSupport(HasIntegratedFind());
+    dbCapabilities_.SetHasKeyValueStore(true);
     db_.OpenInMemory();
   }
 
@@ -2412,11 +2456,8 @@
           ServerResources::GetFileResource(query, ServerResources::INSTALL_LABELS_TABLE);
           db_.Execute(query);
         }
-      }
 
-      // New in Orthanc 1.12.99
-      if (version_ >= 6)
-      {
+        // New in Orthanc 1.12.99
         if (!transaction->LookupGlobalProperty(tmp, GlobalProperty_SQLiteHasCustomDataAndRevision, true /* unused in SQLite */) 
             || tmp != "1")
         {
@@ -2425,6 +2466,14 @@
           ServerResources::GetFileResource(query, ServerResources::INSTALL_REVISION_AND_CUSTOM_DATA);
           db_.Execute(query);
         }
+
+        if (!db_.DoesTableExist("KeyValueStore"))
+        {
+          LOG(INFO) << "Installing the \"KeyValueStore\" table";
+          std::string query;
+          ServerResources::GetFileResource(query, ServerResources::INSTALL_KEY_VALUE_STORE);
+          db_.Execute(query);
+        }
       }
 
       transaction->Commit(0);
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Mon Apr 07 13:23:11 2025 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Wed May 07 19:27:06 2025 +0200
@@ -3200,6 +3200,12 @@
     return db_.GetDatabaseCapabilities().HasFindSupport();
   }
 
+  bool StatelessDatabaseOperations::HasKeyValueStore()
+  {
+    boost::shared_lock<boost::shared_mutex> lock(mutex_);
+    return db_.GetDatabaseCapabilities().HasKeyValueStore();
+  }
+
   void StatelessDatabaseOperations::ExecuteCount(uint64_t& count,
                                                  const FindRequest& request)
   {
@@ -3320,4 +3326,94 @@
       }
     }
   }
+
+  void StatelessDatabaseOperations::StoreKeyValue(const std::string& pluginId,
+                                                  const std::string& key,
+                                                  const std::string& value)
+  {
+    class Operations : public IReadWriteOperations
+    {
+    private:
+      const std::string& pluginId_;
+      const std::string& key_;
+      const std::string& value_;
+
+    public:
+      Operations(const std::string& pluginId,
+                 const std::string& key,
+                 const std::string& value) :
+        pluginId_(pluginId),
+        key_(key),
+        value_(value)
+      {
+      }
+
+      virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE
+      {
+        transaction.StoreKeyValue(pluginId_, key_, value_);
+      }
+    };
+
+    Operations operations(pluginId, key, value);
+    Apply(operations);
+  }
+
+  void StatelessDatabaseOperations::DeleteKeyValue(const std::string& pluginId,
+                                                   const std::string& key)
+  {
+    class Operations : public IReadWriteOperations
+    {
+    private:
+      const std::string& pluginId_;
+      const std::string& key_;
+
+    public:
+      Operations(const std::string& pluginId,
+                 const std::string& key) :
+        pluginId_(pluginId),
+        key_(key)
+      {
+      }
+
+      virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE
+      {
+        transaction.DeleteKeyValue(pluginId_, key_);
+      }
+    };
+
+    Operations operations(pluginId, key);
+    Apply(operations);
+  }
+
+  bool StatelessDatabaseOperations::GetKeyValue(std::string& value,
+                                                const std::string& pluginId,
+                                                const std::string& key)
+  {
+    class Operations : public ReadOnlyOperationsT3<std::string&, const std::string&, const std::string& >
+    {
+      bool found_;
+    public:
+      Operations():
+        found_(false)
+      {}
+
+      bool HasFound()
+      {
+        return found_;
+      }
+
+      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
+                              const Tuple& tuple) ORTHANC_OVERRIDE
+      {
+        found_ = transaction.GetKeyValue(tuple.get<0>(), tuple.get<1>(), tuple.get<2>());
+      }
+    };
+
+    Operations operations;
+    operations.Apply(*this, value, pluginId, key);
+
+    return operations.HasFound();
+  }
+
+
 }
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Mon Apr 07 13:23:11 2025 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Wed May 07 19:27:06 2025 +0200
@@ -293,6 +293,14 @@
       {
         transaction_.ExecuteExpand(response, capabilities, request, identifier);
       }
+
+      bool GetKeyValue(std::string& value,
+                       const std::string& pluginId,
+                       const std::string& key)
+      {
+        return transaction_.GetKeyValue(value, pluginId, key);
+      }
+
     };
 
 
@@ -428,6 +436,20 @@
       {
         transaction_.RemoveLabel(id, label);
       }
+
+      void StoreKeyValue(const std::string& pluginId,
+                         const std::string& key,
+                         const std::string& value)
+      {
+        transaction_.StoreKeyValue(pluginId, key, value);
+      }
+
+      void DeleteKeyValue(const std::string& pluginId,
+                          const std::string& key)
+      {
+        transaction_.DeleteKeyValue(pluginId, key);
+      }
+
     };
 
 
@@ -544,6 +566,8 @@
     bool HasExtendedChanges();
 
     bool HasFindSupport();
+
+    bool HasKeyValueStore();
     
     void GetExportedResources(Json::Value& target,
                               int64_t since,
@@ -724,5 +748,17 @@
 
     void ExecuteCount(uint64_t& count,
                       const FindRequest& request);
+
+    void StoreKeyValue(const std::string& pluginId,
+                       const std::string& key,
+                       const std::string& value);
+
+    void DeleteKeyValue(const std::string& pluginId,
+                        const std::string& key);
+
+    bool GetKeyValue(std::string& value,
+                     const std::string& pluginId,
+                     const std::string& key);
+
   };
 }
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp	Mon Apr 07 13:23:11 2025 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp	Wed May 07 19:27:06 2025 +0200
@@ -95,6 +95,7 @@
     static const char* const HAS_LABELS = "HasLabels";
     static const char* const CAPABILITIES = "Capabilities";
     static const char* const HAS_EXTENDED_CHANGES = "HasExtendedChanges";
+    static const char* const HAS_KEY_VALUE_STORE = "HasKeyValueStore";
     static const char* const HAS_EXTENDED_FIND = "HasExtendedFind";
     static const char* const READ_ONLY = "ReadOnly";
 
@@ -211,6 +212,7 @@
     result[CAPABILITIES] = Json::objectValue;
     result[CAPABILITIES][HAS_EXTENDED_CHANGES] = OrthancRestApi::GetIndex(call).HasExtendedChanges();
     result[CAPABILITIES][HAS_EXTENDED_FIND] = OrthancRestApi::GetIndex(call).HasFindSupport();
+    result[CAPABILITIES][HAS_KEY_VALUE_STORE] = OrthancRestApi::GetIndex(call).HasKeyValueStore();
     
     call.GetOutput().AnswerJson(result);
   }