# HG changeset patch # User Sebastien Jodogne # Date 1547123613 -3600 # Node ID 122f22550521051ee0b8b527697a85e9164bf050 # Parent 16df1a6ea452f324454d3f7b52a6bea3823db4d0# Parent 515a783630dfb4654014994a3d35b606e117b85a integration mainline->db-changes diff -r 515a783630df -r 122f22550521 Framework/Common/DatabaseManager.cpp --- a/Framework/Common/DatabaseManager.cpp Thu Jan 10 13:32:34 2019 +0100 +++ b/Framework/Common/DatabaseManager.cpp Thu Jan 10 13:33:33 2019 +0100 @@ -279,34 +279,6 @@ } - IResult& DatabaseManager::CachedStatement::GetResult() const - { - if (result_.get() == NULL) - { - LOG(ERROR) << "Accessing the results of a statement without having executed it"; - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - - return *result_; - } - - - void DatabaseManager::CachedStatement::Setup(const char* sql) - { - statement_ = manager_.LookupCachedStatement(location_); - - if (statement_ == NULL) - { - query_.reset(new Query(sql)); - } - else - { - LOG(TRACE) << "Reusing cached statement from " - << location_.GetFile() << ":" << location_.GetLine(); - } - } - - DatabaseManager::Transaction::Transaction(DatabaseManager& manager) : lock_(manager.mutex_), manager_(manager), @@ -347,40 +319,72 @@ } } - - DatabaseManager::CachedStatement::CachedStatement(const StatementLocation& location, - DatabaseManager& manager, - const char* sql) : - manager_(manager), - lock_(manager_.mutex_), - database_(manager_.GetDatabase()), - location_(location), - transaction_(manager_.GetTransaction()) + + IResult& DatabaseManager::StatementBase::GetResult() const { - Setup(sql); - } + if (result_.get() == NULL) + { + LOG(ERROR) << "Accessing the results of a statement without having executed it"; + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } - - DatabaseManager::CachedStatement::CachedStatement(const StatementLocation& location, - Transaction& transaction, - const char* sql) : - manager_(transaction.GetManager()), - lock_(manager_.mutex_), - database_(manager_.GetDatabase()), - location_(location), - transaction_(manager_.GetTransaction()) - { - Setup(sql); + return *result_; } - DatabaseManager::CachedStatement::~CachedStatement() + void DatabaseManager::StatementBase::SetQuery(Query* query) + { + std::auto_ptr protection(query); + + if (query_.get() != NULL) + { + LOG(ERROR) << "Cannot set twice a query"; + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + + if (query == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + + query_.reset(protection.release()); + } + + + void DatabaseManager::StatementBase::SetResult(IResult* result) + { + std::auto_ptr protection(result); + + if (result_.get() != NULL) + { + LOG(ERROR) << "Cannot execute twice a statement"; + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + + if (result == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + + result_.reset(protection.release()); + } + + + DatabaseManager::StatementBase::StatementBase(DatabaseManager& manager) : + manager_(manager), + lock_(manager_.mutex_), + transaction_(manager_.GetTransaction()) + { + } + + + DatabaseManager::StatementBase::~StatementBase() { manager_.ReleaseImplicitTransaction(); } + - - void DatabaseManager::CachedStatement::SetReadOnly(bool readOnly) + void DatabaseManager::StatementBase::SetReadOnly(bool readOnly) { if (query_.get() != NULL) { @@ -389,8 +393,8 @@ } - void DatabaseManager::CachedStatement::SetParameterType(const std::string& parameter, - ValueType type) + void DatabaseManager::StatementBase::SetParameterType(const std::string& parameter, + ValueType type) { if (query_.get() != NULL) { @@ -398,44 +402,7 @@ } } - - void DatabaseManager::CachedStatement::Execute() - { - Dictionary parameters; - Execute(parameters); - } - - - void DatabaseManager::CachedStatement::Execute(const Dictionary& parameters) - { - if (result_.get() != NULL) - { - LOG(ERROR) << "Cannot execute twice a statement"; - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - - try - { - if (query_.get() != NULL) - { - // Register the newly-created statement - assert(statement_ == NULL); - statement_ = &manager_.CacheStatement(location_, *query_); - query_.reset(NULL); - } - - assert(statement_ != NULL); - result_.reset(transaction_.Execute(*statement_, parameters)); - } - catch (Orthanc::OrthancException& e) - { - manager_.CloseIfUnavailable(e.GetErrorCode()); - throw; - } - } - - - bool DatabaseManager::CachedStatement::IsDone() const + bool DatabaseManager::StatementBase::IsDone() const { try { @@ -449,7 +416,7 @@ } - void DatabaseManager::CachedStatement::Next() + void DatabaseManager::StatementBase::Next() { try { @@ -463,7 +430,7 @@ } - size_t DatabaseManager::CachedStatement::GetResultFieldsCount() const + size_t DatabaseManager::StatementBase::GetResultFieldsCount() const { try { @@ -477,8 +444,8 @@ } - void DatabaseManager::CachedStatement::SetResultFieldType(size_t field, - ValueType type) + void DatabaseManager::StatementBase::SetResultFieldType(size_t field, + ValueType type) { try { @@ -495,7 +462,7 @@ } - const IValue& DatabaseManager::CachedStatement::GetResultField(size_t index) const + const IValue& DatabaseManager::StatementBase::GetResultField(size_t index) const { try { @@ -506,5 +473,88 @@ manager_.CloseIfUnavailable(e.GetErrorCode()); throw; } + } + + + DatabaseManager::CachedStatement::CachedStatement(const StatementLocation& location, + DatabaseManager& manager, + const std::string& sql) : + StatementBase(manager), + location_(location) + { + statement_ = GetManager().LookupCachedStatement(location_); + + if (statement_ == NULL) + { + SetQuery(new Query(sql)); + } + else + { + LOG(TRACE) << "Reusing cached statement from " + << location_.GetFile() << ":" << location_.GetLine(); + } + } + + + void DatabaseManager::CachedStatement::Execute(const Dictionary& parameters) + { + try + { + std::auto_ptr query(ReleaseQuery()); + + if (query.get() != NULL) + { + // Register the newly-created statement + assert(statement_ == NULL); + statement_ = &GetManager().CacheStatement(location_, *query); + } + + assert(statement_ != NULL); + SetResult(GetTransaction().Execute(*statement_, parameters)); + } + catch (Orthanc::OrthancException& e) + { + GetManager().CloseIfUnavailable(e.GetErrorCode()); + throw; + } + } + + + DatabaseManager::StandaloneStatement::StandaloneStatement(DatabaseManager& manager, + const std::string& sql) : + StatementBase(manager) + { + SetQuery(new Query(sql)); + } + + + DatabaseManager::StandaloneStatement::~StandaloneStatement() + { + // The result must be removed before the statement, cf. (*) + ClearResult(); + statement_.reset(); + } + + + void DatabaseManager::StandaloneStatement::Execute(const Dictionary& parameters) + { + try + { + std::auto_ptr query(ReleaseQuery()); + assert(query.get() != NULL); + + // The "statement_" object must be kept as long as the "IResult" + // is not destroyed, as the "IResult" can make calls to the + // statement (this is the case for SQLite and MySQL) - (*) + statement_.reset(GetManager().GetDatabase().Compile(*query)); + assert(statement_.get() != NULL); + + SetResult(GetTransaction().Execute(*statement_, parameters)); + } + catch (Orthanc::OrthancException& e) + { + GetManager().CloseIfUnavailable(e.GetErrorCode()); + throw; + } } } diff -r 515a783630df -r 122f22550521 Framework/Common/DatabaseManager.h --- a/Framework/Common/DatabaseManager.h Thu Jan 10 13:32:34 2019 +0100 +++ b/Framework/Common/DatabaseManager.h Thu Jan 10 13:33:33 2019 +0100 @@ -111,36 +111,51 @@ }; - class CachedStatement : public boost::noncopyable + class StatementBase : public boost::noncopyable { private: DatabaseManager& manager_; boost::recursive_mutex::scoped_lock lock_; - IDatabase& database_; - StatementLocation location_; ITransaction& transaction_; - IPrecompiledStatement* statement_; std::auto_ptr query_; std::auto_ptr result_; - void Setup(const char* sql); - IResult& GetResult() const; - public: - CachedStatement(const StatementLocation& location, - DatabaseManager& manager, - const char* sql); + protected: + DatabaseManager& GetManager() const + { + return manager_; + } + + ITransaction& GetTransaction() const + { + return transaction_; + } + + void SetQuery(Query* query); + + void SetResult(IResult* result); - CachedStatement(const StatementLocation& location, - Transaction& transaction, - const char* sql); + void ClearResult() + { + result_.reset(); + } - ~CachedStatement(); + Query* ReleaseQuery() + { + return query_.release(); + } + public: + StatementBase(DatabaseManager& manager); + + virtual ~StatementBase(); + + // Used only by SQLite IDatabase& GetDatabase() { - return database_; + return manager_.GetDatabase(); } void SetReadOnly(bool readOnly); @@ -148,10 +163,6 @@ void SetParameterType(const std::string& parameter, ValueType type); - void Execute(); - - void Execute(const Dictionary& parameters); - bool IsDone() const; void Next(); @@ -163,5 +174,47 @@ const IValue& GetResultField(size_t index) const; }; + + + class CachedStatement : public StatementBase + { + private: + StatementLocation location_; + IPrecompiledStatement* statement_; + + public: + CachedStatement(const StatementLocation& location, + DatabaseManager& manager, + const std::string& sql); + + void Execute() + { + Dictionary parameters; + Execute(parameters); + } + + void Execute(const Dictionary& parameters); + }; + + + class StandaloneStatement : public StatementBase + { + private: + std::auto_ptr statement_; + + public: + StandaloneStatement(DatabaseManager& manager, + const std::string& sql); + + virtual ~StandaloneStatement(); + + void Execute() + { + Dictionary parameters; + Execute(parameters); + } + + void Execute(const Dictionary& parameters); + }; }; } diff -r 515a783630df -r 122f22550521 Framework/Common/Query.cpp --- a/Framework/Common/Query.cpp Thu Jan 10 13:32:34 2019 +0100 +++ b/Framework/Common/Query.cpp Thu Jan 10 13:33:33 2019 +0100 @@ -125,8 +125,8 @@ if (found == parameters_.end()) { - LOG(ERROR) << "Inexistent parameter in a SQL query: " << parameter; - throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentItem); + throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentItem, + "Inexistent parameter in a SQL query: " + parameter); } else { @@ -142,8 +142,8 @@ if (found == parameters_.end()) { - LOG(ERROR) << "Ignoring inexistent parameter in a SQL query: " << parameter; - throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentItem, + "Inexistent parameter in a SQL query: " + parameter); } else { diff -r 515a783630df -r 122f22550521 Framework/Plugins/IndexBackend.cpp --- a/Framework/Plugins/IndexBackend.cpp Thu Jan 10 13:32:34 2019 +0100 +++ b/Framework/Plugins/IndexBackend.cpp Thu Jan 10 13:33:33 2019 +0100 @@ -29,6 +29,7 @@ #include #include #include +#include namespace OrthancDatabases @@ -55,7 +56,7 @@ } - int64_t IndexBackend::ReadInteger64(const DatabaseManager::CachedStatement& statement, + int64_t IndexBackend::ReadInteger64(const DatabaseManager::StatementBase& statement, size_t field) { if (statement.IsDone()) @@ -77,7 +78,7 @@ } - int32_t IndexBackend::ReadInteger32(const DatabaseManager::CachedStatement& statement, + int32_t IndexBackend::ReadInteger32(const DatabaseManager::StatementBase& statement, size_t field) { if (statement.IsDone()) @@ -99,11 +100,11 @@ } - std::string IndexBackend::ReadString(const DatabaseManager::CachedStatement& statement, + std::string IndexBackend::ReadString(const DatabaseManager::StatementBase& statement, size_t field) { const IValue& value = statement.GetResultField(field); - + switch (value.GetType()) { case ValueType_BinaryString: @@ -704,14 +705,14 @@ case Dialect_PostgreSQL: statement.reset(new DatabaseManager::CachedStatement( - STATEMENT_FROM_HERE, GetManager(), - "SELECT CAST(COUNT(*) AS BIGINT) FROM Resources WHERE resourceType=${type}")); + STATEMENT_FROM_HERE, GetManager(), + "SELECT CAST(COUNT(*) AS BIGINT) FROM Resources WHERE resourceType=${type}")); break; case Dialect_SQLite: statement.reset(new DatabaseManager::CachedStatement( - STATEMENT_FROM_HERE, GetManager(), - "SELECT COUNT(*) FROM Resources WHERE resourceType=${type}")); + STATEMENT_FROM_HERE, GetManager(), + "SELECT COUNT(*) FROM Resources WHERE resourceType=${type}")); break; default: @@ -1579,4 +1580,308 @@ ReadListOfStrings(childrenPublicIds, statement, args); } + + +#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 + class IndexBackend::LookupFormatter : public Orthanc::ISqlLookupFormatter + { + private: + Dialect dialect_; + size_t count_; + Dictionary dictionary_; + + static std::string FormatParameter(size_t index) + { + return "p" + boost::lexical_cast(index); + } + + public: + LookupFormatter(Dialect dialect) : + dialect_(dialect), + count_(0) + { + } + + virtual std::string GenerateParameter(const std::string& value) + { + const std::string key = FormatParameter(count_); + + count_ ++; + dictionary_.SetUtf8Value(key, value); + + return "${" + key + "}"; + } + + virtual std::string FormatResourceType(Orthanc::ResourceType level) + { + return boost::lexical_cast(Orthanc::Plugins::Convert(level)); + } + + virtual std::string FormatWildcardEscape() + { + switch (dialect_) + { + case Dialect_SQLite: + case Dialect_PostgreSQL: + return "ESCAPE '\\'"; + + case Dialect_MySQL: + return "ESCAPE '\\\\'"; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + } + + void PrepareStatement(DatabaseManager::StandaloneStatement& statement) const + { + statement.SetReadOnly(true); + + for (size_t i = 0; i < count_; i++) + { + statement.SetParameterType(FormatParameter(i), ValueType_Utf8String); + } + } + + const Dictionary& GetDictionary() const + { + return dictionary_; + } + }; +#endif + + +#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 + // New primitive since Orthanc 1.5.2 + void IndexBackend::LookupResources(const std::vector& lookup, + OrthancPluginResourceType queryLevel, + uint32_t limit, + bool requestSomeInstance) + { + LookupFormatter formatter(manager_.GetDialect()); + + std::string sql; + Orthanc::ISqlLookupFormatter::Apply(sql, formatter, lookup, + Orthanc::Plugins::Convert(queryLevel), limit); + + if (requestSomeInstance) + { + // Composite query to find some instance if requested + switch (queryLevel) + { + case OrthancPluginResourceType_Patient: + sql = ("SELECT patients.publicId, MIN(instances.publicId) FROM (" + sql + ") patients " + "INNER JOIN Resources studies ON studies.parentId = patients.internalId " + "INNER JOIN Resources series ON series.parentId = studies.internalId " + "INNER JOIN Resources instances ON instances.parentId = series.internalId " + "GROUP BY patients.publicId"); + break; + + case OrthancPluginResourceType_Study: + sql = ("SELECT studies.publicId, MIN(instances.publicId) FROM (" + sql + ") studies " + "INNER JOIN Resources series ON series.parentId = studies.internalId " + "INNER JOIN Resources instances ON instances.parentId = series.internalId " + "GROUP BY studies.publicId"); + break; + + case OrthancPluginResourceType_Series: + sql = ("SELECT series.publicId, MIN(instances.publicId) FROM (" + sql + ") series " + "INNER JOIN Resources instances ON instances.parentId = series.internalId " + "GROUP BY series.publicId"); + break; + + case OrthancPluginResourceType_Instance: + sql = ("SELECT instances.publicId, instances.publicId FROM (" + sql + ") instances"); + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + } + + DatabaseManager::StandaloneStatement statement(GetManager(), sql); + formatter.PrepareStatement(statement); + + statement.Execute(formatter.GetDictionary()); + + while (!statement.IsDone()) + { + if (requestSomeInstance) + { + GetOutput().AnswerMatchingResource(ReadString(statement, 0), ReadString(statement, 1)); + } + else + { + GetOutput().AnswerMatchingResource(ReadString(statement, 0)); + } + + statement.Next(); + } + } +#endif + + +#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 + static void ExecuteSetResourcesContentTags( + DatabaseManager& manager, + const std::string& table, + const std::string& variablePrefix, + uint32_t count, + const OrthancPluginResourcesContentTags* tags) + { + std::string sql; + Dictionary args; + + for (uint32_t i = 0; i < count; i++) + { + std::string name = variablePrefix + boost::lexical_cast(i); + + args.SetUtf8Value(name, tags[i].value); + + std::string insert = ("(" + boost::lexical_cast(tags[i].resource) + ", " + + boost::lexical_cast(tags[i].group) + ", " + + boost::lexical_cast(tags[i].element) + ", " + + "${" + name + "})"); + + if (sql.empty()) + { + sql = "INSERT INTO " + table + " VALUES " + insert; + } + else + { + sql += ", " + insert; + } + } + + if (!sql.empty()) + { + DatabaseManager::StandaloneStatement statement(manager, sql); + + for (uint32_t i = 0; i < count; i++) + { + statement.SetParameterType(variablePrefix + boost::lexical_cast(i), + ValueType_Utf8String); + } + + statement.Execute(args); + } + } +#endif + + +#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 + static void ExecuteSetResourcesContentMetadata( + DatabaseManager& manager, + uint32_t count, + const OrthancPluginResourcesContentMetadata* metadata) + { + std::string sqlRemove; // To overwrite + std::string sqlInsert; + Dictionary args; + + for (uint32_t i = 0; i < count; i++) + { + std::string name = "m" + boost::lexical_cast(i); + + args.SetUtf8Value(name, metadata[i].value); + + std::string insert = ("(" + boost::lexical_cast(metadata[i].resource) + ", " + + boost::lexical_cast(metadata[i].metadata) + ", " + + "${" + name + "})"); + + std::string remove = ("(id=" + boost::lexical_cast(metadata[i].resource) + + " AND type=" + boost::lexical_cast(metadata[i].metadata) + + ")"); + + if (sqlInsert.empty()) + { + sqlInsert = "INSERT INTO Metadata VALUES " + insert; + } + else + { + sqlInsert += ", " + insert; + } + + if (sqlRemove.empty()) + { + sqlRemove = "DELETE FROM Metadata WHERE " + remove; + } + else + { + sqlRemove += " OR " + remove; + } + } + + if (!sqlRemove.empty()) + { + DatabaseManager::StandaloneStatement statement(manager, sqlRemove); + statement.Execute(); + } + + if (!sqlInsert.empty()) + { + DatabaseManager::StandaloneStatement statement(manager, sqlInsert); + + for (uint32_t i = 0; i < count; i++) + { + statement.SetParameterType("m" + boost::lexical_cast(i), + ValueType_Utf8String); + } + + statement.Execute(args); + } + } +#endif + + +#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 + // New primitive since Orthanc 1.5.2 + void IndexBackend::SetResourcesContent( + uint32_t countIdentifierTags, + const OrthancPluginResourcesContentTags* identifierTags, + uint32_t countMainDicomTags, + const OrthancPluginResourcesContentTags* mainDicomTags, + uint32_t countMetadata, + const OrthancPluginResourcesContentMetadata* metadata) + { + /** + * TODO - PostgreSQL doesn't allow multiple commands in a prepared + * statement, so we execute 3 separate commands (for identifiers, + * main tags and metadata). Maybe MySQL does not suffer from the + * same limitation, to check. + **/ + + ExecuteSetResourcesContentTags(GetManager(), "DicomIdentifiers", "i", + countIdentifierTags, identifierTags); + + ExecuteSetResourcesContentTags(GetManager(), "MainDicomTags", "t", + countMainDicomTags, mainDicomTags); + + ExecuteSetResourcesContentMetadata(GetManager(), countMetadata, metadata); + } +#endif + + +#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 + // New primitive since Orthanc 1.5.2 + void IndexBackend::GetChildrenMetadata(std::list& target, + int64_t resourceId, + int32_t metadata) + { + DatabaseManager::CachedStatement statement( + STATEMENT_FROM_HERE, manager_, + "SELECT value FROM Metadata WHERE type=${metadata} AND " + "id IN (SELECT internalId FROM Resources WHERE parentId=${id})"); + + statement.SetReadOnly(true); + statement.SetParameterType("id", ValueType_Integer64); + statement.SetParameterType("metadata", ValueType_Integer64); + + Dictionary args; + args.SetIntegerValue("id", static_cast(resourceId)); + args.SetIntegerValue("metadata", static_cast(metadata)); + + ReadListOfStrings(target, statement, args); + } +#endif } diff -r 515a783630df -r 122f22550521 Framework/Plugins/IndexBackend.h --- a/Framework/Plugins/IndexBackend.h Thu Jan 10 13:32:34 2019 +0100 +++ b/Framework/Plugins/IndexBackend.h Thu Jan 10 13:33:33 2019 +0100 @@ -30,6 +30,8 @@ class IndexBackend : public OrthancPlugins::IDatabaseBackend { private: + class LookupFormatter; + DatabaseManager manager_; protected: @@ -38,13 +40,13 @@ return manager_; } - static int64_t ReadInteger64(const DatabaseManager::CachedStatement& statement, + static int64_t ReadInteger64(const DatabaseManager::StatementBase& statement, size_t field); - static int32_t ReadInteger32(const DatabaseManager::CachedStatement& statement, + static int32_t ReadInteger32(const DatabaseManager::StatementBase& statement, size_t field); - static std::string ReadString(const DatabaseManager::CachedStatement& statement, + static std::string ReadString(const DatabaseManager::StatementBase& statement, size_t field); template @@ -242,7 +244,6 @@ virtual void ClearMainDicomTags(int64_t internalId); - // For unit testing only! virtual uint64_t GetResourcesCount(); @@ -256,5 +257,31 @@ // For unit tests only! virtual void GetChildren(std::list& childrenPublicIds, int64_t id); + +#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 + // New primitive since Orthanc 1.5.2 + virtual void LookupResources(const std::vector& lookup, + OrthancPluginResourceType queryLevel, + uint32_t limit, + bool requestSomeInstance); +#endif + +#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 + // New primitive since Orthanc 1.5.2 + virtual void SetResourcesContent( + uint32_t countIdentifierTags, + const OrthancPluginResourcesContentTags* identifierTags, + uint32_t countMainDicomTags, + const OrthancPluginResourcesContentTags* mainDicomTags, + uint32_t countMetadata, + const OrthancPluginResourcesContentMetadata* metadata); +#endif + +#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 + // New primitive since Orthanc 1.5.2 + virtual void GetChildrenMetadata(std::list& target, + int64_t resourceId, + int32_t metadata); +#endif }; } diff -r 515a783630df -r 122f22550521 Framework/Plugins/OrthancCppDatabasePlugin.h --- a/Framework/Plugins/OrthancCppDatabasePlugin.h Thu Jan 10 13:32:34 2019 +0100 +++ b/Framework/Plugins/OrthancCppDatabasePlugin.h Thu Jan 10 13:33:33 2019 +0100 @@ -32,9 +32,14 @@ # error HAS_ORTHANC_EXCEPTION must be set to 1 #endif +#if ORTHANC_ENABLE_PLUGINS != 1 +# error ORTHANC_ENABLE_PLUGINS must be set to 1 +#endif -#include + #include +#include + #define ORTHANC_PLUGINS_DATABASE_CATCH \ @@ -75,7 +80,9 @@ AllowedAnswers_Attachment, AllowedAnswers_Change, AllowedAnswers_DicomTag, - AllowedAnswers_ExportedResource + AllowedAnswers_ExportedResource, + AllowedAnswers_MatchingResource, + AllowedAnswers_String }; OrthancPluginContext* context_; @@ -243,6 +250,43 @@ OrthancPluginDatabaseAnswerExportedResource(context_, database_, &exported); } + + +#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 + void AnswerMatchingResource(const std::string& resourceId) + { + if (allowedAnswers_ != AllowedAnswers_All && + allowedAnswers_ != AllowedAnswers_MatchingResource) + { + throw std::runtime_error("Cannot answer with an exported resource in the current state"); + } + + OrthancPluginMatchingResource match; + match.resourceId = resourceId.c_str(); + match.someInstanceId = NULL; + + OrthancPluginDatabaseAnswerMatchingResource(context_, database_, &match); + } +#endif + + +#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 + void AnswerMatchingResource(const std::string& resourceId, + const std::string& someInstanceId) + { + if (allowedAnswers_ != AllowedAnswers_All && + allowedAnswers_ != AllowedAnswers_MatchingResource) + { + throw std::runtime_error("Cannot answer with an exported resource in the current state"); + } + + OrthancPluginMatchingResource match; + match.resourceId = resourceId.c_str(); + match.someInstanceId = someInstanceId.c_str(); + + OrthancPluginDatabaseAnswerMatchingResource(context_, database_, &match); + } +#endif }; @@ -447,6 +491,44 @@ OrthancPluginStorageArea* storageArea) = 0; virtual void ClearMainDicomTags(int64_t internalId) = 0; + + virtual bool HasCreateInstance() const = 0; + +#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 + virtual void LookupResources(const std::vector& lookup, + OrthancPluginResourceType queryLevel, + uint32_t limit, + bool requestSomeInstance) = 0; +#endif + +#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 + virtual void CreateInstance(OrthancPluginCreateInstanceResult& result, + const char* hashPatient, + const char* hashStudy, + const char* hashSeries, + const char* hashInstance) + { + throw std::runtime_error("Not implemented"); + } +#endif + + +#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 + virtual void SetResourcesContent( + uint32_t countIdentifierTags, + const OrthancPluginResourcesContentTags* identifierTags, + uint32_t countMainDicomTags, + const OrthancPluginResourcesContentTags* mainDicomTags, + uint32_t countMetadata, + const OrthancPluginResourcesContentMetadata* metadata) = 0; +#endif + + +#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 + virtual void GetChildrenMetadata(std::list& target, + int64_t resourceId, + int32_t metadata) = 0; +#endif }; @@ -1383,6 +1465,7 @@ void* payload) { IDatabaseBackend* backend = reinterpret_cast(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); try { @@ -1398,6 +1481,7 @@ OrthancPluginStorageArea* storageArea) { IDatabaseBackend* backend = reinterpret_cast(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); try { @@ -1412,6 +1496,7 @@ int64_t internalId) { IDatabaseBackend* backend = reinterpret_cast(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); try { @@ -1421,7 +1506,116 @@ ORTHANC_PLUGINS_DATABASE_CATCH } + +#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 + /* Use GetOutput().AnswerResource() */ + static OrthancPluginErrorCode LookupResources( + OrthancPluginDatabaseContext* context, + void* payload, + uint32_t constraintsCount, + const OrthancPluginDatabaseConstraint* constraints, + OrthancPluginResourceType queryLevel, + uint32_t limit, + uint8_t requestSomeInstance) + { + IDatabaseBackend* backend = reinterpret_cast(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_MatchingResource); + + try + { + std::vector lookup; + lookup.reserve(constraintsCount); + + for (uint32_t i = 0; i < constraintsCount; i++) + { + lookup.push_back(Orthanc::DatabaseConstraint(constraints[i])); + } + + backend->LookupResources(lookup, queryLevel, limit, (requestSomeInstance != 0)); + return OrthancPluginErrorCode_Success; + } + ORTHANC_PLUGINS_DATABASE_CATCH + } +#endif + + +#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 + static OrthancPluginErrorCode CreateInstance(OrthancPluginCreateInstanceResult* output, + void* payload, + const char* hashPatient, + const char* hashStudy, + const char* hashSeries, + const char* hashInstance) + { + IDatabaseBackend* backend = reinterpret_cast(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); + + try + { + backend->CreateInstance(*output, hashPatient, hashStudy, hashSeries, hashInstance); + return OrthancPluginErrorCode_Success; + } + ORTHANC_PLUGINS_DATABASE_CATCH + } +#endif + + +#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 + static OrthancPluginErrorCode SetResourcesContent( + void* payload, + uint32_t countIdentifierTags, + const OrthancPluginResourcesContentTags* identifierTags, + uint32_t countMainDicomTags, + const OrthancPluginResourcesContentTags* mainDicomTags, + uint32_t countMetadata, + const OrthancPluginResourcesContentMetadata* metadata) + { + IDatabaseBackend* backend = reinterpret_cast(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); + + try + { + backend->SetResourcesContent(countIdentifierTags, identifierTags, + countMainDicomTags, mainDicomTags, + countMetadata, metadata); + return OrthancPluginErrorCode_Success; + } + ORTHANC_PLUGINS_DATABASE_CATCH + } +#endif + + +#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 + // New primitive since Orthanc 1.5.2 + static OrthancPluginErrorCode GetChildrenMetadata(OrthancPluginDatabaseContext* context, + void* payload, + int64_t resourceId, + int32_t metadata) + { + IDatabaseBackend* backend = reinterpret_cast(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); + + try + { + std::list values; + backend->GetChildrenMetadata(values, resourceId, metadata); + + for (std::list::const_iterator + it = values.begin(); it != values.end(); ++it) + { + OrthancPluginDatabaseAnswerString(backend->GetOutput().context_, + backend->GetOutput().database_, + it->c_str()); + } + + return OrthancPluginErrorCode_Success; + } + ORTHANC_PLUGINS_DATABASE_CATCH + } +#endif + + public: /** * Register a custom database back-end written in C++. @@ -1468,7 +1662,7 @@ params.logExportedResource = LogExportedResource; params.lookupAttachment = LookupAttachment; params.lookupGlobalProperty = LookupGlobalProperty; - params.lookupIdentifier = NULL; // Unused starting with Orthanc 0.9.5 (db v6) + params.lookupIdentifier = NULL; // Unused starting with Orthanc 0.9.5 (db v6) params.lookupIdentifier2 = NULL; // Unused starting with Orthanc 0.9.5 (db v6) params.lookupMetadata = LookupMetadata; params.lookupParent = LookupParent; @@ -1490,25 +1684,38 @@ extensions.getDatabaseVersion = GetDatabaseVersion; extensions.upgradeDatabase = UpgradeDatabase; extensions.clearMainDicomTags = ClearMainDicomTags; - extensions.getAllInternalIds = GetAllInternalIds; // New in Orthanc 0.9.5 (db v6) - extensions.lookupIdentifier3 = LookupIdentifier3; // New in Orthanc 0.9.5 (db v6) + extensions.getAllInternalIds = GetAllInternalIds; // New in Orthanc 0.9.5 (db v6) + extensions.lookupIdentifier3 = LookupIdentifier3; // New in Orthanc 0.9.5 (db v6) bool performanceWarning = true; #if defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE) // Macro introduced in Orthanc 1.3.1 # if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 4, 0) extensions.lookupIdentifierRange = LookupIdentifierRange; // New in Orthanc 1.4.0 - performanceWarning = false; # endif #endif +#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 + // Optimizations brought by Orthanc 1.5.2 + extensions.lookupResources = LookupResources; // Fast lookup + extensions.setResourcesContent = SetResourcesContent; // Fast setting tags/metadata + extensions.getChildrenMetadata = GetChildrenMetadata; + + if (backend.HasCreateInstance()) + { + extensions.createInstance = CreateInstance; // Fast creation of resources + } + + performanceWarning = false; +#endif + if (performanceWarning) { char info[1024]; sprintf(info, "Performance warning: The database index plugin was compiled " "against an old version of the Orthanc SDK (%d.%d.%d): " - "Consider upgrading to version 1.4.0 of the Orthanc SDK", + "Consider upgrading to version 1.5.2 of the Orthanc SDK", ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER, ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER, ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER); diff -r 515a783630df -r 122f22550521 MySQL/CMakeLists.txt --- a/MySQL/CMakeLists.txt Thu Jan 10 13:32:34 2019 +0100 +++ b/MySQL/CMakeLists.txt Thu Jan 10 13:33:33 2019 +0100 @@ -6,6 +6,7 @@ if (ORTHANC_PLUGIN_VERSION STREQUAL "mainline") set(ORTHANC_FRAMEWORK_VERSION "mainline") set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "hg") + set(ORTHANC_FRAMEWORK_BRANCH "db-changes") # TODO - Remove this once out of "db-changes" branch else() set(ORTHANC_FRAMEWORK_VERSION "1.4.0") set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "web") @@ -78,7 +79,6 @@ add_definitions( -DORTHANC_PLUGIN_VERSION="${ORTHANC_PLUGIN_VERSION}" - -DHAS_ORTHANC_EXCEPTION=1 ) set_target_properties(OrthancMySQLStorage PROPERTIES diff -r 515a783630df -r 122f22550521 PostgreSQL/CMakeLists.txt --- a/PostgreSQL/CMakeLists.txt Thu Jan 10 13:32:34 2019 +0100 +++ b/PostgreSQL/CMakeLists.txt Thu Jan 10 13:33:33 2019 +0100 @@ -6,6 +6,7 @@ if (ORTHANC_PLUGIN_VERSION STREQUAL "mainline") set(ORTHANC_FRAMEWORK_VERSION "mainline") set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "hg") + set(ORTHANC_FRAMEWORK_BRANCH "db-changes") # TODO - Remove this once out of "db-changes" branch else() set(ORTHANC_FRAMEWORK_VERSION "1.4.0") set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "web") @@ -51,7 +52,10 @@ EmbedResources( - POSTGRESQL_PREPARE_INDEX ${CMAKE_SOURCE_DIR}/Plugins/PrepareIndex.sql + POSTGRESQL_PREPARE_INDEX ${CMAKE_SOURCE_DIR}/Plugins/PrepareIndex.sql + POSTGRESQL_CREATE_INSTANCE ${CMAKE_SOURCE_DIR}/Plugins/CreateInstance.sql + POSTGRESQL_FAST_TOTAL_SIZE ${CMAKE_SOURCE_DIR}/Plugins/FastTotalSize.sql + POSTGRESQL_FAST_COUNT_RESOURCES ${CMAKE_SOURCE_DIR}/Plugins/FastCountResources.sql ) add_library(OrthancPostgreSQLIndex SHARED @@ -78,7 +82,6 @@ add_definitions( -DORTHANC_PLUGIN_VERSION="${ORTHANC_PLUGIN_VERSION}" - -DHAS_ORTHANC_EXCEPTION=1 ) set_target_properties(OrthancPostgreSQLStorage PROPERTIES diff -r 515a783630df -r 122f22550521 PostgreSQL/NEWS --- a/PostgreSQL/NEWS Thu Jan 10 13:32:34 2019 +0100 +++ b/PostgreSQL/NEWS Thu Jan 10 13:33:33 2019 +0100 @@ -2,6 +2,7 @@ =============================== * New configuration option: "EnableSsl" +* Database optimizations by implementing new primitives of Orthanc SDK 1.5.2 * Fix issue 105 (Unable to connect to PostgreSQL database using SSL) * Fix Debian issue #906771 (Uncaught exception prevents db intialization (likely related to pg_trgm)) diff -r 515a783630df -r 122f22550521 PostgreSQL/Plugins/CreateInstance.sql --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/PostgreSQL/Plugins/CreateInstance.sql Thu Jan 10 13:33:33 2019 +0100 @@ -0,0 +1,68 @@ +CREATE FUNCTION CreateInstance( + IN patient TEXT, + IN study TEXT, + IN series TEXT, + IN instance TEXT, + OUT isNewPatient BIGINT, + OUT isNewStudy BIGINT, + OUT isNewSeries BIGINT, + OUT isNewInstance BIGINT, + OUT patientKey BIGINT, + OUT studyKey BIGINT, + OUT seriesKey BIGINT, + OUT instanceKey BIGINT) AS $body$ + +BEGIN + SELECT internalId FROM Resources INTO instanceKey WHERE publicId = instance AND resourceType = 3; + + IF NOT (instanceKey IS NULL) THEN + -- This instance already exists, stop here + isNewInstance := 0; + ELSE + SELECT internalId FROM Resources INTO patientKey WHERE publicId = patient AND resourceType = 0; + SELECT internalId FROM Resources INTO studyKey WHERE publicId = study AND resourceType = 1; + SELECT internalId FROM Resources INTO seriesKey WHERE publicId = series AND resourceType = 2; + + IF patientKey IS NULL THEN + -- Must create a new patient + ASSERT studyKey IS NULL; + ASSERT seriesKey IS NULL; + ASSERT instanceKey IS NULL; + INSERT INTO Resources VALUES (DEFAULT, 0, patient, NULL) RETURNING internalId INTO patientKey; + isNewPatient := 1; + ELSE + isNewPatient := 0; + END IF; + + ASSERT NOT patientKey IS NULL; + + IF studyKey IS NULL THEN + -- Must create a new study + ASSERT seriesKey IS NULL; + ASSERT instanceKey IS NULL; + INSERT INTO Resources VALUES (DEFAULT, 1, study, patientKey) RETURNING internalId INTO studyKey; + isNewStudy := 1; + ELSE + isNewStudy := 0; + END IF; + + ASSERT NOT studyKey IS NULL; + + IF seriesKey IS NULL THEN + -- Must create a new series + ASSERT instanceKey IS NULL; + INSERT INTO Resources VALUES (DEFAULT, 2, series, studyKey) RETURNING internalId INTO seriesKey; + isNewSeries := 1; + ELSE + isNewSeries := 0; + END IF; + + ASSERT NOT seriesKey IS NULL; + ASSERT instanceKey IS NULL; + + INSERT INTO Resources VALUES (DEFAULT, 3, instance, seriesKey) RETURNING internalId INTO instanceKey; + isNewInstance := 1; + END IF; +END; + +$body$ LANGUAGE plpgsql; diff -r 515a783630df -r 122f22550521 PostgreSQL/Plugins/FastCountResources.sql --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/PostgreSQL/Plugins/FastCountResources.sql Thu Jan 10 13:33:33 2019 +0100 @@ -0,0 +1,33 @@ +-- https://wiki.postgresql.org/wiki/Count_estimate + +INSERT INTO GlobalIntegers +SELECT 2, CAST(COALESCE(COUNT(*), 0) AS BIGINT) FROM Resources WHERE resourceType = 0; -- Count patients + +INSERT INTO GlobalIntegers +SELECT 3, CAST(COALESCE(COUNT(*), 0) AS BIGINT) FROM Resources WHERE resourceType = 1; -- Count studies + +INSERT INTO GlobalIntegers +SELECT 4, CAST(COALESCE(COUNT(*), 0) AS BIGINT) FROM Resources WHERE resourceType = 2; -- Count series + +INSERT INTO GlobalIntegers +SELECT 5, CAST(COALESCE(COUNT(*), 0) AS BIGINT) FROM Resources WHERE resourceType = 3; -- Count instances + + +CREATE OR REPLACE FUNCTION CountResourcesTrackerFunc() +RETURNS TRIGGER AS $$ +BEGIN + IF TG_OP = 'INSERT' THEN + UPDATE GlobalIntegers SET value = value + 1 WHERE key = new.resourceType + 2; + RETURN new; + ELSIF TG_OP = 'DELETE' THEN + UPDATE GlobalIntegers SET value = value - 1 WHERE key = old.resourceType + 2; + RETURN old; + END IF; +END; +$$ LANGUAGE plpgsql; + + +CREATE TRIGGER CountResourcesTracker +AFTER INSERT OR DELETE ON Resources +FOR EACH ROW +EXECUTE PROCEDURE CountResourcesTrackerFunc(); diff -r 515a783630df -r 122f22550521 PostgreSQL/Plugins/FastTotalSize.sql --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/PostgreSQL/Plugins/FastTotalSize.sql Thu Jan 10 13:33:33 2019 +0100 @@ -0,0 +1,41 @@ +CREATE TABLE GlobalIntegers( + key INTEGER PRIMARY KEY, + value BIGINT); + +INSERT INTO GlobalIntegers +SELECT 0, CAST(COALESCE(SUM(compressedSize), 0) AS BIGINT) FROM AttachedFiles; + +INSERT INTO GlobalIntegers +SELECT 1, CAST(COALESCE(SUM(uncompressedSize), 0) AS BIGINT) FROM AttachedFiles; + + + +CREATE FUNCTION AttachedFileIncrementSizeFunc() +RETURNS TRIGGER AS $body$ +BEGIN + UPDATE GlobalIntegers SET value = value + new.compressedSize WHERE key = 0; + UPDATE GlobalIntegers SET value = value + new.uncompressedSize WHERE key = 1; + RETURN NULL; +END; +$body$ LANGUAGE plpgsql; + +CREATE FUNCTION AttachedFileDecrementSizeFunc() +RETURNS TRIGGER AS $body$ +BEGIN + UPDATE GlobalIntegers SET value = value - old.compressedSize WHERE key = 0; + UPDATE GlobalIntegers SET value = value - old.uncompressedSize WHERE key = 1; + RETURN NULL; +END; +$body$ LANGUAGE plpgsql; + + + +CREATE TRIGGER AttachedFileIncrementSize +AFTER INSERT ON AttachedFiles +FOR EACH ROW +EXECUTE PROCEDURE AttachedFileIncrementSizeFunc(); + +CREATE TRIGGER AttachedFileDecrementSize +AFTER DELETE ON AttachedFiles +FOR EACH ROW +EXECUTE PROCEDURE AttachedFileDecrementSizeFunc(); diff -r 515a783630df -r 122f22550521 PostgreSQL/Plugins/PostgreSQLIndex.cpp --- a/PostgreSQL/Plugins/PostgreSQLIndex.cpp Thu Jan 10 13:32:34 2019 +0100 +++ b/PostgreSQL/Plugins/PostgreSQLIndex.cpp Thu Jan 10 13:33:33 2019 +0100 @@ -35,6 +35,8 @@ { // Some aliases for internal properties static const GlobalProperty GlobalProperty_HasTrigramIndex = GlobalProperty_DatabaseInternal0; + static const GlobalProperty GlobalProperty_HasCreateInstance = GlobalProperty_DatabaseInternal1; + static const GlobalProperty GlobalProperty_HasFastCountResources = GlobalProperty_DatabaseInternal2; } @@ -126,7 +128,8 @@ PostgreSQLTransaction t(*db); int hasTrigram = 0; - if (!LookupGlobalIntegerProperty(hasTrigram, *db, t, Orthanc::GlobalProperty_HasTrigramIndex) || + if (!LookupGlobalIntegerProperty(hasTrigram, *db, t, + Orthanc::GlobalProperty_HasTrigramIndex) || hasTrigram != 1) { /** @@ -162,6 +165,75 @@ << "PostgreSQL server, e.g. on Debian: sudo apt install postgresql-contrib"; } } + else + { + t.Commit(); + } + } + + { + PostgreSQLTransaction t(*db); + + int hasCreateInstance = 0; + if (!LookupGlobalIntegerProperty(hasCreateInstance, *db, t, + Orthanc::GlobalProperty_HasCreateInstance) || + hasCreateInstance != 1) + { + LOG(INFO) << "Installing the CreateInstance extension"; + + std::string query; + Orthanc::EmbeddedResources::GetFileResource + (query, Orthanc::EmbeddedResources::POSTGRESQL_CREATE_INSTANCE); + db->Execute(query); + + SetGlobalIntegerProperty(*db, t, Orthanc::GlobalProperty_HasCreateInstance, 1); + } + + t.Commit(); + } + + { + PostgreSQLTransaction t(*db); + + int hasFastTotalSize = 0; + if (!LookupGlobalIntegerProperty(hasFastTotalSize, *db, t, + Orthanc::GlobalProperty_GetTotalSizeIsFast) || + hasFastTotalSize != 1) + { + LOG(INFO) << "Installing the FastTotalSize extension"; + + std::string query; + Orthanc::EmbeddedResources::GetFileResource + (query, Orthanc::EmbeddedResources::POSTGRESQL_FAST_TOTAL_SIZE); + db->Execute(query); + + SetGlobalIntegerProperty(*db, t, Orthanc::GlobalProperty_GetTotalSizeIsFast, 1); + } + + t.Commit(); + } + + { + PostgreSQLTransaction t(*db); + + // Installing this extension requires the "GlobalIntegers" table + // created by the "FastTotalSize" extension + int property = 0; + if (!LookupGlobalIntegerProperty(property, *db, t, + Orthanc::GlobalProperty_HasFastCountResources) || + property != 1) + { + LOG(INFO) << "Installing the FastCountResources extension"; + + std::string query; + Orthanc::EmbeddedResources::GetFileResource + (query, Orthanc::EmbeddedResources::POSTGRESQL_FAST_COUNT_RESOURCES); + db->Execute(query); + + SetGlobalIntegerProperty(*db, t, Orthanc::GlobalProperty_HasFastCountResources, 1); + } + + t.Commit(); } return db.release(); @@ -195,4 +267,131 @@ return ReadInteger64(statement, 0); } + + + uint64_t PostgreSQLIndex::GetTotalCompressedSize() + { + // Fast version if extension "./FastTotalSize.sql" is installed + uint64_t result; + + { + DatabaseManager::CachedStatement statement( + STATEMENT_FROM_HERE, GetManager(), + "SELECT value FROM GlobalIntegers WHERE key = 0"); + + statement.SetReadOnly(true); + statement.Execute(); + + result = static_cast(ReadInteger64(statement, 0)); + } + + assert(result == IndexBackend::GetTotalCompressedSize()); + return result; + } + + + uint64_t PostgreSQLIndex::GetTotalUncompressedSize() + { + // Fast version if extension "./FastTotalSize.sql" is installed + uint64_t result; + + { + DatabaseManager::CachedStatement statement( + STATEMENT_FROM_HERE, GetManager(), + "SELECT value FROM GlobalIntegers WHERE key = 1"); + + statement.SetReadOnly(true); + statement.Execute(); + + result = static_cast(ReadInteger64(statement, 0)); + } + + assert(result == IndexBackend::GetTotalUncompressedSize()); + return result; + } + + +#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 + void PostgreSQLIndex::CreateInstance(OrthancPluginCreateInstanceResult& result, + const char* hashPatient, + const char* hashStudy, + const char* hashSeries, + const char* hashInstance) + { + DatabaseManager::CachedStatement statement( + STATEMENT_FROM_HERE, GetManager(), + "SELECT * FROM CreateInstance(${patient}, ${study}, ${series}, ${instance})"); + + statement.SetParameterType("patient", ValueType_Utf8String); + statement.SetParameterType("study", ValueType_Utf8String); + statement.SetParameterType("series", ValueType_Utf8String); + statement.SetParameterType("instance", ValueType_Utf8String); + + Dictionary args; + args.SetUtf8Value("patient", hashPatient); + args.SetUtf8Value("study", hashStudy); + args.SetUtf8Value("series", hashSeries); + args.SetUtf8Value("instance", hashInstance); + + statement.Execute(args); + + if (statement.IsDone() || + statement.GetResultFieldsCount() != 8) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_Database); + } + + for (size_t i = 0; i < 8; i++) + { + statement.SetResultFieldType(i, ValueType_Integer64); + } + + result.isNewInstance = (ReadInteger64(statement, 3) == 1); + result.instanceId = ReadInteger64(statement, 7); + + if (result.isNewInstance) + { + result.isNewPatient = (ReadInteger64(statement, 0) == 1); + result.isNewStudy = (ReadInteger64(statement, 1) == 1); + result.isNewSeries = (ReadInteger64(statement, 2) == 1); + result.patientId = ReadInteger64(statement, 4); + result.studyId = ReadInteger64(statement, 5); + result.seriesId = ReadInteger64(statement, 6); + } + } +#endif + + + uint64_t PostgreSQLIndex::GetResourceCount(OrthancPluginResourceType resourceType) + { + // Optimized version thanks to the "FastCountResources.sql" extension + + assert(OrthancPluginResourceType_Patient == 0 && + OrthancPluginResourceType_Study == 1 && + OrthancPluginResourceType_Series == 2 && + OrthancPluginResourceType_Instance == 3); + + uint64_t result; + + { + DatabaseManager::CachedStatement statement( + STATEMENT_FROM_HERE, GetManager(), + "SELECT value FROM GlobalIntegers WHERE key = ${key}"); + + statement.SetParameterType("key", ValueType_Integer64); + + Dictionary args; + + // For an explanation of the "+ 2" below, check out "FastCountResources.sql" + args.SetIntegerValue("key", static_cast(resourceType + 2)); + + statement.SetReadOnly(true); + statement.Execute(args); + + result = static_cast(ReadInteger64(statement, 0)); + } + + assert(result == IndexBackend::GetResourceCount(resourceType)); + return result; + } } diff -r 515a783630df -r 122f22550521 PostgreSQL/Plugins/PostgreSQLIndex.h --- a/PostgreSQL/Plugins/PostgreSQLIndex.h Thu Jan 10 13:32:34 2019 +0100 +++ b/PostgreSQL/Plugins/PostgreSQLIndex.h Thu Jan 10 13:33:33 2019 +0100 @@ -71,6 +71,28 @@ } virtual int64_t CreateResource(const char* publicId, - OrthancPluginResourceType type); + OrthancPluginResourceType type) + ORTHANC_OVERRIDE; + + virtual uint64_t GetTotalCompressedSize() ORTHANC_OVERRIDE; + + virtual uint64_t GetTotalUncompressedSize() ORTHANC_OVERRIDE; + + virtual bool HasCreateInstance() const ORTHANC_OVERRIDE + { + return true; + } + +#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 + virtual void CreateInstance(OrthancPluginCreateInstanceResult& result, + const char* hashPatient, + const char* hashStudy, + const char* hashSeries, + const char* hashInstance) + ORTHANC_OVERRIDE; +#endif + + virtual uint64_t GetResourceCount(OrthancPluginResourceType resourceType) + ORTHANC_OVERRIDE; }; } diff -r 515a783630df -r 122f22550521 PostgreSQL/UnitTests/PostgreSQLTests.cpp --- a/PostgreSQL/UnitTests/PostgreSQLTests.cpp Thu Jan 10 13:32:34 2019 +0100 +++ b/PostgreSQL/UnitTests/PostgreSQLTests.cpp Thu Jan 10 13:33:33 2019 +0100 @@ -34,6 +34,7 @@ # undef S_IXOTH #endif +#include "../Plugins/PostgreSQLIndex.h" #include "../Plugins/PostgreSQLStorageArea.h" #include "../../Framework/PostgreSQL/PostgreSQLTransaction.h" #include "../../Framework/PostgreSQL/PostgreSQLResult.h" @@ -437,3 +438,78 @@ ASSERT_TRUE(db->DoesTableExist("test2")); } + +#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 +TEST(PostgreSQLIndex, CreateInstance) +{ + OrthancDatabases::PostgreSQLIndex db(globalParameters_); + db.SetClearAll(true); + db.Open(); + + std::string s; + ASSERT_TRUE(db.LookupGlobalProperty(s, Orthanc::GlobalProperty_DatabaseInternal1)); + ASSERT_EQ("1", s); + + OrthancPluginCreateInstanceResult r1, r2; + + memset(&r1, 0, sizeof(r1)); + db.CreateInstance(r1, "a", "b", "c", "d"); + ASSERT_TRUE(r1.isNewInstance); + ASSERT_TRUE(r1.isNewSeries); + ASSERT_TRUE(r1.isNewStudy); + ASSERT_TRUE(r1.isNewPatient); + + memset(&r2, 0, sizeof(r2)); + db.CreateInstance(r2, "a", "b", "c", "d"); + ASSERT_FALSE(r2.isNewInstance); + ASSERT_EQ(r1.instanceId, r2.instanceId); + + // Breaking the hierarchy + memset(&r2, 0, sizeof(r2)); + ASSERT_THROW(db.CreateInstance(r2, "a", "e", "c", "f"), Orthanc::OrthancException); + + memset(&r2, 0, sizeof(r2)); + db.CreateInstance(r2, "a", "b", "c", "e"); + ASSERT_TRUE(r2.isNewInstance); + ASSERT_FALSE(r2.isNewSeries); + ASSERT_FALSE(r2.isNewStudy); + ASSERT_FALSE(r2.isNewPatient); + ASSERT_EQ(r1.patientId, r2.patientId); + ASSERT_EQ(r1.studyId, r2.studyId); + ASSERT_EQ(r1.seriesId, r2.seriesId); + ASSERT_NE(r1.instanceId, r2.instanceId); + + memset(&r2, 0, sizeof(r2)); + db.CreateInstance(r2, "a", "b", "f", "g"); + ASSERT_TRUE(r2.isNewInstance); + ASSERT_TRUE(r2.isNewSeries); + ASSERT_FALSE(r2.isNewStudy); + ASSERT_FALSE(r2.isNewPatient); + ASSERT_EQ(r1.patientId, r2.patientId); + ASSERT_EQ(r1.studyId, r2.studyId); + ASSERT_NE(r1.seriesId, r2.seriesId); + ASSERT_NE(r1.instanceId, r2.instanceId); + + memset(&r2, 0, sizeof(r2)); + db.CreateInstance(r2, "a", "h", "i", "j"); + ASSERT_TRUE(r2.isNewInstance); + ASSERT_TRUE(r2.isNewSeries); + ASSERT_TRUE(r2.isNewStudy); + ASSERT_FALSE(r2.isNewPatient); + ASSERT_EQ(r1.patientId, r2.patientId); + ASSERT_NE(r1.studyId, r2.studyId); + ASSERT_NE(r1.seriesId, r2.seriesId); + ASSERT_NE(r1.instanceId, r2.instanceId); + + memset(&r2, 0, sizeof(r2)); + db.CreateInstance(r2, "k", "l", "m", "n"); + ASSERT_TRUE(r2.isNewInstance); + ASSERT_TRUE(r2.isNewSeries); + ASSERT_TRUE(r2.isNewStudy); + ASSERT_TRUE(r2.isNewPatient); + ASSERT_NE(r1.patientId, r2.patientId); + ASSERT_NE(r1.studyId, r2.studyId); + ASSERT_NE(r1.seriesId, r2.seriesId); + ASSERT_NE(r1.instanceId, r2.instanceId); +} +#endif diff -r 515a783630df -r 122f22550521 Resources/CMake/DatabasesPluginConfiguration.cmake --- a/Resources/CMake/DatabasesPluginConfiguration.cmake Thu Jan 10 13:32:34 2019 +0100 +++ b/Resources/CMake/DatabasesPluginConfiguration.cmake Thu Jan 10 13:33:33 2019 +0100 @@ -40,10 +40,20 @@ endif() +add_definitions( + -DHAS_ORTHANC_EXCEPTION=1 + -DORTHANC_ENABLE_PLUGINS=1 + ) + + list(APPEND DATABASES_SOURCES ${ORTHANC_CORE_SOURCES} ${ORTHANC_DATABASES_ROOT}/Framework/Plugins/GlobalProperties.cpp ${ORTHANC_DATABASES_ROOT}/Framework/Plugins/IndexBackend.cpp ${ORTHANC_DATABASES_ROOT}/Framework/Plugins/StorageBackend.cpp ${ORTHANC_ROOT}/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp + + # New from "db-changes" + ${ORTHANC_ROOT}/OrthancServer/Search/DatabaseConstraint.cpp + ${ORTHANC_ROOT}/OrthancServer/Search/ISqlLookupFormatter.cpp ) diff -r 515a783630df -r 122f22550521 SQLite/CMakeLists.txt --- a/SQLite/CMakeLists.txt Thu Jan 10 13:32:34 2019 +0100 +++ b/SQLite/CMakeLists.txt Thu Jan 10 13:33:33 2019 +0100 @@ -6,6 +6,7 @@ if (ORTHANC_PLUGIN_VERSION STREQUAL "mainline") set(ORTHANC_FRAMEWORK_VERSION "mainline") set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "hg") + set(ORTHANC_FRAMEWORK_BRANCH "db-changes") # TODO - Remove this once out of "db-changes" branch else() set(ORTHANC_FRAMEWORK_VERSION "1.4.0") set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "web") @@ -34,7 +35,6 @@ add_definitions( -DORTHANC_PLUGIN_VERSION="${ORTHANC_PLUGIN_VERSION}" - -DHAS_ORTHANC_EXCEPTION=1 ) #set_target_properties(OrthancSQLiteStorage PROPERTIES