Mercurial > hg > orthanc-databases
changeset 690:e4cf08a87f8c
integration attach-custom-data->mainline
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Fri, 13 Jun 2025 15:16:52 +0200 |
parents | 1975d5f63b34 (current diff) 431aab517a46 (diff) |
children | 540918f7417b |
files | |
diffstat | 56 files changed, 1968 insertions(+), 491 deletions(-) [+] |
line wrap: on
line diff
--- a/Framework/Common/BinaryStringValue.cpp Mon May 19 11:18:43 2025 +0200 +++ b/Framework/Common/BinaryStringValue.cpp Fri Jun 13 15:16:52 2025 +0200 @@ -31,6 +31,20 @@ namespace OrthancDatabases { + BinaryStringValue::BinaryStringValue(const void* data, + size_t size) + { + if (data == NULL && size > 0) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + else + { + content_.assign(reinterpret_cast<const char*>(data), size); + } + } + + IValue* BinaryStringValue::Convert(ValueType target) const { switch (target)
--- a/Framework/Common/BinaryStringValue.h Mon May 19 11:18:43 2025 +0200 +++ b/Framework/Common/BinaryStringValue.h Fri Jun 13 15:16:52 2025 +0200 @@ -35,11 +35,18 @@ std::string content_; public: + BinaryStringValue() + { + } + explicit BinaryStringValue(const std::string& content) : content_(content) { } + BinaryStringValue(const void* data, + size_t size); + const std::string& GetContent() const { return content_; @@ -55,6 +62,11 @@ return content_.size(); } + void Swap(std::string& other) + { + content_.swap(other); + } + virtual ValueType GetType() const ORTHANC_OVERRIDE { return ValueType_BinaryString;
--- a/Framework/Common/DatabaseManager.cpp Mon May 19 11:18:43 2025 +0200 +++ b/Framework/Common/DatabaseManager.cpp Fri Jun 13 15:16:52 2025 +0200 @@ -99,7 +99,7 @@ const Query& query) { LOG(TRACE) << "Caching statement from " << statementId.GetFile() << ":" << statementId.GetLine() << "" << statementId.GetDynamicStatement(); - + std::unique_ptr<IPrecompiledStatement> statement(GetDatabase().Compile(query)); IPrecompiledStatement* tmp = statement.get(); @@ -394,6 +394,17 @@ } + void DatabaseManager::StatementBase::SetParametersTypes(const Dictionary& parameters) + { + Dictionary::Types parametersTypes; + parameters.GetParametersType(parametersTypes); + + for (Dictionary::Types::const_iterator it = parametersTypes.begin(); it != parametersTypes.end(); ++it) + { + SetParameterType(it->first, it->second); + } + } + void DatabaseManager::StatementBase::SetParameterType(const std::string& parameter, ValueType type) { @@ -550,6 +561,18 @@ } } + std::string DatabaseManager::StatementBase::ReadStringOrNull(size_t field) const + { + if (IsNull(field)) + { + return std::string(); + } + else + { + return ReadString(field); + } + } + DatabaseManager::CachedStatement::CachedStatement(const StatementId& statementId, DatabaseManager& manager, const std::string& sql) : @@ -589,6 +612,9 @@ void DatabaseManager::CachedStatement::ExecuteInternal(const Dictionary& parameters, bool withResults) { + // the query parameters_ type can not be trusted (they are all Utf8String by default), we need a parameters dico with the actual types + SetParametersTypes(parameters); + try { std::unique_ptr<Query> query(ReleaseQuery()); @@ -597,7 +623,7 @@ { // Register the newly-created statement assert(statement_ == NULL); - statement_ = &GetManager().CacheStatement(statementId_, *query); + statement_ = &GetManager().CacheStatement(statementId_, *query); } assert(statement_ != NULL); @@ -679,6 +705,9 @@ void DatabaseManager::StandaloneStatement::ExecuteInternal(const Dictionary& parameters, bool withResults) { + // the query parameters_ type can not be trusted (they are all Utf8String by default), we need a parameters dico with the actual types + SetParametersTypes(parameters); + try { std::unique_ptr<Query> query(ReleaseQuery());
--- a/Framework/Common/DatabaseManager.h Mon May 19 11:18:43 2025 +0200 +++ b/Framework/Common/DatabaseManager.h Fri Jun 13 15:16:52 2025 +0200 @@ -155,6 +155,8 @@ return query_.release(); } + void SetParametersTypes(const Dictionary& parameters); + public: explicit StatementBase(DatabaseManager& manager); @@ -188,6 +190,8 @@ std::string ReadString(size_t field) const; + std::string ReadStringOrNull(size_t field) const; + bool IsNull(size_t field) const; void PrintResult(std::ostream& stream)
--- a/Framework/Common/Dictionary.cpp Mon May 19 11:18:43 2025 +0200 +++ b/Framework/Common/Dictionary.cpp Fri Jun 13 15:16:52 2025 +0200 @@ -106,6 +106,14 @@ } + void Dictionary::SetBinaryValue(const std::string& key, + const void* data, + size_t size) + { + SetValue(key, new BinaryStringValue(data, size)); + } + + void Dictionary::SetFileValue(const std::string& key, const std::string& file) { @@ -134,9 +142,9 @@ SetValue(key, new Integer32Value(value)); } - void Dictionary::SetNullValue(const std::string& key) + void Dictionary::SetUtf8NullValue(const std::string& key) { - SetValue(key, new NullValue); + SetValue(key, new Utf8StringValue()); }
--- a/Framework/Common/Dictionary.h Mon May 19 11:18:43 2025 +0200 +++ b/Framework/Common/Dictionary.h Fri Jun 13 15:16:52 2025 +0200 @@ -33,6 +33,8 @@ { class Dictionary : public boost::noncopyable { + public: + typedef std::map<std::string, ValueType> Types; private: typedef std::map<std::string, IValue*> Values; @@ -56,9 +58,15 @@ void SetUtf8Value(const std::string& key, const std::string& utf8); + void SetUtf8NullValue(const std::string& key); + void SetBinaryValue(const std::string& key, const std::string& binary); + void SetBinaryValue(const std::string& key, + const void* data, + size_t size); + void SetFileValue(const std::string& key, const std::string& file); @@ -72,10 +80,10 @@ void SetInteger32Value(const std::string& key, int32_t value); - void SetNullValue(const std::string& key); - const IValue& GetValue(const std::string& key) const; void GetParametersType(Query::Parameters& target) const; + + void GetParametersTypes(Types& target) const; }; }
--- a/Framework/Common/IValue.h Mon May 19 11:18:43 2025 +0200 +++ b/Framework/Common/IValue.h Fri Jun 13 15:16:52 2025 +0200 @@ -40,5 +40,10 @@ virtual ValueType GetType() const = 0; virtual IValue* Convert(ValueType target) const = 0; + + virtual bool IsNull() const // TODO: right now, only the Utf8StringValue implements nullable values + { + return false; + } }; }
--- a/Framework/Common/Utf8StringValue.cpp Mon May 19 11:18:43 2025 +0200 +++ b/Framework/Common/Utf8StringValue.cpp Fri Jun 13 15:16:52 2025 +0200 @@ -57,7 +57,14 @@ break; case ValueType_Utf8String: - return new Utf8StringValue(utf8_); + if (IsNull()) + { + return new Utf8StringValue(); + } + else + { + return new Utf8StringValue(utf8_); + } default: throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
--- a/Framework/Common/Utf8StringValue.h Mon May 19 11:18:43 2025 +0200 +++ b/Framework/Common/Utf8StringValue.h Fri Jun 13 15:16:52 2025 +0200 @@ -34,13 +34,25 @@ { private: std::string utf8_; + bool isNull_; public: explicit Utf8StringValue(const std::string& utf8) : - utf8_(utf8) + utf8_(utf8), + isNull_(false) { } + explicit Utf8StringValue() : + isNull_(true) + { + } + + virtual bool IsNull() const ORTHANC_OVERRIDE + { + return isNull_; + } + const std::string& GetContent() const { return utf8_;
--- a/Framework/MySQL/MySQLStatement.cpp Mon May 19 11:18:43 2025 +0200 +++ b/Framework/MySQL/MySQLStatement.cpp Fri Jun 13 15:16:52 2025 +0200 @@ -468,62 +468,64 @@ throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentItem); } - ValueType type = formatter_.GetParameterType(i); - const IValue& value = parameters.GetValue(name); - if (value.GetType() != type) - { - LOG(ERROR) << "Bad type of argument provided to a SQL query: " << name; - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadParameterType); - } - // https://dev.mysql.com/doc/refman/8.0/en/c-api-prepared-statement-type-codes.html - switch (type) + if (value.GetType() == ValueType_Null) { - case ValueType_Integer64: + inputs[i].buffer = NULL; + inputs[i].buffer_type = MYSQL_TYPE_NULL; + } + else + { + ValueType type = formatter_.GetParameterType(i); + + if (value.GetType() != type) { - int64Parameters.push_back(dynamic_cast<const Integer64Value&>(value).GetValue()); - inputs[i].buffer = &int64Parameters.back(); - inputs[i].buffer_type = MYSQL_TYPE_LONGLONG; - break; + LOG(ERROR) << "Bad type of argument provided to a SQL query: " << name; + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadParameterType); } - case ValueType_Utf8String: + // https://dev.mysql.com/doc/refman/8.0/en/c-api-prepared-statement-type-codes.html + switch (type) { - const std::string& utf8 = dynamic_cast<const Utf8StringValue&>(value).GetContent(); - inputs[i].buffer = const_cast<char*>(utf8.c_str()); - inputs[i].buffer_length = utf8.size(); - inputs[i].buffer_type = MYSQL_TYPE_STRING; - break; - } + case ValueType_Integer64: + { + int64Parameters.push_back(dynamic_cast<const Integer64Value&>(value).GetValue()); + inputs[i].buffer = &int64Parameters.back(); + inputs[i].buffer_type = MYSQL_TYPE_LONGLONG; + break; + } - case ValueType_BinaryString: - { - const std::string& content = dynamic_cast<const BinaryStringValue&>(value).GetContent(); - inputs[i].buffer = const_cast<char*>(content.c_str()); - inputs[i].buffer_length = content.size(); - inputs[i].buffer_type = MYSQL_TYPE_BLOB; - break; - } + case ValueType_Utf8String: + { + const std::string& utf8 = dynamic_cast<const Utf8StringValue&>(value).GetContent(); + inputs[i].buffer = const_cast<char*>(utf8.c_str()); + inputs[i].buffer_length = utf8.size(); + inputs[i].buffer_type = MYSQL_TYPE_STRING; + break; + } - case ValueType_InputFile: - { - const std::string& content = dynamic_cast<const InputFileValue&>(value).GetContent(); - inputs[i].buffer = const_cast<char*>(content.c_str()); - inputs[i].buffer_length = content.size(); - inputs[i].buffer_type = MYSQL_TYPE_BLOB; - break; - } + case ValueType_BinaryString: + { + const std::string& content = dynamic_cast<const BinaryStringValue&>(value).GetContent(); + inputs[i].buffer = const_cast<char*>(content.c_str()); + inputs[i].buffer_length = content.size(); + inputs[i].buffer_type = MYSQL_TYPE_BLOB; + break; + } - case ValueType_Null: - { - inputs[i].buffer = NULL; - inputs[i].buffer_type = MYSQL_TYPE_NULL; - break; + case ValueType_InputFile: + { + const std::string& content = dynamic_cast<const InputFileValue&>(value).GetContent(); + inputs[i].buffer = const_cast<char*>(content.c_str()); + inputs[i].buffer_length = content.size(); + inputs[i].buffer_type = MYSQL_TYPE_BLOB; + break; + } + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); } - - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); } }
--- a/Framework/Plugins/DatabaseBackendAdapterV2.cpp Mon May 19 11:18:43 2025 +0200 +++ b/Framework/Plugins/DatabaseBackendAdapterV2.cpp Fri Jun 13 15:16:52 2025 +0200 @@ -187,7 +187,8 @@ const std::string& uncompressedHash, int32_t compressionType, uint64_t compressedSize, - const std::string& compressedHash) ORTHANC_OVERRIDE + const std::string& compressedHash, + const std::string& /*customData*/) ORTHANC_OVERRIDE { OrthancPluginAttachment attachment; attachment.uuid = uuid.c_str(); @@ -219,7 +220,8 @@ const std::string& uncompressedHash, int32_t compressionType, uint64_t compressedSize, - const std::string& compressedHash) ORTHANC_OVERRIDE + const std::string& compressedHash, + const std::string& /*customData*/) ORTHANC_OVERRIDE { if (allowedAnswers_ != AllowedAnswers_All && allowedAnswers_ != AllowedAnswers_Attachment)
--- a/Framework/Plugins/DatabaseBackendAdapterV3.cpp Mon May 19 11:18:43 2025 +0200 +++ b/Framework/Plugins/DatabaseBackendAdapterV3.cpp Fri Jun 13 15:16:52 2025 +0200 @@ -421,7 +421,8 @@ const std::string& uncompressedHash, int32_t compressionType, uint64_t compressedSize, - const std::string& compressedHash) ORTHANC_OVERRIDE + const std::string& compressedHash, + const std::string& /*customData*/) ORTHANC_OVERRIDE { OrthancPluginDatabaseEvent event; event.type = OrthancPluginDatabaseEventType_DeletedAttachment; @@ -467,7 +468,8 @@ const std::string& uncompressedHash, int32_t compressionType, uint64_t compressedSize, - const std::string& compressedHash) ORTHANC_OVERRIDE + const std::string& compressedHash, + const std::string& /*customData*/) ORTHANC_OVERRIDE { SetupAnswerType(_OrthancPluginDatabaseAnswerType_Attachment);
--- a/Framework/Plugins/DatabaseBackendAdapterV4.cpp Mon May 19 11:18:43 2025 +0200 +++ b/Framework/Plugins/DatabaseBackendAdapterV4.cpp Fri Jun 13 15:16:52 2025 +0200 @@ -195,7 +195,8 @@ const std::string& uncompressedHash, int32_t compressionType, uint64_t compressedSize, - const std::string& compressedHash) ORTHANC_OVERRIDE + const std::string& compressedHash, + const std::string& customData) ORTHANC_OVERRIDE { Orthanc::DatabasePluginMessages::FileInfo* attachment; @@ -224,6 +225,10 @@ attachment->set_compression_type(compressionType); attachment->set_compressed_size(compressedSize); attachment->set_compressed_hash(compressedHash); + +#if ORTHANC_PLUGINS_HAS_ATTACHMENTS_CUSTOM_DATA == 1 + attachment->set_custom_data(customData); +#endif } virtual void SignalDeletedResource(const std::string& publicId, @@ -269,7 +274,8 @@ const std::string& uncompressedHash, int32_t compressionType, uint64_t compressedSize, - const std::string& compressedHash) ORTHANC_OVERRIDE + const std::string& compressedHash, + const std::string& customData) ORTHANC_OVERRIDE { if (lookupAttachment_ != NULL) { @@ -286,6 +292,9 @@ lookupAttachment_->mutable_attachment()->set_compression_type(compressionType); lookupAttachment_->mutable_attachment()->set_compressed_size(compressedSize); lookupAttachment_->mutable_attachment()->set_compressed_hash(compressedHash); +#if ORTHANC_PLUGINS_HAS_ATTACHMENTS_CUSTOM_DATA == 1 + lookupAttachment_->mutable_attachment()->set_custom_data(customData); +#endif } else { @@ -445,6 +454,18 @@ response.mutable_get_system_information()->set_has_extended_changes(accessor.GetBackend().HasExtendedChanges()); #endif +#if ORTHANC_PLUGINS_HAS_ATTACHMENTS_CUSTOM_DATA == 1 + response.mutable_get_system_information()->set_has_attachment_custom_data(accessor.GetBackend().HasAttachmentCustomDataSupport()); +#endif + +#if ORTHANC_PLUGINS_HAS_QUEUES == 1 + response.mutable_get_system_information()->set_supports_queues(accessor.GetBackend().HasQueues()); +#endif + +#if ORTHANC_PLUGINS_HAS_KEY_VALUE_STORES == 1 + response.mutable_get_system_information()->set_supports_key_value_stores(accessor.GetBackend().HasKeyValueStores()); +#endif + break; } @@ -472,16 +493,12 @@ } case Orthanc::DatabasePluginMessages::OPERATION_CLOSE: - { pool.CloseConnections(); break; - } case Orthanc::DatabasePluginMessages::OPERATION_FLUSH_TO_DISK: - { // Raise an exception since "set_supports_flush_to_disk(false)" throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); - } case Orthanc::DatabasePluginMessages::OPERATION_START_TRANSACTION: { @@ -669,16 +686,12 @@ switch (request.operation()) { case Orthanc::DatabasePluginMessages::OPERATION_ROLLBACK: - { manager.RollbackTransaction(); break; - } case Orthanc::DatabasePluginMessages::OPERATION_COMMIT: - { manager.CommitTransaction(); break; - } case Orthanc::DatabasePluginMessages::OPERATION_ADD_ATTACHMENT: { @@ -690,22 +703,24 @@ attachment.compressionType = request.add_attachment().attachment().compression_type(); attachment.compressedSize = request.add_attachment().attachment().compressed_size(); attachment.compressedHash = request.add_attachment().attachment().compressed_hash().c_str(); - + +#if ORTHANC_PLUGINS_HAS_ATTACHMENTS_CUSTOM_DATA == 1 + backend.AddAttachment(manager, request.add_attachment().id(), attachment, request.add_attachment().revision(), + request.add_attachment().attachment().custom_data()); +#else backend.AddAttachment(manager, request.add_attachment().id(), attachment, request.add_attachment().revision()); +#endif + break; } - + case Orthanc::DatabasePluginMessages::OPERATION_CLEAR_CHANGES: - { backend.ClearChanges(manager); break; - } case Orthanc::DatabasePluginMessages::OPERATION_CLEAR_EXPORTED_RESOURCES: - { backend.ClearExportedResources(manager); break; - } case Orthanc::DatabasePluginMessages::OPERATION_DELETE_ATTACHMENT: { @@ -715,10 +730,8 @@ } case Orthanc::DatabasePluginMessages::OPERATION_DELETE_METADATA: - { backend.DeleteMetadata(manager, request.delete_metadata().id(), request.delete_metadata().type()); break; - } case Orthanc::DatabasePluginMessages::OPERATION_DELETE_RESOURCE: { @@ -788,6 +801,7 @@ response.mutable_get_changes()->set_done(done); break; } + #if ORTHANC_PLUGINS_HAS_CHANGES_EXTENDED == 1 case Orthanc::DatabasePluginMessages::OPERATION_GET_CHANGES_EXTENDED: { @@ -895,16 +909,12 @@ } case Orthanc::DatabasePluginMessages::OPERATION_GET_TOTAL_COMPRESSED_SIZE: - { response.mutable_get_total_compressed_size()->set_size(backend.GetTotalCompressedSize(manager)); break; - } case Orthanc::DatabasePluginMessages::OPERATION_GET_TOTAL_UNCOMPRESSED_SIZE: - { response.mutable_get_total_uncompressed_size()->set_size(backend.GetTotalUncompressedSize(manager)); break; - } case Orthanc::DatabasePluginMessages::OPERATION_IS_PROTECTED_PATIENT: { @@ -928,16 +938,13 @@ } case Orthanc::DatabasePluginMessages::OPERATION_LOG_CHANGE: - { backend.LogChange(manager, request.log_change().change_type(), request.log_change().resource_id(), Convert(request.log_change().resource_type()), request.log_change().date().c_str()); break; - } case Orthanc::DatabasePluginMessages::OPERATION_LOG_EXPORTED_RESOURCE: - { backend.LogExportedResource(manager, Convert(request.log_exported_resource().resource_type()), request.log_exported_resource().public_id().c_str(), @@ -948,7 +955,6 @@ request.log_exported_resource().series_instance_uid().c_str(), request.log_exported_resource().sop_instance_uid().c_str()); break; - } case Orthanc::DatabasePluginMessages::OPERATION_LOOKUP_ATTACHMENT: { @@ -1094,34 +1100,26 @@ } case Orthanc::DatabasePluginMessages::OPERATION_SET_GLOBAL_PROPERTY: - { backend.SetGlobalProperty(manager, request.set_global_property().server_id().c_str(), request.set_global_property().property(), request.set_global_property().value().c_str()); break; - } case Orthanc::DatabasePluginMessages::OPERATION_CLEAR_MAIN_DICOM_TAGS: - { backend.ClearMainDicomTags(manager, request.clear_main_dicom_tags().id()); break; - } case Orthanc::DatabasePluginMessages::OPERATION_SET_METADATA: - { backend.SetMetadata(manager, request.set_metadata().id(), request.set_metadata().metadata_type(), request.set_metadata().value().c_str(), request.set_metadata().revision()); break; - } case Orthanc::DatabasePluginMessages::OPERATION_SET_PROTECTED_PATIENT: - { backend.SetProtectedPatient(manager, request.set_protected_patient().patient_id(), request.set_protected_patient().protected_patient()); break; - } case Orthanc::DatabasePluginMessages::OPERATION_IS_DISK_SIZE_ABOVE: { @@ -1131,10 +1129,8 @@ } case Orthanc::DatabasePluginMessages::OPERATION_LOOKUP_RESOURCES: - { ApplyLookupResources(*response.mutable_lookup_resources(), request.lookup_resources(), backend, manager); break; - } case Orthanc::DatabasePluginMessages::OPERATION_CREATE_INSTANCE: { @@ -1236,10 +1232,8 @@ } case Orthanc::DatabasePluginMessages::OPERATION_GET_LAST_CHANGE_INDEX: - { response.mutable_get_last_change_index()->set_result(backend.GetLastChangeIndex(manager)); break; - } case Orthanc::DatabasePluginMessages::OPERATION_LOOKUP_RESOURCE_AND_PARENT: { @@ -1288,16 +1282,12 @@ } case Orthanc::DatabasePluginMessages::OPERATION_ADD_LABEL: - { backend.AddLabel(manager, request.add_label().id(), request.add_label().label()); break; - } case Orthanc::DatabasePluginMessages::OPERATION_REMOVE_LABEL: - { backend.RemoveLabel(manager, request.remove_label().id(), request.remove_label().label()); break; - } case Orthanc::DatabasePluginMessages::OPERATION_LIST_LABELS: { @@ -1323,16 +1313,96 @@ #if ORTHANC_PLUGINS_HAS_INTEGRATED_FIND == 1 case Orthanc::DatabasePluginMessages::OPERATION_FIND: + backend.ExecuteFind(response, manager, request.find()); + break; + + case Orthanc::DatabasePluginMessages::OPERATION_COUNT_RESOURCES: + backend.ExecuteCount(response, manager, request.find()); + break; +#endif + +#if ORTHANC_PLUGINS_HAS_KEY_VALUE_STORES == 1 + case Orthanc::DatabasePluginMessages::OPERATION_STORE_KEY_VALUE: + backend.StoreKeyValue(manager, + request.store_key_value().store_id(), + request.store_key_value().key(), + request.store_key_value().value()); + break; + + case Orthanc::DatabasePluginMessages::OPERATION_GET_KEY_VALUE: { - backend.ExecuteFind(response, manager, request.find()); + std::string 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); + + if (found) + { + response.mutable_get_key_value()->set_value(value); + } break; } - case Orthanc::DatabasePluginMessages::OPERATION_COUNT_RESOURCES: + case Orthanc::DatabasePluginMessages::OPERATION_DELETE_KEY_VALUE: + backend.DeleteKeyValue(manager, + request.delete_key_value().store_id(), + request.delete_key_value().key()); + break; + + case Orthanc::DatabasePluginMessages::OPERATION_LIST_KEY_VALUES: + backend.ListKeysValues(response, manager, request.list_keys_values()); + break; + +#endif + +#if ORTHANC_PLUGINS_HAS_QUEUES == 1 + case Orthanc::DatabasePluginMessages::OPERATION_ENQUEUE_VALUE: + backend.EnqueueValue(manager, + request.enqueue_value().queue_id(), + request.enqueue_value().value()); + break; + + case Orthanc::DatabasePluginMessages::OPERATION_DEQUEUE_VALUE: { - backend.ExecuteCount(response, manager, request.find()); + std::string 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); + + if (found) + { + response.mutable_dequeue_value()->set_value(value); + } + break; } + + case Orthanc::DatabasePluginMessages::OPERATION_GET_QUEUE_SIZE: + { + uint64_t size = backend.GetQueueSize(manager, + request.get_queue_size().queue_id()); + response.mutable_get_queue_size()->set_size(size); + break; + } + +#endif + +#if ORTHANC_PLUGINS_HAS_ATTACHMENTS_CUSTOM_DATA == 1 + case Orthanc::DatabasePluginMessages::OPERATION_GET_ATTACHMENT_CUSTOM_DATA: + { + std::string customData; + backend.GetAttachmentCustomData(customData, manager, request.get_attachment_custom_data().uuid()); + response.mutable_get_attachment_custom_data()->set_custom_data(customData); + break; + } + + case Orthanc::DatabasePluginMessages::OPERATION_SET_ATTACHMENT_CUSTOM_DATA: + backend.SetAttachmentCustomData(manager, + request.set_attachment_custom_data().uuid(), + request.set_attachment_custom_data().custom_data()); + break; #endif default: @@ -1381,8 +1451,8 @@ } default: - LOG(ERROR) << "Not implemented request type from protobuf: " << request.type(); - break; + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented, "Not implemented request type from protobuf: " + + boost::lexical_cast<std::string>(request.type())); } std::string s;
--- a/Framework/Plugins/IDatabaseBackend.h Mon May 19 11:18:43 2025 +0200 +++ b/Framework/Plugins/IDatabaseBackend.h Fri Jun 13 15:16:52 2025 +0200 @@ -59,11 +59,26 @@ virtual bool HasRevisionsSupport() const = 0; + virtual bool HasAttachmentCustomDataSupport() const = 0; + + virtual bool HasKeyValueStores() const = 0; + + virtual bool HasQueues() const = 0; + virtual void AddAttachment(DatabaseManager& manager, int64_t id, const OrthancPluginAttachment& attachment, int64_t revision) = 0; +#if ORTHANC_PLUGINS_HAS_ATTACHMENTS_CUSTOM_DATA == 1 + // New in Orthanc 1.12.8 + virtual void AddAttachment(DatabaseManager& manager, + int64_t id, + const OrthancPluginAttachment& attachment, + int64_t revision, + const std::string& customData) = 0; +#endif + virtual void AttachChild(DatabaseManager& manager, int64_t parent, int64_t child) = 0; @@ -400,6 +415,50 @@ const Orthanc::DatabasePluginMessages::Find_Request& request) = 0; #endif +#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; + + virtual void DeleteKeyValue(DatabaseManager& manager, + 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, + const Orthanc::DatabasePluginMessages::ListKeysValues_Request& request) = 0; +#endif + +#if ORTHANC_PLUGINS_HAS_QUEUES == 1 + virtual void EnqueueValue(DatabaseManager& manager, + const std::string& queueId, + const std::string& value) = 0; + + virtual bool DequeueValue(std::string& value, + DatabaseManager& manager, + const std::string& queueId, + bool fromFront) = 0; + + virtual uint64_t GetQueueSize(DatabaseManager& manager, + const std::string& queueId) = 0; +#endif + +#if ORTHANC_PLUGINS_HAS_ATTACHMENTS_CUSTOM_DATA == 1 + virtual void GetAttachmentCustomData(std::string& customData, + DatabaseManager& manager, + const std::string& attachmentUuid) = 0; + + virtual void SetAttachmentCustomData(DatabaseManager& manager, + const std::string& attachmentUuid, + const std::string& customData) = 0; +#endif + virtual bool HasPerformDbHousekeeping() = 0; virtual void PerformDbHousekeeping(DatabaseManager& manager) = 0;
--- a/Framework/Plugins/IDatabaseBackendOutput.h Mon May 19 11:18:43 2025 +0200 +++ b/Framework/Plugins/IDatabaseBackendOutput.h Fri Jun 13 15:16:52 2025 +0200 @@ -56,7 +56,8 @@ const std::string& uncompressedHash, int32_t compressionType, uint64_t compressedSize, - const std::string& compressedHash) = 0; + const std::string& compressedHash, + const std::string& customData) = 0; virtual void SignalDeletedResource(const std::string& publicId, OrthancPluginResourceType resourceType) = 0; @@ -70,7 +71,8 @@ const std::string& uncompressedHash, int32_t compressionType, uint64_t compressedSize, - const std::string& compressedHash) = 0; + const std::string& compressedHash, + const std::string& customData) = 0; virtual void AnswerChange(int64_t seq, int32_t changeType,
--- a/Framework/Plugins/IndexBackend.cpp Mon May 19 11:18:43 2025 +0200 +++ b/Framework/Plugins/IndexBackend.cpp Fri Jun 13 15:16:52 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" @@ -270,11 +268,13 @@ DatabaseManager::CachedStatement statement( STATEMENT_FROM_HERE, manager, "SELECT uuid, fileType, uncompressedSize, uncompressedHash, compressionType, " - "compressedSize, compressedHash FROM DeletedFiles"); + "compressedSize, compressedHash, revision, customData FROM DeletedFiles"); statement.SetReadOnly(true); statement.Execute(); + statement.SetResultFieldType(8, ValueType_BinaryString); + while (!statement.IsDone()) { output.SignalDeletedAttachment(statement.ReadString(0), @@ -283,7 +283,8 @@ statement.ReadString(3), statement.ReadInteger32(4), statement.ReadInteger64(5), - statement.ReadString(6)); + statement.ReadString(6), + statement.ReadStringOrNull(8)); statement.Next(); } @@ -354,12 +355,26 @@ } } - - static void ExecuteAddAttachment(DatabaseManager::CachedStatement& statement, - Dictionary& args, + static void ExecuteAddAttachment(DatabaseManager& manager, int64_t id, - const OrthancPluginAttachment& attachment) + const char* uuid, + int32_t contentType, + uint64_t uncompressedSize, + const char* uncompressedHash, + int32_t compressionType, + uint64_t compressedSize, + const char* compressedHash, + const void* customData, + size_t customDataSize, + int64_t revision) { + DatabaseManager::CachedStatement statement( + STATEMENT_FROM_HERE, manager, + "INSERT INTO AttachedFiles VALUES(${id}, ${type}, ${uuid}, ${compressed}, " + "${uncompressed}, ${compression}, ${hash}, ${hash-compressed}, ${revision}, ${custom-data})"); + + Dictionary args; + statement.SetParameterType("id", ValueType_Integer64); statement.SetParameterType("type", ValueType_Integer64); statement.SetParameterType("uuid", ValueType_Utf8String); @@ -368,52 +383,50 @@ statement.SetParameterType("compression", ValueType_Integer64); statement.SetParameterType("hash", ValueType_Utf8String); statement.SetParameterType("hash-compressed", ValueType_Utf8String); + statement.SetParameterType("revision", ValueType_Integer64); + statement.SetParameterType("custom-data", ValueType_Utf8String); args.SetIntegerValue("id", id); - args.SetIntegerValue("type", attachment.contentType); - args.SetUtf8Value("uuid", attachment.uuid); - args.SetIntegerValue("compressed", attachment.compressedSize); - args.SetIntegerValue("uncompressed", attachment.uncompressedSize); - args.SetIntegerValue("compression", attachment.compressionType); - args.SetUtf8Value("hash", attachment.uncompressedHash); - args.SetUtf8Value("hash-compressed", attachment.compressedHash); + args.SetIntegerValue("type", contentType); + args.SetUtf8Value("uuid", uuid); + args.SetIntegerValue("compressed", compressedSize); + args.SetIntegerValue("uncompressed", uncompressedSize); + args.SetIntegerValue("compression", compressionType); + args.SetUtf8Value("hash", uncompressedHash); + args.SetUtf8Value("hash-compressed", compressedHash); + args.SetIntegerValue("revision", revision); + args.SetBinaryValue("custom-data", customData, customDataSize); statement.Execute(args); } - + void IndexBackend::AddAttachment(DatabaseManager& manager, int64_t id, const OrthancPluginAttachment& attachment, int64_t revision) { - if (HasRevisionsSupport()) - { - DatabaseManager::CachedStatement statement( - STATEMENT_FROM_HERE, manager, - "INSERT INTO AttachedFiles VALUES(${id}, ${type}, ${uuid}, ${compressed}, " - "${uncompressed}, ${compression}, ${hash}, ${hash-compressed}, ${revision})"); - - Dictionary args; - - statement.SetParameterType("revision", ValueType_Integer64); - args.SetIntegerValue("revision", revision); - - ExecuteAddAttachment(statement, args, id, attachment); - } - else - { - DatabaseManager::CachedStatement statement( - STATEMENT_FROM_HERE, manager, - "INSERT INTO AttachedFiles VALUES(${id}, ${type}, ${uuid}, ${compressed}, " - "${uncompressed}, ${compression}, ${hash}, ${hash-compressed})"); - - Dictionary args; - ExecuteAddAttachment(statement, args, id, attachment); - } + assert(HasRevisionsSupport()); // all plugins support these features now + ExecuteAddAttachment(manager, id, attachment.uuid, attachment.contentType, attachment.uncompressedSize, attachment.uncompressedHash, + attachment.compressionType, attachment.compressedSize, attachment.compressedHash, NULL, 0, revision); } - + +#if ORTHANC_PLUGINS_HAS_ATTACHMENTS_CUSTOM_DATA == 1 + void IndexBackend::AddAttachment(DatabaseManager& manager, + int64_t id, + const OrthancPluginAttachment& attachment, + int64_t revision, + const std::string& customData) + { + assert(HasRevisionsSupport() && HasAttachmentCustomDataSupport()); + ExecuteAddAttachment(manager, id, attachment.uuid, attachment.contentType, attachment.uncompressedSize, attachment.uncompressedHash, + attachment.compressionType, attachment.compressedSize, attachment.compressedHash, + customData.empty() ? NULL : customData.c_str(), customData.size(), revision); + } +#endif + + void IndexBackend::AttachChild(DatabaseManager& manager, int64_t parent, int64_t child) @@ -957,7 +970,7 @@ if (statement.IsDone()) { - throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource, "No resource type found for internal id."); + throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource, "No resource type found for internal id"); } else { @@ -1178,12 +1191,20 @@ statement.Execute(args); } - - static bool ExecuteLookupAttachment(DatabaseManager::CachedStatement& statement, - IDatabaseBackendOutput& output, + + bool IndexBackend::LookupAttachment(IDatabaseBackendOutput& output, + int64_t& revision /*out*/, + DatabaseManager& manager, int64_t id, int32_t contentType) { + assert(HasRevisionsSupport() && HasAttachmentCustomDataSupport()); // we have forced all v4 plugins to support both ! + DatabaseManager::CachedStatement statement( + STATEMENT_FROM_HERE, manager, + "SELECT uuid, uncompressedSize, compressionType, compressedSize, uncompressedHash, " + "compressedHash, revision, customData FROM AttachedFiles WHERE id=${id} AND fileType=${type}"); + + statement.SetReadOnly(true); statement.SetParameterType("id", ValueType_Integer64); statement.SetParameterType("type", ValueType_Integer64); @@ -1193,6 +1214,7 @@ args.SetIntegerValue("type", static_cast<int>(contentType)); statement.Execute(args); + statement.SetResultFieldType(7, ValueType_BinaryString); if (statement.IsDone()) { @@ -1200,64 +1222,31 @@ } else { + if (statement.GetResultField(6).GetType() == ValueType_Null) + { + // "NULL" can happen with a database created by PostgreSQL + // plugin <= 3.3 (because of "ALTER TABLE AttachedFiles") + revision = 0; + } + else + { + revision = statement.ReadInteger64(6); + } + + std::string customData; + customData = statement.ReadStringOrNull(7); + output.AnswerAttachment(statement.ReadString(0), contentType, statement.ReadInteger64(1), statement.ReadString(4), statement.ReadInteger32(2), statement.ReadInteger64(3), - statement.ReadString(5)); + statement.ReadString(5), + customData); return true; } - } - - - - /* Use GetOutput().AnswerAttachment() */ - bool IndexBackend::LookupAttachment(IDatabaseBackendOutput& output, - int64_t& revision /*out*/, - DatabaseManager& manager, - int64_t id, - int32_t contentType) - { - if (HasRevisionsSupport()) - { - DatabaseManager::CachedStatement statement( - STATEMENT_FROM_HERE, manager, - "SELECT uuid, uncompressedSize, compressionType, compressedSize, uncompressedHash, " - "compressedHash, revision FROM AttachedFiles WHERE id=${id} AND fileType=${type}"); - - if (ExecuteLookupAttachment(statement, output, id, contentType)) - { - if (statement.GetResultField(6).GetType() == ValueType_Null) - { - // "NULL" can happen with a database created by PostgreSQL - // plugin <= 3.3 (because of "ALTER TABLE AttachedFiles") - revision = 0; - } - else - { - revision = statement.ReadInteger64(6); - } - - return true; - } - else - { - return false; - } - } - else - { - DatabaseManager::CachedStatement statement( - STATEMENT_FROM_HERE, manager, - "SELECT uuid, uncompressedSize, compressionType, compressedSize, uncompressedHash, " - "compressedHash FROM AttachedFiles WHERE id=${id} AND fileType=${type}"); - - revision = 0; - - return ExecuteLookupAttachment(statement, output, id, contentType); - } + } @@ -3253,11 +3242,12 @@ #define C3_STRING_1 3 #define C4_STRING_2 4 #define C5_STRING_3 5 -#define C6_INT_1 6 -#define C7_INT_2 7 -#define C8_INT_3 8 -#define C9_BIG_INT_1 9 -#define C10_BIG_INT_2 10 +#define C6_STRING_4 6 +#define C7_INT_1 7 +#define C8_INT_2 8 +#define C9_INT_3 9 +#define C10_BIG_INT_1 10 +#define C11_BIG_INT_2 11 #define QUERY_LOOKUP 1 #define QUERY_MAIN_DICOM_TAGS 2 @@ -3431,11 +3421,11 @@ std::string revisionInC7; if (HasRevisionsSupport()) { - revisionInC7 = " revision AS c7_int2, "; + revisionInC7 = " revision AS c8_int2, "; } else { - revisionInC7 = " 0 AS C7_int2, "; + revisionInC7 = " 0 AS c8_int2, "; } @@ -3446,11 +3436,12 @@ " Lookup.publicId AS c3_string1, " " " + formatter.FormatNull("TEXT") + " AS c4_string2, " " " + formatter.FormatNull("TEXT") + " AS c5_string3, " - " " + formatter.FormatNull("INT") + " AS c6_int1, " - " " + formatter.FormatNull("INT") + " AS c7_int2, " - " " + formatter.FormatNull("INT") + " AS c8_int3, " - " " + formatter.FormatNull("BIGINT") + " AS c9_big_int1, " - " " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 " + " " + formatter.FormatNull("BYTEA") + " AS c6_string4, " + " " + formatter.FormatNull("INT") + " AS c7_int1, " + " " + formatter.FormatNull("INT") + " AS c8_int2, " + " " + formatter.FormatNull("INT") + " AS c9_int3, " + " " + formatter.FormatNull("BIGINT") + " AS c10_big_int1, " + " " + formatter.FormatNull("BIGINT") + " AS c11_big_int2 " " FROM Lookup "; // need MainDicomTags from resource ? @@ -3463,11 +3454,12 @@ " value AS c3_string1, " " " + formatter.FormatNull("TEXT") + " AS c4_string2, " " " + formatter.FormatNull("TEXT") + " AS c5_string3, " - " tagGroup AS c6_int1, " - " tagElement AS c7_int2, " - " " + formatter.FormatNull("INT") + " AS c8_int3, " - " " + formatter.FormatNull("BIGINT") + " AS c9_big_int1, " - " " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 " + " " + formatter.FormatNull("BYTEA") + " AS c6_string4, " + " tagGroup AS c7_int1, " + " tagElement AS c8_int2, " + " " + formatter.FormatNull("INT") + " AS c9_int3, " + " " + formatter.FormatNull("BIGINT") + " AS c10_big_int1, " + " " + formatter.FormatNull("BIGINT") + " AS c11_big_int2 " "FROM Lookup " "INNER JOIN MainDicomTags ON MainDicomTags.id = Lookup.internalId "; } @@ -3482,11 +3474,12 @@ " value AS c3_string1, " " " + formatter.FormatNull("TEXT") + " AS c4_string2, " " " + formatter.FormatNull("TEXT") + " AS c5_string3, " - " type AS c6_int1, " + " " + formatter.FormatNull("BYTEA") + " AS c6_string4, " + " type AS c7_int1, " + revisionInC7 + - " " + formatter.FormatNull("INT") + " AS c8_int3, " - " " + formatter.FormatNull("BIGINT") + " AS c9_big_int1, " - " " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 " + " " + formatter.FormatNull("INT") + " AS c9_int3, " + " " + formatter.FormatNull("BIGINT") + " AS c10_big_int1, " + " " + formatter.FormatNull("BIGINT") + " AS c11_big_int2 " "FROM Lookup " "INNER JOIN Metadata ON Metadata.id = Lookup.internalId "; } @@ -3501,11 +3494,12 @@ " uuid AS c3_string1, " " uncompressedHash AS c4_string2, " " compressedHash AS c5_string3, " - " fileType AS c6_int1, " + " customData AS c6_string4, " + " fileType AS c7_int1, " + revisionInC7 + - " compressionType AS c8_int3, " - " compressedSize AS c9_big_int1, " - " uncompressedSize AS c10_big_int2 " + " compressionType AS c9_int3, " + " compressedSize AS c10_big_int1, " + " uncompressedSize AS c11_big_int2 " "FROM Lookup " "INNER JOIN AttachedFiles ON AttachedFiles.id = Lookup.internalId "; } @@ -3520,11 +3514,12 @@ " label AS c3_string1, " " " + formatter.FormatNull("TEXT") + " AS c4_string2, " " " + formatter.FormatNull("TEXT") + " AS c5_string3, " - " " + formatter.FormatNull("INT") + " AS c6_int1, " - " " + formatter.FormatNull("INT") + " AS c7_int2, " - " " + formatter.FormatNull("INT") + " AS c8_int3, " - " " + formatter.FormatNull("BIGINT") + " AS c9_big_int1, " - " " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 " + " " + formatter.FormatNull("BYTEA") + " AS c6_string4, " + " " + formatter.FormatNull("INT") + " AS c7_int1, " + " " + formatter.FormatNull("INT") + " AS c8_int2, " + " " + formatter.FormatNull("INT") + " AS c9_int3, " + " " + formatter.FormatNull("BIGINT") + " AS c10_big_int1, " + " " + formatter.FormatNull("BIGINT") + " AS c11_big_int2 " "FROM Lookup " "INNER JOIN Labels ON Labels.id = Lookup.internalId "; } @@ -3558,11 +3553,12 @@ " value AS c3_string1, " " " + formatter.FormatNull("TEXT") + " AS c4_string2, " " " + formatter.FormatNull("TEXT") + " AS c5_string3, " - " tagGroup AS c6_int1, " - " tagElement AS c7_int2, " - " " + formatter.FormatNull("INT") + " AS c8_int3, " - " " + formatter.FormatNull("BIGINT") + " AS c9_big_int1, " - " " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 " + " " + formatter.FormatNull("BYTEA") + " AS c6_string4, " + " tagGroup AS c7_int1, " + " tagElement AS c8_int2, " + " " + formatter.FormatNull("INT") + " AS c9_int3, " + " " + formatter.FormatNull("BIGINT") + " AS c10_big_int1, " + " " + formatter.FormatNull("BIGINT") + " AS c11_big_int2 " "FROM Lookup " "INNER JOIN Resources currentLevel ON Lookup.internalId = currentLevel.internalId " "INNER JOIN MainDicomTags ON MainDicomTags.id = currentLevel.parentId "; @@ -3577,11 +3573,12 @@ " value AS c3_string1, " " " + formatter.FormatNull("TEXT") + " AS c4_string2, " " " + formatter.FormatNull("TEXT") + " AS c5_string3, " - " type AS c6_int1, " + " " + formatter.FormatNull("BYTEA") + " AS c6_string4, " + " type AS c7_int1, " + revisionInC7 + - " " + formatter.FormatNull("INT") + " AS c8_int3, " - " " + formatter.FormatNull("BIGINT") + " AS c9_big_int1, " - " " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 " + " " + formatter.FormatNull("INT") + " AS c9_int3, " + " " + formatter.FormatNull("BIGINT") + " AS c10_big_int1, " + " " + formatter.FormatNull("BIGINT") + " AS c11_big_int2 " "FROM Lookup " "INNER JOIN Resources currentLevel ON Lookup.internalId = currentLevel.internalId " "INNER JOIN Metadata ON Metadata.id = currentLevel.parentId "; @@ -3613,11 +3610,12 @@ " value AS c3_string1, " " " + formatter.FormatNull("TEXT") + " AS c4_string2, " " " + formatter.FormatNull("TEXT") + " AS c5_string3, " - " tagGroup AS c6_int1, " - " tagElement AS c7_int2, " - " " + formatter.FormatNull("INT") + " AS c8_int3, " - " " + formatter.FormatNull("BIGINT") + " AS c9_big_int1, " - " " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 " + " " + formatter.FormatNull("BYTEA") + " AS c6_string4, " + " tagGroup AS c7_int1, " + " tagElement AS c8_int2, " + " " + formatter.FormatNull("INT") + " AS c9_int3, " + " " + formatter.FormatNull("BIGINT") + " AS c10_big_int1, " + " " + formatter.FormatNull("BIGINT") + " AS c11_big_int2 " "FROM Lookup " "INNER JOIN Resources currentLevel ON Lookup.internalId = currentLevel.internalId " "INNER JOIN Resources parentLevel ON currentLevel.parentId = parentLevel.internalId " @@ -3633,11 +3631,12 @@ " value AS c3_string1, " " " + formatter.FormatNull("TEXT") + " AS c4_string2, " " " + formatter.FormatNull("TEXT") + " AS c5_string3, " - " type AS c6_int1, " + " " + formatter.FormatNull("BYTEA") + " AS c6_string4, " + " type AS c7_int1, " + revisionInC7 + - " " + formatter.FormatNull("INT") + " AS c8_int3, " - " " + formatter.FormatNull("BIGINT") + " AS c9_big_int1, " - " " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 " + " " + formatter.FormatNull("INT") + " AS c9_int3, " + " " + formatter.FormatNull("BIGINT") + " AS c10_big_int1, " + " " + formatter.FormatNull("BIGINT") + " AS c11_big_int2 " "FROM Lookup " "INNER JOIN Resources currentLevel ON Lookup.internalId = currentLevel.internalId " "INNER JOIN Resources parentLevel ON currentLevel.parentId = parentLevel.internalId " @@ -3660,11 +3659,12 @@ " value AS c3_string1, " " " + formatter.FormatNull("TEXT") + " AS c4_string2, " " " + formatter.FormatNull("TEXT") + " AS c5_string3, " - " tagGroup AS c6_int1, " - " tagElement AS c7_int2, " - " " + formatter.FormatNull("INT") + " AS c8_int3, " - " " + formatter.FormatNull("BIGINT") + " AS c9_big_int1, " - " " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 " + " " + formatter.FormatNull("BYTEA") + " AS c6_string4, " + " tagGroup AS c7_int1, " + " tagElement AS c8_int2, " + " " + formatter.FormatNull("INT") + " AS c9_int3, " + " " + formatter.FormatNull("BIGINT") + " AS c10_big_int1, " + " " + formatter.FormatNull("BIGINT") + " AS c11_big_int2 " "FROM Lookup " " INNER JOIN Resources childLevel ON childLevel.parentId = Lookup.internalId " " INNER JOIN MainDicomTags ON MainDicomTags.id = childLevel.internalId AND (tagGroup, tagElement) IN (" + JoinRequestedTags(childrenSpec) + ")"; @@ -3680,11 +3680,12 @@ " childLevel.publicId AS c3_string1, " " " + formatter.FormatNull("TEXT") + " AS c4_string2, " " " + formatter.FormatNull("TEXT") + " AS c5_string3, " - " " + formatter.FormatNull("INT") + " AS c6_int1, " - " " + formatter.FormatNull("INT") + " AS c7_int2, " - " " + formatter.FormatNull("INT") + " AS c8_int3, " - " " + formatter.FormatNull("BIGINT") + " AS c9_big_int1, " - " " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 " + " " + formatter.FormatNull("BYTEA") + " AS c6_string4, " + " " + formatter.FormatNull("INT") + " AS c7_int1, " + " " + formatter.FormatNull("INT") + " AS c8_int2, " + " " + formatter.FormatNull("INT") + " AS c9_int3, " + " " + formatter.FormatNull("BIGINT") + " AS c10_big_int1, " + " " + formatter.FormatNull("BIGINT") + " AS c11_big_int2 " "FROM Lookup " " INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId "; } @@ -3715,16 +3716,17 @@ " " + formatter.FormatNull("TEXT") + " AS c3_string1, " " " + formatter.FormatNull("TEXT") + " AS c4_string2, " " " + formatter.FormatNull("TEXT") + " AS c5_string3, " - " " + formatter.FormatNull("INT") + " AS c6_int1, " - " " + formatter.FormatNull("INT") + " AS c7_int2, " - " " + formatter.FormatNull("INT") + " AS c8_int3, " + " " + formatter.FormatNull("BYTEA") + " AS c6_string4, " + " " + formatter.FormatNull("INT") + " AS c7_int1, " + " " + formatter.FormatNull("INT") + " AS c8_int2, " + " " + formatter.FormatNull("INT") + " AS c9_int3, " " COALESCE(" " (" + getResourcesChildCount + ")," " (SELECT COUNT(childLevel.internalId)" " FROM Resources AS childLevel" " WHERE Lookup.internalId = childLevel.parentId" - " )) AS c9_big_int1, " - " " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 " + " )) AS c10_big_int1, " + " " + formatter.FormatNull("BIGINT") + " AS c11_big_int2 " "FROM Lookup " "LEFT JOIN Resources ON Lookup.internalId = Resources.internalId "; } @@ -3737,11 +3739,12 @@ " " + formatter.FormatNull("TEXT") + " AS c3_string1, " " " + formatter.FormatNull("TEXT") + " AS c4_string2, " " " + formatter.FormatNull("TEXT") + " AS c5_string3, " - " " + formatter.FormatNull("INT") + " AS c6_int1, " - " " + formatter.FormatNull("INT") + " AS c7_int2, " - " " + formatter.FormatNull("INT") + " AS c8_int3, " - " COUNT(childLevel.internalId) AS c9_big_int1, " - " " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 " + " " + formatter.FormatNull("BYTEA") + " AS c6_string4, " + " " + formatter.FormatNull("INT") + " AS c7_int1, " + " " + formatter.FormatNull("INT") + " AS c8_int2, " + " " + formatter.FormatNull("INT") + " AS c9_int3, " + " COUNT(childLevel.internalId) AS c10_big_int1, " + " " + formatter.FormatNull("BIGINT") + " AS c11_big_int2 " "FROM Lookup " " INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId GROUP BY Lookup.internalId "; } @@ -3756,11 +3759,12 @@ " value AS c3_string1, " " " + formatter.FormatNull("TEXT") + " AS c4_string2, " " " + formatter.FormatNull("TEXT") + " AS c5_string3, " - " type AS c6_int1, " + " " + formatter.FormatNull("BYTEA") + " AS c6_string4, " + " type AS c7_int1, " + revisionInC7 + - " " + formatter.FormatNull("INT") + " AS c8_int3, " - " " + formatter.FormatNull("BIGINT") + " AS c9_big_int1, " - " " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 " + " " + formatter.FormatNull("INT") + " AS c9_int3, " + " " + formatter.FormatNull("BIGINT") + " AS c10_big_int1, " + " " + formatter.FormatNull("BIGINT") + " AS c11_big_int2 " "FROM Lookup " " INNER JOIN Resources childLevel ON childLevel.parentId = Lookup.internalId " " INNER JOIN Metadata ON Metadata.id = childLevel.internalId AND Metadata.type IN (" + JoinRequestedMetadata(childrenSpec) + ") "; @@ -3780,11 +3784,12 @@ " grandChildLevel.publicId AS c3_string1, " " " + formatter.FormatNull("TEXT") + " AS c4_string2, " " " + formatter.FormatNull("TEXT") + " AS c5_string3, " - " " + formatter.FormatNull("INT") + " AS c6_int1, " - " " + formatter.FormatNull("INT") + " AS c7_int2, " - " " + formatter.FormatNull("INT") + " AS c8_int3, " - " " + formatter.FormatNull("BIGINT") + " AS c9_big_int1, " - " " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 " + " " + formatter.FormatNull("BYTEA") + " AS c6_string4, " + " " + formatter.FormatNull("INT") + " AS c7_int1, " + " " + formatter.FormatNull("INT") + " AS c8_int2, " + " " + formatter.FormatNull("INT") + " AS c9_int3, " + " " + formatter.FormatNull("BIGINT") + " AS c10_big_int1, " + " " + formatter.FormatNull("BIGINT") + " AS c11_big_int2 " "FROM Lookup " "INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId " "INNER JOIN Resources grandChildLevel ON childLevel.internalId = grandChildLevel.parentId "; @@ -3793,6 +3798,7 @@ { if (HasChildCountColumn()) { + // we get the count value either from the childCount column if it has been computed or from the Resources table std::string getResourcesGrandChildCount; @@ -3815,23 +3821,25 @@ // we get the count value either from the childCount column if it has been computed or from the Resources table sql += "UNION ALL SELECT " + " " TOSTRING(QUERY_GRAND_CHILDREN_COUNT) " AS c0_queryId, " " Lookup.internalId AS c1_internalId, " " " + formatter.FormatNull("BIGINT") + " AS c2_rowNumber, " " " + formatter.FormatNull("TEXT") + " AS c3_string1, " " " + formatter.FormatNull("TEXT") + " AS c4_string2, " " " + formatter.FormatNull("TEXT") + " AS c5_string3, " - " " + formatter.FormatNull("INT") + " AS c6_int1, " - " " + formatter.FormatNull("INT") + " AS c7_int2, " - " " + formatter.FormatNull("INT") + " AS c8_int3, " + " " + formatter.FormatNull("BYTEA") + " AS c6_string4, " + " " + formatter.FormatNull("INT") + " AS c7_int1, " + " " + formatter.FormatNull("INT") + " AS c8_int2, " + " " + formatter.FormatNull("INT") + " AS c9_int3, " " COALESCE(" " (" + getResourcesGrandChildCount + ")," " (SELECT COUNT(grandChildLevel.internalId)" " FROM Resources AS childLevel" " INNER JOIN Resources AS grandChildLevel ON childLevel.internalId = grandChildLevel.parentId" " WHERE Lookup.internalId = childLevel.parentId" - " )) AS c9_big_int1, " - " " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 " + " )) AS c10_big_int1, " + " " + formatter.FormatNull("BIGINT") + " AS c11_big_int2 " "FROM Lookup "; } else @@ -3843,11 +3851,12 @@ " " + formatter.FormatNull("TEXT") + " AS c3_string1, " " " + formatter.FormatNull("TEXT") + " AS c4_string2, " " " + formatter.FormatNull("TEXT") + " AS c5_string3, " - " " + formatter.FormatNull("INT") + " AS c6_int1, " - " " + formatter.FormatNull("INT") + " AS c7_int2, " - " " + formatter.FormatNull("INT") + " AS c8_int3, " - " COUNT(grandChildLevel.internalId) AS c9_big_int1, " - " " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 " + " " + formatter.FormatNull("BYTEA") + " AS c6_string4, " + " " + formatter.FormatNull("INT") + " AS c7_int1, " + " " + formatter.FormatNull("INT") + " AS c8_int2, " + " " + formatter.FormatNull("INT") + " AS c9_int3, " + " COUNT(grandChildLevel.internalId) AS c10_big_int1, " + " " + formatter.FormatNull("BIGINT") + " AS c11_big_int2 " "FROM Lookup " " INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId " " INNER JOIN Resources grandChildLevel ON childLevel.internalId = grandChildLevel.parentId GROUP BY Lookup.internalId "; @@ -3863,11 +3872,12 @@ " value AS c3_string1, " " " + formatter.FormatNull("TEXT") + " AS c4_string2, " " " + formatter.FormatNull("TEXT") + " AS c5_string3, " - " tagGroup AS c6_int1, " - " tagElement AS c7_int2, " - " " + formatter.FormatNull("INT") + " AS c8_int3, " - " " + formatter.FormatNull("BIGINT") + " AS c9_big_int1, " - " " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 " + " " + formatter.FormatNull("BYTEA") + " AS c6_string4, " + " tagGroup AS c7_int1, " + " tagElement AS c8_int2, " + " " + formatter.FormatNull("INT") + " AS c9_int3, " + " " + formatter.FormatNull("BIGINT") + " AS c10_big_int1, " + " " + formatter.FormatNull("BIGINT") + " AS c11_big_int2 " "FROM Lookup " " INNER JOIN Resources childLevel ON childLevel.parentId = Lookup.internalId " " INNER JOIN Resources grandChildLevel ON grandChildLevel.parentId = childLevel.internalId " @@ -3883,11 +3893,12 @@ " value AS c3_string1, " " " + formatter.FormatNull("TEXT") + " AS c4_string2, " " " + formatter.FormatNull("TEXT") + " AS c5_string3, " - " type AS c6_int1, " + " " + formatter.FormatNull("BYTEA") + " AS c6_string4, " + " type AS c7_int1, " + revisionInC7 + - " " + formatter.FormatNull("INT") + " AS c8_int3, " - " " + formatter.FormatNull("BIGINT") + " AS c9_big_int1, " - " " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 " + " " + formatter.FormatNull("INT") + " AS c9_int3, " + " " + formatter.FormatNull("BIGINT") + " AS c10_big_int1, " + " " + formatter.FormatNull("BIGINT") + " AS c11_big_int2 " "FROM Lookup " " INNER JOIN Resources childLevel ON childLevel.parentId = Lookup.internalId " " INNER JOIN Resources grandChildLevel ON grandChildLevel.parentId = childLevel.internalId " @@ -3908,11 +3919,12 @@ " grandGrandChildLevel.publicId AS c3_string1, " " " + formatter.FormatNull("TEXT") + " AS c4_string2, " " " + formatter.FormatNull("TEXT") + " AS c5_string3, " - " " + formatter.FormatNull("INT") + " AS c6_int1, " - " " + formatter.FormatNull("INT") + " AS c7_int2, " - " " + formatter.FormatNull("INT") + " AS c8_int3, " - " " + formatter.FormatNull("BIGINT") + " AS c9_big_int1, " - " " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 " + " " + formatter.FormatNull("BYTEA") + " AS c6_string4, " + " " + formatter.FormatNull("INT") + " AS c7_int1, " + " " + formatter.FormatNull("INT") + " AS c8_int2, " + " " + formatter.FormatNull("INT") + " AS c9_int3, " + " " + formatter.FormatNull("BIGINT") + " AS c10_big_int1, " + " " + formatter.FormatNull("BIGINT") + " AS c11_big_int2 " "FROM Lookup " "INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId " "INNER JOIN Resources grandChildLevel ON childLevel.internalId = grandChildLevel.parentId " @@ -3952,9 +3964,10 @@ " " + formatter.FormatNull("TEXT") + " AS c3_string1, " " " + formatter.FormatNull("TEXT") + " AS c4_string2, " " " + formatter.FormatNull("TEXT") + " AS c5_string3, " - " " + formatter.FormatNull("INT") + " AS c6_int1, " - " " + formatter.FormatNull("INT") + " AS c7_int2, " - " " + formatter.FormatNull("INT") + " AS c8_int3, " + " " + formatter.FormatNull("BYTEA") + " AS c6_string4, " + " " + formatter.FormatNull("INT") + " AS c7_int1, " + " " + formatter.FormatNull("INT") + " AS c8_int2, " + " " + formatter.FormatNull("INT") + " AS c9_int3, " " COALESCE(" " (" + getResourcesGrandGrandChildCount + ")," " (SELECT COUNT(grandGrandChildLevel.internalId)" @@ -3962,8 +3975,8 @@ " INNER JOIN Resources AS grandChildLevel ON childLevel.internalId = grandChildLevel.parentId" " INNER JOIN Resources AS grandGrandChildLevel ON grandChildLevel.internalId = grandGrandChildLevel.parentId" " WHERE Lookup.internalId = childLevel.parentId" - " )) AS c9_big_int1, " - " " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 " + " )) AS c10_big_int1, " + " " + formatter.FormatNull("BIGINT") + " AS c11_big_int2 " "FROM Lookup "; } else @@ -3975,11 +3988,12 @@ " " + formatter.FormatNull("TEXT") + " AS c3_string1, " " " + formatter.FormatNull("TEXT") + " AS c4_string2, " " " + formatter.FormatNull("TEXT") + " AS c5_string3, " - " " + formatter.FormatNull("INT") + " AS c6_int1, " - " " + formatter.FormatNull("INT") + " AS c7_int2, " - " " + formatter.FormatNull("INT") + " AS c8_int3, " - " COUNT(grandChildLevel.internalId) AS c9_big_int1, " - " " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 " + " " + formatter.FormatNull("BYTEA") + " AS c6_string4, " + " " + formatter.FormatNull("INT") + " AS c7_int1, " + " " + formatter.FormatNull("INT") + " AS c8_int2, " + " " + formatter.FormatNull("INT") + " AS c9_int3, " + " COUNT(grandChildLevel.internalId) AS c10_big_int1, " + " " + formatter.FormatNull("BIGINT") + " AS c11_big_int2 " "FROM Lookup " "INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId " "INNER JOIN Resources grandChildLevel ON childLevel.internalId = grandChildLevel.parentId " @@ -4000,11 +4014,12 @@ " parentLevel.publicId AS c3_string1, " " " + formatter.FormatNull("TEXT") + " AS c4_string2, " " " + formatter.FormatNull("TEXT") + " AS c5_string3, " - " " + formatter.FormatNull("INT") + " AS c6_int1, " - " " + formatter.FormatNull("INT") + " AS c7_int2, " - " " + formatter.FormatNull("INT") + " AS c8_int3, " - " " + formatter.FormatNull("BIGINT") + " AS c9_big_int1, " - " " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 " + " " + formatter.FormatNull("BYTEA") + " AS c6_string4, " + " " + formatter.FormatNull("INT") + " AS c7_int1, " + " " + formatter.FormatNull("INT") + " AS c8_int2, " + " " + formatter.FormatNull("INT") + " AS c9_int3, " + " " + formatter.FormatNull("BIGINT") + " AS c10_big_int1, " + " " + formatter.FormatNull("BIGINT") + " AS c11_big_int2 " "FROM Lookup " " INNER JOIN Resources currentLevel ON currentLevel.internalId = Lookup.internalId " " INNER JOIN Resources parentLevel ON currentLevel.parentId = parentLevel.internalId "; @@ -4021,11 +4036,12 @@ " instancePublicId AS c3_string1, " " " + formatter.FormatNull("TEXT") + " AS c4_string2, " " " + formatter.FormatNull("TEXT") + " AS c5_string3, " - " " + formatter.FormatNull("INT") + " AS c6_int1, " - " " + formatter.FormatNull("INT") + " AS c7_int2, " - " " + formatter.FormatNull("INT") + " AS c8_int3, " - " instanceInternalId AS c9_big_int1, " - " " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 " + " " + formatter.FormatNull("BYTEA") + " AS c6_string4, " + " " + formatter.FormatNull("INT") + " AS c7_int1, " + " " + formatter.FormatNull("INT") + " AS c8_int2, " + " " + formatter.FormatNull("INT") + " AS c9_int3, " + " instanceInternalId AS c10_big_int1, " + " " + formatter.FormatNull("BIGINT") + " AS c11_big_int2 " " FROM OneInstance "; sql += " UNION ALL SELECT" @@ -4035,11 +4051,12 @@ " Metadata.value AS c3_string1, " " " + formatter.FormatNull("TEXT") + " AS c4_string2, " " " + formatter.FormatNull("TEXT") + " AS c5_string3, " - " Metadata.type AS c6_int1, " + " " + formatter.FormatNull("BYTEA") + " AS c6_string4, " + " Metadata.type AS c7_int1, " + revisionInC7 + - " " + formatter.FormatNull("INT") + " AS c8_int3, " - " " + formatter.FormatNull("BIGINT") + " AS c9_big_int1, " - " " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 " + " " + formatter.FormatNull("INT") + " AS c9_int3, " + " " + formatter.FormatNull("BIGINT") + " AS c10_big_int1, " + " " + formatter.FormatNull("BIGINT") + " AS c11_big_int2 " " FROM Metadata " " INNER JOIN OneInstance ON Metadata.id = OneInstance.instanceInternalId"; @@ -4050,11 +4067,12 @@ " uuid AS c3_string1, " " uncompressedHash AS c4_string2, " " compressedHash AS c5_string3, " - " fileType AS c6_int1, " + " customData AS c6_string4, " + " fileType AS c7_int1, " + revisionInC7 + - " compressionType AS c8_int3, " - " compressedSize AS c9_big_int1, " - " uncompressedSize AS c10_big_int2 " + " compressionType AS c9_int3, " + " compressedSize AS c10_big_int1, " + " uncompressedSize AS c11_big_int2 " " FROM AttachedFiles " " INNER JOIN OneInstance ON AttachedFiles.id = OneInstance.instanceInternalId"; @@ -4078,7 +4096,9 @@ } statement->Execute(formatter.GetDictionary()); - + + statement->SetResultFieldType(C6_STRING_4, ValueType_BinaryString); + // LOG(INFO) << sql; std::map<int64_t, Orthanc::DatabasePluginMessages::Find_Response*> responses; @@ -4110,8 +4130,8 @@ Orthanc::DatabasePluginMessages::Find_Response_Tag* tag = content->add_main_dicom_tags(); tag->set_value(statement->ReadString(C3_STRING_1)); - tag->set_group(statement->ReadInteger32(C6_INT_1)); - tag->set_element(statement->ReadInteger32(C7_INT_2)); + tag->set_group(statement->ReadInteger32(C7_INT_1)); + tag->set_element(statement->ReadInteger32(C8_INT_2)); }; break; case QUERY_PARENT_MAIN_DICOM_TAGS: @@ -4120,8 +4140,8 @@ Orthanc::DatabasePluginMessages::Find_Response_Tag* tag = content->add_main_dicom_tags(); tag->set_value(statement->ReadString(C3_STRING_1)); - tag->set_group(statement->ReadInteger32(C6_INT_1)); - tag->set_element(statement->ReadInteger32(C7_INT_2)); + tag->set_group(statement->ReadInteger32(C7_INT_1)); + tag->set_element(statement->ReadInteger32(C8_INT_2)); }; break; case QUERY_GRAND_PARENT_MAIN_DICOM_TAGS: @@ -4130,8 +4150,8 @@ Orthanc::DatabasePluginMessages::Find_Response_Tag* tag = content->add_main_dicom_tags(); tag->set_value(statement->ReadString(C3_STRING_1)); - tag->set_group(statement->ReadInteger32(C6_INT_1)); - tag->set_element(statement->ReadInteger32(C7_INT_2)); + tag->set_group(statement->ReadInteger32(C7_INT_1)); + tag->set_element(statement->ReadInteger32(C8_INT_2)); }; break; case QUERY_CHILDREN_IDENTIFIERS: @@ -4144,7 +4164,7 @@ case QUERY_CHILDREN_COUNT: { Orthanc::DatabasePluginMessages::Find_Response_ChildrenContent* content = GetChildrenContent(responses[internalId], static_cast<Orthanc::DatabasePluginMessages::ResourceType>(request.level() + 1)); - content->set_count(statement->ReadInteger64(C9_BIG_INT_1)); + content->set_count(statement->ReadInteger64(C10_BIG_INT_1)); }; break; case QUERY_CHILDREN_MAIN_DICOM_TAGS: @@ -4152,8 +4172,8 @@ Orthanc::DatabasePluginMessages::Find_Response_ChildrenContent* content = GetChildrenContent(responses[internalId], static_cast<Orthanc::DatabasePluginMessages::ResourceType>(request.level() + 1)); Orthanc::DatabasePluginMessages::Find_Response_Tag* tag = content->add_main_dicom_tags(); tag->set_value(statement->ReadString(C3_STRING_1)); // TODO: handle sequences ?? - tag->set_group(statement->ReadInteger32(C6_INT_1)); - tag->set_element(statement->ReadInteger32(C7_INT_2)); + tag->set_group(statement->ReadInteger32(C7_INT_1)); + tag->set_element(statement->ReadInteger32(C8_INT_2)); }; break; case QUERY_CHILDREN_METADATA: @@ -4162,7 +4182,7 @@ Orthanc::DatabasePluginMessages::Find_Response_Metadata* metadata = content->add_metadata(); metadata->set_value(statement->ReadString(C3_STRING_1)); - metadata->set_key(statement->ReadInteger32(C6_INT_1)); + metadata->set_key(statement->ReadInteger32(C7_INT_1)); metadata->set_revision(0); // Setting a revision is not required in this case, as of Orthanc 1.12.5 }; break; @@ -4176,7 +4196,7 @@ case QUERY_GRAND_CHILDREN_COUNT: { Orthanc::DatabasePluginMessages::Find_Response_ChildrenContent* content = GetChildrenContent(responses[internalId], static_cast<Orthanc::DatabasePluginMessages::ResourceType>(request.level() + 2)); - content->set_count(statement->ReadInteger64(C9_BIG_INT_1)); + content->set_count(statement->ReadInteger64(C10_BIG_INT_1)); }; break; case QUERY_GRAND_CHILDREN_MAIN_DICOM_TAGS: @@ -4185,8 +4205,8 @@ Orthanc::DatabasePluginMessages::Find_Response_Tag* tag = content->add_main_dicom_tags(); tag->set_value(statement->ReadString(C3_STRING_1)); // TODO: handle sequences ?? - tag->set_group(statement->ReadInteger32(C6_INT_1)); - tag->set_element(statement->ReadInteger32(C7_INT_2)); + tag->set_group(statement->ReadInteger32(C7_INT_1)); + tag->set_element(statement->ReadInteger32(C8_INT_2)); }; break; case QUERY_GRAND_CHILDREN_METADATA: @@ -4195,7 +4215,7 @@ Orthanc::DatabasePluginMessages::Find_Response_Metadata* metadata = content->add_metadata(); metadata->set_value(statement->ReadString(C3_STRING_1)); - metadata->set_key(statement->ReadInteger32(C6_INT_1)); + metadata->set_key(statement->ReadInteger32(C7_INT_1)); metadata->set_revision(0); // Setting a revision is not required in this case, as of Orthanc 1.12.5 }; break; @@ -4209,7 +4229,7 @@ case QUERY_GRAND_GRAND_CHILDREN_COUNT: { Orthanc::DatabasePluginMessages::Find_Response_ChildrenContent* content = GetChildrenContent(responses[internalId], static_cast<Orthanc::DatabasePluginMessages::ResourceType>(request.level() + 3)); - content->set_count(statement->ReadInteger64(C9_BIG_INT_1)); + content->set_count(statement->ReadInteger64(C10_BIG_INT_1)); }; break; case QUERY_ATTACHMENTS: @@ -4219,14 +4239,19 @@ attachment->set_uuid(statement->ReadString(C3_STRING_1)); attachment->set_uncompressed_hash(statement->ReadString(C4_STRING_2)); attachment->set_compressed_hash(statement->ReadString(C5_STRING_3)); - attachment->set_content_type(statement->ReadInteger32(C6_INT_1)); - attachment->set_compression_type(statement->ReadInteger32(C8_INT_3)); - attachment->set_compressed_size(statement->ReadInteger64(C9_BIG_INT_1)); - attachment->set_uncompressed_size(statement->ReadInteger64(C10_BIG_INT_2)); - - if (!statement->IsNull(C7_INT_2)) // revision can be null for files that have been atttached by older Orthanc versions + + attachment->set_content_type(statement->ReadInteger32(C7_INT_1)); + attachment->set_compression_type(statement->ReadInteger32(C9_INT_3)); + attachment->set_compressed_size(statement->ReadInteger64(C10_BIG_INT_1)); + attachment->set_uncompressed_size(statement->ReadInteger64(C11_BIG_INT_2)); + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 8) + attachment->set_custom_data(statement->ReadStringOrNull(C6_STRING_4)); +#endif + + if (!statement->IsNull(C8_INT_2)) // revision can be null for files that have been atttached by older Orthanc versions { - responses[internalId]->add_attachments_revisions(statement->ReadInteger32(C7_INT_2)); + responses[internalId]->add_attachments_revisions(statement->ReadInteger32(C8_INT_2)); } else { @@ -4240,11 +4265,11 @@ Orthanc::DatabasePluginMessages::Find_Response_Metadata* metadata = content->add_metadata(); metadata->set_value(statement->ReadString(C3_STRING_1)); - metadata->set_key(statement->ReadInteger32(C6_INT_1)); + metadata->set_key(statement->ReadInteger32(C7_INT_1)); - if (!statement->IsNull(C7_INT_2)) // revision can be null for metadata that have been created by older Orthanc versions + if (!statement->IsNull(C8_INT_2)) // revision can be null for metadata that have been created by older Orthanc versions { - metadata->set_revision(statement->ReadInteger32(C7_INT_2)); + metadata->set_revision(statement->ReadInteger32(C8_INT_2)); } else { @@ -4258,11 +4283,11 @@ Orthanc::DatabasePluginMessages::Find_Response_Metadata* metadata = content->add_metadata(); metadata->set_value(statement->ReadString(C3_STRING_1)); - metadata->set_key(statement->ReadInteger32(C6_INT_1)); - - if (!statement->IsNull(C7_INT_2)) // revision can be null for metadata that have been created by older Orthanc versions + metadata->set_key(statement->ReadInteger32(C7_INT_1)); + + if (!statement->IsNull(C8_INT_2)) // revision can be null for metadata that have been created by older Orthanc versions { - metadata->set_revision(statement->ReadInteger32(C7_INT_2)); + metadata->set_revision(statement->ReadInteger32(C8_INT_2)); } else { @@ -4276,11 +4301,11 @@ Orthanc::DatabasePluginMessages::Find_Response_Metadata* metadata = content->add_metadata(); metadata->set_value(statement->ReadString(C3_STRING_1)); - metadata->set_key(statement->ReadInteger32(C6_INT_1)); - - if (!statement->IsNull(C7_INT_2)) // revision can be null for metadata that have been created by older Orthanc versions + metadata->set_key(statement->ReadInteger32(C7_INT_1)); + + if (!statement->IsNull(C8_INT_2)) // revision can be null for metadata that have been created by older Orthanc versions { - metadata->set_revision(statement->ReadInteger32(C7_INT_2)); + metadata->set_revision(statement->ReadInteger32(C8_INT_2)); } else { @@ -4302,11 +4327,11 @@ Orthanc::DatabasePluginMessages::Find_Response_Metadata* metadata = responses[internalId]->add_one_instance_metadata(); metadata->set_value(statement->ReadString(C3_STRING_1)); - metadata->set_key(statement->ReadInteger32(C6_INT_1)); - - if (!statement->IsNull(C7_INT_2)) // revision can be null for metadata that have been created by older Orthanc versions + metadata->set_key(statement->ReadInteger32(C7_INT_1)); + + if (!statement->IsNull(C8_INT_2)) // revision can be null for metadata that have been created by older Orthanc versions { - metadata->set_revision(statement->ReadInteger32(C7_INT_2)); + metadata->set_revision(statement->ReadInteger32(C8_INT_2)); } else { @@ -4320,10 +4345,14 @@ attachment->set_uuid(statement->ReadString(C3_STRING_1)); attachment->set_uncompressed_hash(statement->ReadString(C4_STRING_2)); attachment->set_compressed_hash(statement->ReadString(C5_STRING_3)); - attachment->set_content_type(statement->ReadInteger32(C6_INT_1)); - attachment->set_compression_type(statement->ReadInteger32(C8_INT_3)); - attachment->set_compressed_size(statement->ReadInteger64(C9_BIG_INT_1)); - attachment->set_uncompressed_size(statement->ReadInteger64(C10_BIG_INT_2)); + attachment->set_content_type(statement->ReadInteger32(C7_INT_1)); + attachment->set_compression_type(statement->ReadInteger32(C9_INT_3)); + attachment->set_compressed_size(statement->ReadInteger64(C10_BIG_INT_1)); + attachment->set_uncompressed_size(statement->ReadInteger64(C11_BIG_INT_2)); + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 8) + attachment->set_custom_data(statement->ReadStringOrNull(C6_STRING_4)); +#endif }; break; default: @@ -4333,4 +4362,325 @@ } } #endif + +#if ORTHANC_PLUGINS_HAS_KEY_VALUE_STORES == 1 + void IndexBackend::StoreKeyValue(DatabaseManager& manager, + const std::string& storeId, + const std::string& key, + const std::string& value) + { + // TODO: that probably needs to be adapted in ODBC and MySQL + DatabaseManager::CachedStatement statement( + STATEMENT_FROM_HERE, manager, + "INSERT INTO KeyValueStores VALUES(${storeId}, ${key}, ${value}) ON CONFLICT (storeId, key) DO UPDATE SET value = EXCLUDED.value;"); + + statement.SetParameterType("storeId", ValueType_Utf8String); + statement.SetParameterType("key", ValueType_Utf8String); + statement.SetParameterType("value", ValueType_BinaryString); + + Dictionary args; + args.SetUtf8Value("storeId", storeId); + args.SetUtf8Value("key", key); + args.SetBinaryValue("value", value); + + statement.Execute(args); + } + + void IndexBackend::DeleteKeyValue(DatabaseManager& manager, + const std::string& storeId, + const std::string& key) + { + DatabaseManager::CachedStatement statement( + STATEMENT_FROM_HERE, manager, + "DELETE FROM KeyValueStores WHERE storeId = ${storeId} AND key = ${key}"); + + 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(std::string& value, + DatabaseManager& manager, + const std::string& storeId, + const std::string& key) + { + DatabaseManager::CachedStatement statement( + STATEMENT_FROM_HERE, manager, + "SELECT value FROM KeyValueStores WHERE storeId = ${storeId} AND key = ${key}"); + + statement.SetReadOnly(true); + 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()) + { + return false; + } + else + { + value = statement.ReadString(0); + return true; + } + } + + void IndexBackend::ListKeysValues(Orthanc::DatabasePluginMessages::TransactionResponse& response, + DatabaseManager& manager, + const Orthanc::DatabasePluginMessages::ListKeysValues_Request& request) + { + response.mutable_list_keys_values()->Clear(); + + LookupFormatter formatter(manager.GetDialect()); + + std::unique_ptr<DatabaseManager::CachedStatement> statement; + + std::string storeIdParameter = formatter.GenerateParameter(request.store_id()); + + if (request.from_first()) + { + statement.reset(new DatabaseManager::CachedStatement( + STATEMENT_FROM_HERE, manager, + "SELECT key, value FROM KeyValueStores WHERE storeId= " + storeIdParameter + " ORDER BY key ASC " + formatter.FormatLimits(0, request.limit()))); + } + 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) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + statement->SetResultFieldType(0, ValueType_Utf8String); + statement->SetResultFieldType(1, ValueType_BinaryString); + + while (!statement->IsDone()) + { + Orthanc::DatabasePluginMessages::ListKeysValues_Response_KeyValue* kv = response.mutable_list_keys_values()->add_keys_values(); + kv->set_key(statement->ReadString(0)); + kv->set_value(statement->ReadString(1)); + + statement->Next(); + } + } + + } +#endif + +#if ORTHANC_PLUGINS_HAS_QUEUES == 1 + void IndexBackend::EnqueueValue(DatabaseManager& manager, + const std::string& queueId, + const std::string& value) + { + DatabaseManager::CachedStatement statement( + STATEMENT_FROM_HERE, manager, + "INSERT INTO Queues VALUES(${AUTOINCREMENT} ${queueId}, ${value})"); + + statement.SetParameterType("queueId", ValueType_Utf8String); + statement.SetParameterType("value", ValueType_BinaryString); + + Dictionary args; + args.SetUtf8Value("queueId", queueId); + args.SetBinaryValue("value", value); + + statement.Execute(args); + } + + + bool IndexBackend::DequeueValueSQLite(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) + { + 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; + } + } + + + 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) + { + DatabaseManager::CachedStatement statement( + STATEMENT_FROM_HERE, manager, + "SELECT COUNT(*) FROM Queues WHERE queueId = ${queueId}"); + + statement.SetReadOnly(true); + statement.SetParameterType("queueId", ValueType_Utf8String); + + Dictionary args; + args.SetUtf8Value("queueId", queueId); + + statement.Execute(args); + statement.SetResultFieldType(0, ValueType_Integer64); + + return statement.ReadInteger64(0); + } +#endif + +#if ORTHANC_PLUGINS_HAS_ATTACHMENTS_CUSTOM_DATA == 1 + void IndexBackend::GetAttachmentCustomData(std::string& customData, + DatabaseManager& manager, + const std::string& attachmentUuid) + { + DatabaseManager::CachedStatement statement( + STATEMENT_FROM_HERE, manager, + "SELECT customData FROM AttachedFiles WHERE uuid = ${uuid}"); + + statement.SetReadOnly(true); + statement.SetParameterType("uuid", ValueType_Utf8String); + + Dictionary args; + args.SetUtf8Value("uuid", attachmentUuid); + + statement.Execute(args); + statement.SetResultFieldType(8, ValueType_BinaryString); + + if (statement.IsDone()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource, "Nonexistent attachment: " + attachmentUuid); + } + else + { + customData = statement.ReadStringOrNull(0); + } + } + + void IndexBackend::SetAttachmentCustomData(DatabaseManager& manager, + const std::string& attachmentUuid, + const std::string& customData) + { + DatabaseManager::CachedStatement statement( + STATEMENT_FROM_HERE, manager, + "UPDATE AttachedFiles SET customData = ${customData} WHERE uuid = ${uuid}"); + + statement.SetParameterType("uuid", ValueType_Utf8String); + statement.SetParameterType("customData", ValueType_Utf8String); + + Dictionary args; + args.SetUtf8Value("uuid", attachmentUuid); + args.SetBinaryValue("customData", customData); + + statement.Execute(args); + } +#endif }
--- a/Framework/Plugins/IndexBackend.h Mon May 19 11:18:43 2025 +0200 +++ b/Framework/Plugins/IndexBackend.h Fri Jun 13 15:16:52 2025 +0200 @@ -84,6 +84,13 @@ 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, @@ -102,7 +109,16 @@ int64_t id, const OrthancPluginAttachment& attachment, int64_t revision) ORTHANC_OVERRIDE; - + +#if ORTHANC_PLUGINS_HAS_ATTACHMENTS_CUSTOM_DATA == 1 + // New in Orthanc 1.12.8 + virtual void AddAttachment(DatabaseManager& manager, + int64_t id, + const OrthancPluginAttachment& attachment, + int64_t revision, + const std::string& customData) ORTHANC_OVERRIDE; +#endif + virtual void AttachChild(DatabaseManager& manager, int64_t parent, int64_t child) ORTHANC_OVERRIDE; @@ -459,6 +475,52 @@ const Orthanc::DatabasePluginMessages::Find_Request& request) ORTHANC_OVERRIDE; #endif +#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; + + virtual void DeleteKeyValue(DatabaseManager& manager, + 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, + const Orthanc::DatabasePluginMessages::ListKeysValues_Request& request) ORTHANC_OVERRIDE; +#endif + +#if ORTHANC_PLUGINS_HAS_QUEUES == 1 + virtual void EnqueueValue(DatabaseManager& manager, + const std::string& queueId, + const std::string& value) ORTHANC_OVERRIDE; + + virtual bool DequeueValue(std::string& value, + DatabaseManager& manager, + const std::string& queueId, + bool fromFront) ORTHANC_OVERRIDE; + + virtual uint64_t GetQueueSize(DatabaseManager& manager, + const std::string& queueId) ORTHANC_OVERRIDE; + +#endif + +#if ORTHANC_PLUGINS_HAS_ATTACHMENTS_CUSTOM_DATA == 1 + virtual void GetAttachmentCustomData(std::string& customData, + DatabaseManager& manager, + const std::string& attachmentUuid) ORTHANC_OVERRIDE; + + virtual void SetAttachmentCustomData(DatabaseManager& manager, + const std::string& attachmentUuid, + const std::string& customData) ORTHANC_OVERRIDE; + +#endif + virtual bool HasPerformDbHousekeeping() ORTHANC_OVERRIDE { return false;
--- a/Framework/Plugins/IndexUnitTests.h Mon May 19 11:18:43 2025 +0200 +++ b/Framework/Plugins/IndexUnitTests.h Fri Jun 13 15:16:52 2025 +0200 @@ -217,6 +217,102 @@ } +#if ORTHANC_PLUGINS_HAS_KEY_VALUE_STORES == 1 +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); + } + } + } + } +} +#endif + + +static void FillBlob(std::string& blob) +{ + blob.clear(); + blob.push_back(0); + blob.push_back(1); + blob.push_back(0); + blob.push_back(2); +} + + +static void CheckBlob(const std::string& s) +{ + 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])); +} + + TEST(IndexBackend, Basic) { using namespace OrthancDatabases; @@ -250,6 +346,13 @@ std::unique_ptr<IDatabaseBackendOutput> output(db.CreateOutput()); + { + // Sanity check + std::string blob; + FillBlob(blob); + CheckBlob(blob); + } + std::string s; ASSERT_TRUE(db.LookupGlobalProperty(s, *manager, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabaseSchemaVersion)); ASSERT_EQ("6", s); @@ -422,7 +525,12 @@ a2.compressedSize = 4242; a2.compressedHash = "md5_2"; +#if ORTHANC_PLUGINS_HAS_ATTACHMENTS_CUSTOM_DATA == 1 + db.AddAttachment(*manager, a, a1, 42, "my_custom_data"); +#else db.AddAttachment(*manager, a, a1, 42); +#endif + db.ListAvailableAttachments(fc, *manager, a); ASSERT_EQ(1u, fc.size()); ASSERT_EQ(Orthanc::FileContentType_Dicom, fc.front()); @@ -431,6 +539,32 @@ ASSERT_EQ(2u, fc.size()); ASSERT_FALSE(db.LookupAttachment(*output, revision, *manager, b, Orthanc::FileContentType_Dicom)); +#if ORTHANC_PLUGINS_HAS_ATTACHMENTS_CUSTOM_DATA == 1 + { + std::string s; + ASSERT_THROW(db.GetAttachmentCustomData(s, *manager, "nope"), Orthanc::OrthancException); + + db.GetAttachmentCustomData(s, *manager, "uuid1"); + ASSERT_EQ("my_custom_data", s); + + db.GetAttachmentCustomData(s, *manager, "uuid2"); + ASSERT_TRUE(s.empty()); + + { + std::string blob; + FillBlob(blob); + db.SetAttachmentCustomData(*manager, "uuid1", blob); + } + + db.GetAttachmentCustomData(s, *manager, "uuid1"); + CheckBlob(s); + + db.SetAttachmentCustomData(*manager, "uuid1", ""); + db.GetAttachmentCustomData(s, *manager, "uuid1"); + ASSERT_TRUE(s.empty()); + } +#endif + ASSERT_EQ(4284u, db.GetTotalCompressedSize(*manager)); ASSERT_EQ(4284u, db.GetTotalUncompressedSize(*manager)); @@ -803,5 +937,125 @@ } #endif + +#if ORTHANC_PLUGINS_HAS_KEY_VALUE_STORES == 1 + { + 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; + FillBlob(blob); + db.StoreKeyValue(*manager, "test", "blob", blob); // Storing binary values + } + + ASSERT_TRUE(db.GetKeyValue(s, *manager, "test", "blob")); + CheckBlob(s); + db.DeleteKeyValue(*manager, "test", "blob"); + ASSERT_FALSE(db.GetKeyValue(s, *manager, "test", "blob")); + + manager->CommitTransaction(); + } +#endif + + +#if ORTHANC_PLUGINS_HAS_QUEUES == 1 + { + 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; + FillBlob(blob); + db.EnqueueValue(*manager, "test", blob); // Storing binary values + } + + ASSERT_TRUE(db.DequeueValue(s, *manager, "test", true)); + CheckBlob(s); + + ASSERT_FALSE(db.DequeueValue(s, *manager, "test", true)); + + ASSERT_EQ(1u, db.GetQueueSize(*manager, "another")); + + manager->CommitTransaction(); + } +#endif + manager->Close(); }
--- a/Framework/Plugins/MessagesToolbox.h Mon May 19 11:18:43 2025 +0200 +++ b/Framework/Plugins/MessagesToolbox.h Fri Jun 13 15:16:52 2025 +0200 @@ -40,24 +40,31 @@ #define ORTHANC_PLUGINS_HAS_INTEGRATED_FIND 0 +#define ORTHANC_PLUGINS_HAS_CHANGES_EXTENDED 0 #if defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE) # if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 5) # undef ORTHANC_PLUGINS_HAS_INTEGRATED_FIND # define ORTHANC_PLUGINS_HAS_INTEGRATED_FIND 1 -# endif -#endif - - -#define ORTHANC_PLUGINS_HAS_CHANGES_EXTENDED 0 - -#if defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE) -# if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 5) # undef ORTHANC_PLUGINS_HAS_CHANGES_EXTENDED # define ORTHANC_PLUGINS_HAS_CHANGES_EXTENDED 1 # endif #endif +#define ORTHANC_PLUGINS_HAS_ATTACHMENTS_CUSTOM_DATA 0 +#define ORTHANC_PLUGINS_HAS_KEY_VALUE_STORES 0 +#define ORTHANC_PLUGINS_HAS_QUEUES 0 + +#if defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE) +# if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 8) +# undef ORTHANC_PLUGINS_HAS_ATTACHMENTS_CUSTOM_DATA +# define ORTHANC_PLUGINS_HAS_ATTACHMENTS_CUSTOM_DATA 1 +# undef ORTHANC_PLUGINS_HAS_KEY_VALUE_STORES +# define ORTHANC_PLUGINS_HAS_KEY_VALUE_STORES 1 +# undef ORTHANC_PLUGINS_HAS_QUEUES +# define ORTHANC_PLUGINS_HAS_QUEUES 1 +# endif +#endif #include <Enumerations.h>
--- a/Framework/PostgreSQL/PostgreSQLParameters.cpp Mon May 19 11:18:43 2025 +0200 +++ b/Framework/PostgreSQL/PostgreSQLParameters.cpp Fri Jun 13 15:16:52 2025 +0200 @@ -44,6 +44,7 @@ maxConnectionRetries_ = 10; connectionRetryInterval_ = 5; isVerboseEnabled_ = false; + allowInconsistentChildCounts_ = false; isolationMode_ = IsolationMode_Serializable; }
--- a/Framework/PostgreSQL/PostgreSQLResult.cpp Mon May 19 11:18:43 2025 +0200 +++ b/Framework/PostgreSQL/PostgreSQLResult.cpp Fri Jun 13 15:16:52 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 Mon May 19 11:18:43 2025 +0200 +++ b/Framework/PostgreSQL/PostgreSQLResult.h Fri Jun 13 15:16:52 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 Mon May 19 11:18:43 2025 +0200 +++ b/Framework/PostgreSQL/PostgreSQLStatement.cpp Fri Jun 13 15:16:52 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); } } @@ -528,42 +537,44 @@ { const std::string& name = formatter_.GetParameterName(i); - switch (formatter_.GetParameterType(i)) - { - case ValueType_Integer64: - BindInteger64(i, dynamic_cast<const Integer64Value&>(parameters.GetValue(name)).GetValue()); - break; + const IValue& value = parameters.GetValue(name); - case ValueType_Integer32: - BindInteger(i, dynamic_cast<const Integer32Value&>(parameters.GetValue(name)).GetValue()); - break; + if (value.GetType() == ValueType_Null || value.IsNull()) + { + BindNull(i); + } + else + { + switch (formatter_.GetParameterType(i)) + { + case ValueType_Integer64: + BindInteger64(i, dynamic_cast<const Integer64Value&>(value).GetValue()); + break; - case ValueType_Null: - BindNull(i); - break; + case ValueType_Integer32: + BindInteger(i, dynamic_cast<const Integer32Value&>(value).GetValue()); + break; - case ValueType_Utf8String: - BindString(i, dynamic_cast<const Utf8StringValue&> - (parameters.GetValue(name)).GetContent()); - break; + case ValueType_Utf8String: + BindString(i, dynamic_cast<const Utf8StringValue&>(value).GetContent()); + break; - case ValueType_BinaryString: - BindString(i, dynamic_cast<const BinaryStringValue&> - (parameters.GetValue(name)).GetContent()); - break; + case ValueType_BinaryString: + BindString(i, dynamic_cast<const BinaryStringValue&>(value).GetContent()); + break; - case ValueType_InputFile: - { - const InputFileValue& blob = - dynamic_cast<const InputFileValue&>(parameters.GetValue(name)); + case ValueType_InputFile: + { + const InputFileValue& blob = dynamic_cast<const InputFileValue&>(value); - PostgreSQLLargeObject largeObject(database_, blob.GetContent()); - BindLargeObject(i, largeObject); - break; + PostgreSQLLargeObject largeObject(database_, blob.GetContent()); + BindLargeObject(i, largeObject); + break; + } + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); } - - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); } }
--- a/MySQL/CMakeLists.txt Mon May 19 11:18:43 2025 +0200 +++ b/MySQL/CMakeLists.txt Fri Jun 13 15:16:52 2025 +0200 @@ -19,7 +19,7 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. -cmake_minimum_required(VERSION 2.8) +cmake_minimum_required(VERSION 2.8...4.0) project(OrthancMySQL) set(ORTHANC_PLUGIN_VERSION "mainline") @@ -91,6 +91,7 @@ MYSQL_PREPARE_INDEX ${CMAKE_SOURCE_DIR}/Plugins/PrepareIndex.sql MYSQL_GET_LAST_CHANGE_INDEX ${CMAKE_SOURCE_DIR}/Plugins/GetLastChangeIndex.sql MYSQL_CREATE_INSTANCE ${CMAKE_SOURCE_DIR}/Plugins/CreateInstance.sql + MYSQL_INSTALL_REVISION_AND_CUSTOM_DATA ${CMAKE_SOURCE_DIR}/Plugins/InstallRevisionAndCustomData.sql MYSQL_DELETE_RESOURCES ${CMAKE_SOURCE_DIR}/Plugins/DeleteResources.sql )
--- a/MySQL/NEWS Mon May 19 11:18:43 2025 +0200 +++ b/MySQL/NEWS Fri Jun 13 15:16:52 2025 +0200 @@ -1,6 +1,8 @@ Pending changes in the mainline =============================== +* Added support for customData in AttachedFiles +* Added support for revision in AttachedFiles & Metadata * Added support for ExtendedChanges: - changes?type=...&to=... * Added support for ExtendedFind
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MySQL/Plugins/InstallRevisionAndCustomData.sql Fri Jun 13 15:16:52 2025 +0200 @@ -0,0 +1,28 @@ +ALTER TABLE AttachedFiles ADD COLUMN revision INTEGER; +ALTER TABLE DeletedFiles ADD COLUMN revision INTEGER; +ALTER TABLE Metadata ADD COLUMN revision INTEGER; + +ALTER TABLE AttachedFiles ADD COLUMN customData LONGTEXT; +ALTER TABLE DeletedFiles ADD COLUMN customData LONGTEXT; + +DROP TRIGGER AttachedFileDeleted; + +CREATE TRIGGER AttachedFileDeleted +AFTER DELETE ON AttachedFiles +FOR EACH ROW + BEGIN + INSERT INTO DeletedFiles VALUES(old.uuid, old.filetype, old.compressedSize, + old.uncompressedSize, old.compressionType, + old.uncompressedHash, old.compressedHash, + old.revision, old.customData)@ + END; + + +DROP TRIGGER ResourceDeleted; + +CREATE TRIGGER ResourceDeleted +BEFORE DELETE ON Resources +FOR EACH ROW + BEGIN + INSERT INTO DeletedFiles SELECT uuid, fileType, compressedSize, uncompressedSize, compressionType, uncompressedHash, compressedHash, revision, customData FROM AttachedFiles WHERE id=old.internalId@ + END; \ No newline at end of file
--- a/MySQL/Plugins/MySQLIndex.cpp Mon May 19 11:18:43 2025 +0200 +++ b/MySQL/Plugins/MySQLIndex.cpp Fri Jun 13 15:16:52 2025 +0200 @@ -338,8 +338,26 @@ t.Commit(); } + if (revision == 8) + { + DatabaseManager::Transaction t(manager, TransactionType_ReadWrite); + + // Install revision and customData extension + std::string query; + + Orthanc::EmbeddedResources::GetFileResource + (query, Orthanc::EmbeddedResources::MYSQL_INSTALL_REVISION_AND_CUSTOM_DATA); - if (revision != 8) + // Need to escape arobases: Don't use "t.GetDatabaseTransaction().ExecuteMultiLines()" here + db.ExecuteMultiLines(query, true); + + revision = 9; + SetGlobalIntegerProperty(manager, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabasePatchLevel, revision); + + t.Commit(); + } + + if (revision != 9) { LOG(ERROR) << "MySQL plugin is incompatible with database schema revision: " << revision; throw Orthanc::OrthancException(Orthanc::ErrorCode_Database);
--- a/MySQL/Plugins/MySQLIndex.h Mon May 19 11:18:43 2025 +0200 +++ b/MySQL/Plugins/MySQLIndex.h Fri Jun 13 15:16:52 2025 +0200 @@ -58,9 +58,24 @@ virtual bool HasRevisionsSupport() const ORTHANC_OVERRIDE { - return false; // TODO - REVISIONS + return true; + } + + virtual bool HasAttachmentCustomDataSupport() const ORTHANC_OVERRIDE + { + return true; } + virtual bool HasKeyValueStores() const ORTHANC_OVERRIDE + { + return false; + } + + virtual bool HasQueues() const ORTHANC_OVERRIDE + { + return false; + } + virtual int64_t CreateResource(DatabaseManager& manager, const char* publicId, OrthancPluginResourceType type)
--- a/MySQL/Plugins/PrepareIndex.sql Mon May 19 11:18:43 2025 +0200 +++ b/MySQL/Plugins/PrepareIndex.sql Fri Jun 13 15:16:52 2025 +0200 @@ -48,6 +48,8 @@ compressionType INTEGER, uncompressedHash VARCHAR(40), compressedHash VARCHAR(40), + -- revision INTEGER, -- new in v 4.X, added in MySQLIndex::ConfigureDatabase + -- customData LONGTEXT, -- new in v 4.X, added in MySQLIndex::ConfigureDatabase PRIMARY KEY(id, fileType), CONSTRAINT AttachedFiles1 FOREIGN KEY (id) REFERENCES Resources(internalId) ON DELETE CASCADE ); @@ -104,6 +106,8 @@ compressionType INTEGER, -- 4 uncompressedHash VARCHAR(40), -- 5 compressedHash VARCHAR(40) -- 6 + -- revision INTEGER, -- new in v 4.X, added in MySQLIndex::ConfigureDatabase + -- customData LONGTEXT, -- new in v 4.X, added in MySQLIndex::ConfigureDatabase ); -- End of differences @@ -119,6 +123,8 @@ INSERT INTO DeletedFiles VALUES(old.uuid, old.filetype, old.compressedSize, old.uncompressedSize, old.compressionType, old.uncompressedHash, old.compressedHash)@ + -- old.revision, old.customData -- new in v 4.X, added in MySQLIndex::ConfigureDatabase + END; @@ -127,6 +133,7 @@ FOR EACH ROW BEGIN INSERT INTO DeletedFiles SELECT uuid, fileType, compressedSize, uncompressedSize, compressionType, uncompressedHash, compressedHash FROM AttachedFiles WHERE id=old.internalId@ + -- revision, customData -- new in v 4.X, added in MySQLIndex::ConfigureDatabase END;
--- a/Odbc/CMakeLists.txt Mon May 19 11:18:43 2025 +0200 +++ b/Odbc/CMakeLists.txt Fri Jun 13 15:16:52 2025 +0200 @@ -19,7 +19,7 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. -cmake_minimum_required(VERSION 2.8) +cmake_minimum_required(VERSION 2.8...4.0) project(OrthancOdbc) set(ORTHANC_PLUGIN_VERSION "mainline")
--- a/Odbc/NEWS Mon May 19 11:18:43 2025 +0200 +++ b/Odbc/NEWS Fri Jun 13 15:16:52 2025 +0200 @@ -21,7 +21,6 @@ * Now detecting communication link failure with the DB and retrying to connect. * Fixed "MaximumConnectionRetries" configuration that was not taken into account. - Release 1.1 (2021-12-06) ========================
--- a/Odbc/Plugins/OdbcIndex.cpp Mon May 19 11:18:43 2025 +0200 +++ b/Odbc/Plugins/OdbcIndex.cpp Fri Jun 13 15:16:52 2025 +0200 @@ -163,6 +163,49 @@ return OdbcDatabase::CreateDatabaseFactory(maxConnectionRetries_, connectionRetryInterval_, connectionString_, true); } + static void AdaptTypesToDialect(std::string& sql, Dialect dialect) + { + switch (dialect) + { + case Dialect_SQLite: + boost::replace_all(sql, "${LONGTEXT}", "TEXT"); + boost::replace_all(sql, "${AUTOINCREMENT_TYPE}", "INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT"); + boost::replace_all(sql, "${AUTOINCREMENT_INSERT}", "NULL, "); + break; + + case Dialect_PostgreSQL: + boost::replace_all(sql, "${LONGTEXT}", "TEXT"); + boost::replace_all(sql, "${AUTOINCREMENT_TYPE}", "BIGSERIAL NOT NULL PRIMARY KEY"); + boost::replace_all(sql, "${AUTOINCREMENT_INSERT}", "DEFAULT, "); + break; + + case Dialect_MySQL: + boost::replace_all(sql, "${LONGTEXT}", "LONGTEXT"); + boost::replace_all(sql, "${AUTOINCREMENT_TYPE}", "BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY"); + boost::replace_all(sql, "${AUTOINCREMENT_INSERT}", "NULL, "); + break; + + case Dialect_MSSQL: + /** + * cf. OMSSQL-5: Use VARCHAR(MAX) instead of TEXT: (1) + * Microsoft issued a warning stating that "ntext, text, and + * image data types will be removed in a future version of + * SQL Server" + * (https://msdn.microsoft.com/en-us/library/ms187993.aspx), + * and (2) SQL Server does not support comparison of TEXT + * with '=' operator (e.g. in WHERE statements such as + * IndexBackend::LookupIdentifier())." + **/ + boost::replace_all(sql, "${LONGTEXT}", "VARCHAR(MAX)"); + boost::replace_all(sql, "${AUTOINCREMENT_TYPE}", "BIGINT IDENTITY NOT NULL PRIMARY KEY"); + boost::replace_all(sql, "${AUTOINCREMENT_INSERT}", ""); + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + } + void OdbcIndex::ConfigureDatabase(DatabaseManager& manager, bool hasIdentifierTags, @@ -190,46 +233,8 @@ { std::string sql; Orthanc::EmbeddedResources::GetFileResource(sql, Orthanc::EmbeddedResources::ODBC_PREPARE_INDEX); - - switch (db.GetDialect()) - { - case Dialect_SQLite: - boost::replace_all(sql, "${LONGTEXT}", "TEXT"); - boost::replace_all(sql, "${AUTOINCREMENT_TYPE}", "INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT"); - boost::replace_all(sql, "${AUTOINCREMENT_INSERT}", "NULL, "); - break; - - case Dialect_PostgreSQL: - boost::replace_all(sql, "${LONGTEXT}", "TEXT"); - boost::replace_all(sql, "${AUTOINCREMENT_TYPE}", "BIGSERIAL NOT NULL PRIMARY KEY"); - boost::replace_all(sql, "${AUTOINCREMENT_INSERT}", "DEFAULT, "); - break; - - case Dialect_MySQL: - boost::replace_all(sql, "${LONGTEXT}", "LONGTEXT"); - boost::replace_all(sql, "${AUTOINCREMENT_TYPE}", "BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY"); - boost::replace_all(sql, "${AUTOINCREMENT_INSERT}", "NULL, "); - break; - - case Dialect_MSSQL: - /** - * cf. OMSSQL-5: Use VARCHAR(MAX) instead of TEXT: (1) - * Microsoft issued a warning stating that "ntext, text, and - * image data types will be removed in a future version of - * SQL Server" - * (https://msdn.microsoft.com/en-us/library/ms187993.aspx), - * and (2) SQL Server does not support comparison of TEXT - * with '=' operator (e.g. in WHERE statements such as - * IndexBackend::LookupIdentifier())." - **/ - boost::replace_all(sql, "${LONGTEXT}", "VARCHAR(MAX)"); - boost::replace_all(sql, "${AUTOINCREMENT_TYPE}", "BIGINT IDENTITY NOT NULL PRIMARY KEY"); - boost::replace_all(sql, "${AUTOINCREMENT_INSERT}", ""); - break; - - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); - } + + AdaptTypesToDialect(sql, db.GetDialect()); { DatabaseManager::Transaction t(manager, TransactionType_ReadWrite); @@ -243,6 +248,22 @@ db.ExecuteMultiLines("ALTER DATABASE CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"); } + { // v 4.X: add customData + int patchLevel; + + if (!LookupGlobalIntegerProperty(patchLevel, manager, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabasePatchLevel)) + { + std::string sqlAddCustomData = "ALTER TABLE AttachedFiles ADD customData ${LONGTEXT};" + "ALTER TABLE DeletedFiles ADD customData ${LONGTEXT}"; + + AdaptTypesToDialect(sqlAddCustomData, db.GetDialect()); + + db.ExecuteMultiLines(sqlAddCustomData); + + SetGlobalIntegerProperty(manager, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabasePatchLevel, 1); + } + } + t.Commit(); } }
--- a/Odbc/Plugins/OdbcIndex.h Mon May 19 11:18:43 2025 +0200 +++ b/Odbc/Plugins/OdbcIndex.h Fri Jun 13 15:16:52 2025 +0200 @@ -73,6 +73,21 @@ return true; } + virtual bool HasAttachmentCustomDataSupport() const ORTHANC_OVERRIDE + { + return true; + } + + virtual bool HasKeyValueStores() const ORTHANC_OVERRIDE + { + return false; + } + + virtual bool HasQueues() const ORTHANC_OVERRIDE + { + return false; + } + virtual int64_t CreateResource(DatabaseManager& manager, const char* publicId, OrthancPluginResourceType type) ORTHANC_OVERRIDE;
--- a/PostgreSQL/CMakeLists.txt Mon May 19 11:18:43 2025 +0200 +++ b/PostgreSQL/CMakeLists.txt Fri Jun 13 15:16:52 2025 +0200 @@ -19,7 +19,7 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. -cmake_minimum_required(VERSION 2.8) +cmake_minimum_required(VERSION 2.8...4.0) project(OrthancPostgreSQL) set(ORTHANC_PLUGIN_VERSION "mainline") @@ -94,6 +94,7 @@ POSTGRESQL_UPGRADE_REV1_TO_REV2 ${CMAKE_SOURCE_DIR}/Plugins/SQL/Upgrades/Rev1ToRev2.sql 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 )
--- a/PostgreSQL/NEWS Mon May 19 11:18:43 2025 +0200 +++ b/PostgreSQL/NEWS Fri Jun 13 15:16:52 2025 +0200 @@ -1,6 +1,20 @@ Pending changes in the mainline =============================== +DB schema revision: 4 + +Minimum plugin SDK (for build): 1.12.6 +Optimal plugin SDK: 1.12.6 + +Minimum Orthanc runtime: 1.12.6 +Optimal Orthanc runtime: 1.12.6 + +Minimal Postgresql Server version: 9 +Optimal Postgresql Server version: 11+ + + +* Added support for customData in AttachedFiles + Release 7.2 (2025-02-27)
--- a/PostgreSQL/Plugins/PostgreSQLIndex.cpp Mon May 19 11:18:43 2025 +0200 +++ b/PostgreSQL/Plugins/PostgreSQLIndex.cpp Fri Jun 13 15:16:52 2025 +0200 @@ -49,7 +49,7 @@ static const GlobalProperty GlobalProperty_HasComputeStatisticsReadOnly = GlobalProperty_DatabaseInternal4; } -#define CURRENT_DB_REVISION 4 +#define CURRENT_DB_REVISION 5 namespace OrthancDatabases { @@ -235,6 +235,19 @@ currentRevision = 4; } + if (currentRevision == 4) + { + LOG(WARNING) << "Upgrading DB schema from revision 4 to revision 5"; + + std::string query; + + Orthanc::EmbeddedResources::GetFileResource + (query, Orthanc::EmbeddedResources::POSTGRESQL_UPGRADE_REV4_TO_REV5); + t.GetDatabaseTransaction().ExecuteMultiLines(query); + hasAppliedAnUpgrade = true; + currentRevision = 5; + } + if (hasAppliedAnUpgrade) { LOG(WARNING) << "Upgrading DB schema by applying PrepareIndex.sql";
--- a/PostgreSQL/Plugins/PostgreSQLIndex.h Mon May 19 11:18:43 2025 +0200 +++ b/PostgreSQL/Plugins/PostgreSQLIndex.h Fri Jun 13 15:16:52 2025 +0200 @@ -72,6 +72,21 @@ return true; } + virtual bool HasAttachmentCustomDataSupport() const ORTHANC_OVERRIDE + { + return true; + } + + virtual bool HasKeyValueStores() const ORTHANC_OVERRIDE + { + return true; + } + + virtual bool HasQueues() const ORTHANC_OVERRIDE + { + return true; + } + virtual int64_t CreateResource(DatabaseManager& manager, const char* publicId, OrthancPluginResourceType type) ORTHANC_OVERRIDE;
--- a/PostgreSQL/Plugins/SQL/Downgrades/Rev3ToRev2.sql Mon May 19 11:18:43 2025 +0200 +++ b/PostgreSQL/Plugins/SQL/Downgrades/Rev3ToRev2.sql Fri Jun 13 15:16:52 2025 +0200 @@ -1,4 +1,4 @@ --- This file contains an SQL procedure to downgrade from schema Rev3 to Rev2 (version = 6, revision = 1). +-- This file contains an SQL procedure to downgrade from schema Rev3 to Rev2 (version = 6). -- It actually deletes the ChildCount table and triggers -- It actually does not uninstall ChildrenIndex2 because it is anyway more efficient than -- ChildrenIndex and is not incompatible with previous revisions.
--- a/PostgreSQL/Plugins/SQL/Downgrades/Rev4ToRev3.sql Mon May 19 11:18:43 2025 +0200 +++ b/PostgreSQL/Plugins/SQL/Downgrades/Rev4ToRev3.sql Fri Jun 13 15:16:52 2025 +0200 @@ -1,5 +1,5 @@ -- This file contains an SQL procedure to downgrade from schema Rev4 to Rev3 (version = 6). - -- It re-installs the old childcount trigger mechanisms +-- It re-installs the old childcount trigger mechanisms DO $$ DECLARE
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/PostgreSQL/Plugins/SQL/Downgrades/Rev5ToRev4.sql Fri Jun 13 15:16:52 2025 +0200 @@ -0,0 +1,62 @@ +-- This file contains an SQL procedure to downgrade from schema Rev5 to Rev4 (version = 6). +-- It removes the column that has been added in Rev5 + +-- these constraints were introduced in Rev5 +ALTER TABLE AttachedFiles DROP COLUMN customData; + +-- reinstall previous triggers +CREATE OR REPLACE FUNCTION CreateDeletedFilesTemporaryTable( +) RETURNS VOID AS $body$ + +BEGIN + + SET client_min_messages = warning; -- suppress NOTICE: relation "deletedresources" already exists, skipping + + -- note: temporary tables are created at session (connection) level -> they are likely to exist + CREATE TEMPORARY TABLE IF NOT EXISTS DeletedFiles( + uuid VARCHAR(64) NOT NULL, + fileType INTEGER, + compressedSize BIGINT, + uncompressedSize BIGINT, + compressionType INTEGER, + uncompressedHash VARCHAR(40), + compressedHash VARCHAR(40) + ); + + RESET client_min_messages; + + -- clear the temporary table in case it has been created earlier in the session + DELETE FROM DeletedFiles; +END; + +$body$ LANGUAGE plpgsql; + + +CREATE OR REPLACE FUNCTION AttachedFileDeletedFunc() +RETURNS TRIGGER AS $body$ +BEGIN + INSERT INTO DeletedFiles VALUES + (old.uuid, old.filetype, old.compressedSize, + old.uncompressedSize, old.compressionType, + old.uncompressedHash, old.compressedHash); + RETURN NULL; +END; +$body$ LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS AttachedFileDeleted on AttachedFiles; +CREATE TRIGGER AttachedFileDeleted +AFTER DELETE ON AttachedFiles +FOR EACH ROW +EXECUTE PROCEDURE AttachedFileDeletedFunc(); + + +DROP TABLE IF EXISTS KeyValueStores; + +DROP TABLE IF EXISTS Queues; + +DROP INDEX IF EXISTS QueuesIndex; + + +DELETE FROM GlobalProperties WHERE property IN (4); +INSERT INTO GlobalProperties VALUES (4, 4); -- GlobalProperty_DatabasePatchLevel +
--- a/PostgreSQL/Plugins/SQL/PrepareIndex.sql Mon May 19 11:18:43 2025 +0200 +++ b/PostgreSQL/Plugins/SQL/PrepareIndex.sql Fri Jun 13 15:16:52 2025 +0200 @@ -1,4 +1,4 @@ --- This SQL file creates a DB in Rev2 directly +-- This SQL file creates a DB in Rev5 directly -- It is also run after upgrade scripts to create new tables and or create/replace triggers and functions. -- This script is self contained, it contains everything that needs to be run to create an Orthanc DB. -- Note to developers: @@ -53,6 +53,7 @@ uncompressedHash VARCHAR(40), compressedHash VARCHAR(40), revision INTEGER, + customData BYTEA, -- new in schema rev 5 PRIMARY KEY(id, fileType) ); @@ -305,7 +306,9 @@ uncompressedSize BIGINT, compressionType INTEGER, uncompressedHash VARCHAR(40), - compressedHash VARCHAR(40) + compressedHash VARCHAR(40), + revision INTEGER, + customData BYTEA ); RESET client_min_messages; @@ -323,7 +326,8 @@ INSERT INTO DeletedFiles VALUES (old.uuid, old.filetype, old.compressedSize, old.uncompressedSize, old.compressionType, - old.uncompressedHash, old.compressedHash); + old.uncompressedHash, old.compressedHash, + old.revision, old.customData); RETURN NULL; END; $body$ LANGUAGE plpgsql; @@ -717,11 +721,28 @@ EXECUTE PROCEDURE UpdateChildCount(); +-- new in 1.12.8 (rev 5 ?) + +CREATE TABLE KeyValueStores( + storeId TEXT NOT NULL, + key 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 BYTEA NOT NULL +); + +CREATE INDEX QueuesIndex ON Queues (queueId, id); + -- 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, 4); -- GlobalProperty_DatabasePatchLevel +INSERT INTO GlobalProperties VALUES (4, 5); -- 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
--- a/PostgreSQL/Plugins/SQL/Upgrades/Rev1ToRev2.sql Mon May 19 11:18:43 2025 +0200 +++ b/PostgreSQL/Plugins/SQL/Upgrades/Rev1ToRev2.sql Fri Jun 13 15:16:52 2025 +0200 @@ -1,4 +1,4 @@ --- This file contains part of the changes required to upgrade from Revision 1 to Revision 2 (DB version 6 and revision 1) +-- This file contains part of the changes required to upgrade from Revision 1 to Revision 2 (DB version 6) -- It actually contains only the changes that: -- can not be executed with an idempotent statement in SQL -- or would polute the PrepareIndex.sql
--- a/PostgreSQL/Plugins/SQL/Upgrades/Rev2ToRev3.sql Mon May 19 11:18:43 2025 +0200 +++ b/PostgreSQL/Plugins/SQL/Upgrades/Rev2ToRev3.sql Fri Jun 13 15:16:52 2025 +0200 @@ -41,4 +41,3 @@ -- other changes performed in PrepareIndex.sql: -- add ChildCount triggers -
--- a/PostgreSQL/Plugins/SQL/Upgrades/Rev3ToRev4.sql Mon May 19 11:18:43 2025 +0200 +++ b/PostgreSQL/Plugins/SQL/Upgrades/Rev3ToRev4.sql Fri Jun 13 15:16:52 2025 +0200 @@ -1,2 +1,2 @@ -- everything is performed in PrepareIndex.sql -SELECT 1; \ No newline at end of file +SELECT 1;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/PostgreSQL/Plugins/SQL/Upgrades/Rev4ToRev5.sql Fri Jun 13 15:16:52 2025 +0200 @@ -0,0 +1,19 @@ +-- This file contains part of the changes required to upgrade from Revision 4 to Revision 5 (DB version 6) +-- It actually contains only the changes that: + -- can not be executed with an idempotent statement in SQL + -- or would polute the PrepareIndex.sql +-- This file is executed only if the current schema is in revision 4 and it is executed +-- before PrepareIndex.sql that is idempotent. + + + +DO $body$ +BEGIN + + BEGIN + ALTER TABLE AttachedFiles ADD COLUMN customData BYTEA; + EXCEPTION + WHEN duplicate_column THEN RAISE NOTICE 'column customData already exists in AttachedFiles.'; + END; + +END $body$ LANGUAGE plpgsql;
--- a/README Mon May 19 11:18:43 2025 +0200 +++ b/README Fri Jun 13 15:16:52 2025 +0200 @@ -57,6 +57,69 @@ https://orthanc.uclouvain.be/book/developers/repositories.html +Development +----------- + +PostgreSQL +========== + +To quickly start a test PG server: + + docker run -p 5432:5432 --rm --env POSTGRES_HOST_AUTH_METHOD=trust postgres:13.4 + +And use this Orthanc configuration: + "PostgreSQL": { + "EnableIndex": true, + "EnableStorage": false, // DICOM files are stored in the Orthanc container in /var/lib/orthanc/db/ + "Host": "localhost", // the name of the PostgreSQL container + "Database": "postgres", // default database name in PostgreSQL container (no need to create it) + "Username": "postgres", // default user name in PostgreSQL container (no need to create it) + "Password": "postgres" + }, + +MySQL +===== + +To quickly start a test MySQL server: + + docker run -p 3306:3306 --rm --env MYSQL_PASSWORD=orthanc --env MYSQL_USER=orthanc --env MYSQL_DATABASE=orthanc --env MYSQL_ROOT_PASSWORD=pwd-root mysql:8.0 mysqld --default-authentication-plugin=mysql_native_password --log-bin-trust-function-creators=1 + +And use this Orthanc configuration: + "MySQL": { + "EnableIndex": true, + "EnableStorage": false, + "Host": "localhost", + "Database": "orthanc", + "Username": "orthanc", + "Password": "orthanc", + "UnixSocket": "" + }, + + +ODBC (SQL Server) +================= + +To quickly start a test MySQL server: + + docker run -e "ACCEPT_EULA=Y" --rm --env "SA_PASSWORD=yourStrong-Password" --entrypoint=bash -it -p 1433:1433 mcr.microsoft.com/mssql/server:2019-latest + +Then: + (sleep 15s && /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P yourStrong-Password -Q 'CREATE DATABASE orthanctest') & /opt/mssql/bin/sqlservr + +And use this Orthanc configuration: + "Odbc" : { + "IndexConnectionString": "Driver={ODBC Driver 17 for SQL Server};Server=tcp:localhost,1433;Database=orthanctest;Uid=sa;Pwd=yourStrong-Password;Encrypt=yes;TrustServerCertificate=yes;Connection Timeout=30;", + "EnableIndex": true, + "EnableStorage": false + } + + +SQLite +====== + +To quickly test the SQLite plugin, simply run orthanc and load the plugin (no configuration required). + + Licensing ---------
--- a/Resources/Orthanc/CMake/Compiler.cmake Mon May 19 11:18:43 2025 +0200 +++ b/Resources/Orthanc/CMake/Compiler.cmake Fri Jun 13 15:16:52 2025 +0200 @@ -22,6 +22,16 @@ # This file sets all the compiler-related flags +if (${CMAKE_SYSTEM_NAME} STREQUAL "Darwin") + # Since Orthanc 1.12.7 that allows CMake 4.0, builds for macOS + # require the C++ standard to be explicitly set to C++11. Do *not* + # do this on GNU/Linux, as third-party system libraries could have + # been compiled with higher versions of the C++ standard. + set(CMAKE_CXX_STANDARD 11) + set(CMAKE_CXX_STANDARD_REQUIRED ON) + set(CMAKE_CXX_EXTENSIONS OFF) +endif() + # Save the current compiler flags to the cache every time cmake configures the project set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS}" CACHE STRING "compiler flags" FORCE) @@ -239,7 +249,9 @@ add_definitions( -D_XOPEN_SOURCE=1 ) - link_libraries(iconv) + + # Linking with iconv breaks the Universal builds on modern compilers + # link_libraries(iconv) elseif (CMAKE_SYSTEM_NAME STREQUAL "Emscripten") message("Building using Emscripten (for WebAssembly or asm.js targets)")
--- a/Resources/Orthanc/CMake/DownloadOrthancFramework.cmake Mon May 19 11:18:43 2025 +0200 +++ b/Resources/Orthanc/CMake/DownloadOrthancFramework.cmake Fri Jun 13 15:16:52 2025 +0200 @@ -167,6 +167,10 @@ set(ORTHANC_FRAMEWORK_MD5 "1e61779ea4a7cd705720bdcfed8a6a73") elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.5") set(ORTHANC_FRAMEWORK_MD5 "5bb69f092981fdcfc11dec0a0f9a7db3") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.6") + set(ORTHANC_FRAMEWORK_MD5 "0e971f32f4f3e4951e0f3b5de49a3da6") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.7") + set(ORTHANC_FRAMEWORK_MD5 "f27c27d7a7a694dab1fd7f0a99d9715a") # Below this point are development snapshots that were used to # release some plugin, before an official release of the Orthanc @@ -499,7 +503,15 @@ include(CheckIncludeFile) include(CheckIncludeFileCXX) - include(FindPythonInterp) + + if(CMAKE_VERSION VERSION_GREATER "3.11") + find_package(Python REQUIRED COMPONENTS Interpreter) + set(PYTHON_EXECUTABLE ${Python_EXECUTABLE}) + else() + include(FindPythonInterp) + find_package(PythonInterp REQUIRED) + endif() + include(${CMAKE_CURRENT_LIST_DIR}/Compiler.cmake) include(${CMAKE_CURRENT_LIST_DIR}/DownloadPackage.cmake) include(${CMAKE_CURRENT_LIST_DIR}/AutoGeneratedCode.cmake)
--- a/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp Mon May 19 11:18:43 2025 +0200 +++ b/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp Fri Jun 13 15:16:52 2025 +0200 @@ -26,6 +26,7 @@ #include <boost/algorithm/string/predicate.hpp> #include <boost/move/unique_ptr.hpp> #include <boost/thread.hpp> +#include <boost/algorithm/string/join.hpp> #include <json/reader.h> @@ -2051,6 +2052,26 @@ DoPost(target, index, uri, body, headers)); } + bool OrthancPeers::DoPost(Json::Value& target, + size_t index, + const std::string& uri, + const std::string& body, + const HttpHeaders& headers, + unsigned int timeout) const + { + MemoryBuffer buffer; + + if (DoPost(buffer, index, uri, body, headers, timeout)) + { + buffer.ToJson(target); + return true; + } + else + { + return false; + } + } + bool OrthancPeers::DoPost(Json::Value& target, size_t index, @@ -2098,6 +2119,17 @@ const std::string& body, const HttpHeaders& headers) const { + return DoPost(target, index, uri, body, headers, timeout_); + } + + + bool OrthancPeers::DoPost(MemoryBuffer& target, + size_t index, + const std::string& uri, + const std::string& body, + const HttpHeaders& headers, + unsigned int timeout) const + { if (index >= index_.size()) { ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange); @@ -2116,7 +2148,7 @@ OrthancPluginErrorCode code = OrthancPluginCallPeerApi (GetGlobalContext(), *answer, NULL, &status, peers_, static_cast<uint32_t>(index), OrthancPluginHttpMethod_Post, uri.c_str(), - pluginHeaders.GetSize(), pluginHeaders.GetKeys(), pluginHeaders.GetValues(), body.empty() ? NULL : body.c_str(), body.size(), timeout_); + pluginHeaders.GetSize(), pluginHeaders.GetKeys(), pluginHeaders.GetValues(), body.empty() ? NULL : body.c_str(), body.size(), timeout); if (code == OrthancPluginErrorCode_Success) { @@ -4077,6 +4109,26 @@ } } + void SerializeGetArguments(std::string& output, const OrthancPluginHttpRequest* request) + { + output.clear(); + std::vector<std::string> arguments; + for (uint32_t i = 0; i < request->getCount; ++i) + { + if (request->getValues[i] && strlen(request->getValues[i]) > 0) + { + arguments.push_back(std::string(request->getKeys[i]) + "=" + std::string(request->getValues[i])); + } + else + { + arguments.push_back(std::string(request->getKeys[i])); + } + } + + output = boost::algorithm::join(arguments, "&"); + } + + #if !ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 4) static void SetPluginProperty(const std::string& pluginIdentifier, _OrthancPluginProperty property, @@ -4130,6 +4182,24 @@ httpStatus_(0) { } + + RestApiClient::RestApiClient(const char* url, + const OrthancPluginHttpRequest* request) : + method_(request->method), + path_(url), + afterPlugins_(false), + httpStatus_(0) + { + OrthancPlugins::GetHttpHeaders(requestHeaders_, request); + + std::string getArguments; + OrthancPlugins::SerializeGetArguments(getArguments, request); + + if (!getArguments.empty()) + { + path_ += "?" + getArguments; + } + } #endif @@ -4195,6 +4265,32 @@ } } } + + void RestApiClient::Forward(OrthancPluginContext* context, OrthancPluginRestOutput* output) + { + if (Execute() && httpStatus_ == 200) + { + const char* mimeType = NULL; + for (HttpHeaders::const_iterator h = answerHeaders_.begin(); h != answerHeaders_.end(); ++h) + { + if (h->first == "content-type") + { + mimeType = h->second.c_str(); + } + } + + AnswerString(answerBody_, mimeType, output); + } + else + { + AnswerHttpError(httpStatus_, output); + } + } + + bool RestApiClient::GetAnswerJson(Json::Value& output) const + { + return ReadJson(output, answerBody_); + } #endif
--- a/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h Mon May 19 11:18:43 2025 +0200 +++ b/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h Fri Jun 13 15:16:52 2025 +0200 @@ -855,6 +855,13 @@ const HttpHeaders& headers) const; bool DoPost(MemoryBuffer& target, + size_t index, + const std::string& uri, + const std::string& body, + const HttpHeaders& headers, + unsigned int timeout) const; + + bool DoPost(MemoryBuffer& target, const std::string& name, const std::string& uri, const std::string& body, @@ -867,6 +874,13 @@ const HttpHeaders& headers) const; bool DoPost(Json::Value& target, + size_t index, + const std::string& uri, + const std::string& body, + const HttpHeaders& headers, + unsigned int timeout) const; + + bool DoPost(Json::Value& target, const std::string& name, const std::string& uri, const std::string& body, @@ -1399,6 +1413,9 @@ // helper method to convert Http headers from the plugin SDK to a std::map void GetHttpHeaders(HttpHeaders& result, const OrthancPluginHttpRequest* request); +// helper method to re-serialize the get arguments from the SDK into a string +void SerializeGetArguments(std::string& output, const OrthancPluginHttpRequest* request); + #if HAS_ORTHANC_PLUGIN_WEBDAV == 1 class IWebDavCollection : public boost::noncopyable { @@ -1528,6 +1545,10 @@ public: RestApiClient(); + + // used to forward a call from the plugin to the core + RestApiClient(const char* url, + const OrthancPluginHttpRequest* request); void SetMethod(OrthancPluginHttpMethod method) { @@ -1584,12 +1605,17 @@ bool Execute(); + // Execute and forward the response as is + void Forward(OrthancPluginContext* context, OrthancPluginRestOutput* output); + uint16_t GetHttpStatus() const; bool LookupAnswerHeader(std::string& value, const std::string& key) const; const std::string& GetAnswerBody() const; + + bool GetAnswerJson(Json::Value& output) const; }; #endif }
--- a/SQLite/CMakeLists.txt Mon May 19 11:18:43 2025 +0200 +++ b/SQLite/CMakeLists.txt Fri Jun 13 15:16:52 2025 +0200 @@ -19,7 +19,7 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. -cmake_minimum_required(VERSION 2.8) +cmake_minimum_required(VERSION 2.8...4.0) project(OrthancSQLite) set(ORTHANC_PLUGIN_VERSION "mainline") @@ -55,6 +55,7 @@ EmbedResources( SQLITE_PREPARE_INDEX ${CMAKE_SOURCE_DIR}/Plugins/PrepareIndex.sql + SQLITE_INSTALL_CUSTOM_DATA ${CMAKE_SOURCE_DIR}/Plugins/InstallCustomData.sql ) if (EXISTS ${ORTHANC_SDK_ROOT}/orthanc/OrthancDatabasePlugin.proto)
--- a/SQLite/NEWS Mon May 19 11:18:43 2025 +0200 +++ b/SQLite/NEWS Fri Jun 13 15:16:52 2025 +0200 @@ -1,3 +1,9 @@ +Pending changes in the mainline +=============================== + +* Added support for customData in AttachedFiles + + Pending changes in the mainline ===============================
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/SQLite/Plugins/InstallCustomData.sql Fri Jun 13 15:16:52 2025 +0200 @@ -0,0 +1,17 @@ + +-- Add new column for customData +ALTER TABLE AttachedFiles ADD COLUMN customData TEXT; +ALTER TABLE DeletedFiles ADD COLUMN revision INTEGER; +ALTER TABLE DeletedFiles ADD COLUMN customData TEXT; + + +DROP TRIGGER AttachedFileDeleted; + +CREATE TRIGGER AttachedFileDeleted +AFTER DELETE ON AttachedFiles +BEGIN + INSERT INTO DeletedFiles VALUES(old.uuid, old.filetype, old.compressedSize, + old.uncompressedSize, old.compressionType, + old.uncompressedHash, old.compressedHash, + old.revision, old.customData); +END;
--- a/SQLite/Plugins/PrepareIndex.sql Mon May 19 11:18:43 2025 +0200 +++ b/SQLite/Plugins/PrepareIndex.sql Fri Jun 13 15:16:52 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 NOT NULL +); + +CREATE INDEX QueuesIndex ON Queues (queueId, id);
--- a/SQLite/Plugins/SQLiteIndex.cpp Mon May 19 11:18:43 2025 +0200 +++ b/SQLite/Plugins/SQLiteIndex.cpp Fri Jun 13 15:16:52 2025 +0200 @@ -149,7 +149,22 @@ SetGlobalIntegerProperty(manager, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabasePatchLevel, revision); } - if (revision != 1) + // install customData + if (!LookupGlobalIntegerProperty(revision, manager, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabasePatchLevel) + || revision == 1) + { + std::string query; + + Orthanc::EmbeddedResources::GetFileResource + (query, Orthanc::EmbeddedResources::SQLITE_INSTALL_CUSTOM_DATA); + + t.GetDatabaseTransaction().ExecuteMultiLines(query); + + revision = 2; + SetGlobalIntegerProperty(manager, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabasePatchLevel, revision); + } + + if (revision != 2) { LOG(ERROR) << "SQLite plugin is incompatible with database schema revision: " << revision; throw Orthanc::OrthancException(Orthanc::ErrorCode_Database);
--- a/SQLite/Plugins/SQLiteIndex.h Mon May 19 11:18:43 2025 +0200 +++ b/SQLite/Plugins/SQLiteIndex.h Fri Jun 13 15:16:52 2025 +0200 @@ -55,6 +55,21 @@ return true; } + virtual bool HasAttachmentCustomDataSupport() const ORTHANC_OVERRIDE + { + return true; + } + + virtual bool HasKeyValueStores() const ORTHANC_OVERRIDE + { + return true; + } + + virtual bool HasQueues() const ORTHANC_OVERRIDE + { + return true; + } + virtual int64_t CreateResource(DatabaseManager& manager, const char* publicId, OrthancPluginResourceType type) ORTHANC_OVERRIDE;