Mercurial > hg > orthanc-databases
changeset 92:2e4f73786199 db-changes
integration mainline->db-changes
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Thu, 17 Jan 2019 11:42:42 +0100 |
parents | e61587582cef (diff) 1bd538a5a783 (current diff) |
children | 5571a6554db0 |
files | |
diffstat | 24 files changed, 1511 insertions(+), 148 deletions(-) [+] |
line wrap: on
line diff
--- a/Framework/Common/DatabaseManager.cpp Thu Jan 17 11:42:24 2019 +0100 +++ b/Framework/Common/DatabaseManager.cpp Thu Jan 17 11:42:42 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<Query> 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<IResult> 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> 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> 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; + } } }
--- a/Framework/Common/DatabaseManager.h Thu Jan 17 11:42:24 2019 +0100 +++ b/Framework/Common/DatabaseManager.h Thu Jan 17 11:42:42 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> query_; std::auto_ptr<IResult> 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<IPrecompiledStatement> statement_; + + public: + StandaloneStatement(DatabaseManager& manager, + const std::string& sql); + + virtual ~StandaloneStatement(); + + void Execute() + { + Dictionary parameters; + Execute(parameters); + } + + void Execute(const Dictionary& parameters); + }; }; }
--- a/Framework/Common/Query.cpp Thu Jan 17 11:42:24 2019 +0100 +++ b/Framework/Common/Query.cpp Thu Jan 17 11:42:42 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 {
--- a/Framework/Plugins/IndexBackend.cpp Thu Jan 17 11:42:24 2019 +0100 +++ b/Framework/Plugins/IndexBackend.cpp Thu Jan 17 11:42:42 2019 +0100 @@ -29,6 +29,7 @@ #include <Core/Logging.h> #include <Core/OrthancException.h> #include <OrthancServer/ServerEnumerations.h> +#include <OrthancServer/Search/ISqlLookupFormatter.h> 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,375 @@ 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<std::string>(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<std::string>(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<Orthanc::DatabaseConstraint>& 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<std::string>(i); + + args.SetUtf8Value(name, tags[i].value); + + std::string insert = ("(" + boost::lexical_cast<std::string>(tags[i].resource) + ", " + + boost::lexical_cast<std::string>(tags[i].group) + ", " + + boost::lexical_cast<std::string>(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<std::string>(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<std::string>(i); + + args.SetUtf8Value(name, metadata[i].value); + + std::string insert = ("(" + boost::lexical_cast<std::string>(metadata[i].resource) + ", " + + boost::lexical_cast<std::string>(metadata[i].metadata) + ", " + + "${" + name + "})"); + + std::string remove = ("(id=" + boost::lexical_cast<std::string>(metadata[i].resource) + + " AND type=" + boost::lexical_cast<std::string>(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<std::string>(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 + + + // New primitive since Orthanc 1.5.2 + void IndexBackend::GetChildrenMetadata(std::list<std::string>& 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<int>(resourceId)); + args.SetIntegerValue("metadata", static_cast<int>(metadata)); + + ReadListOfStrings(target, statement, args); + } + + + // New primitive since Orthanc 1.5.2 + void IndexBackend::TagMostRecentPatient(int64_t patient) + { + int64_t seq; + + { + DatabaseManager::CachedStatement statement( + STATEMENT_FROM_HERE, manager_, + "SELECT * FROM PatientRecyclingOrder WHERE seq >= " + "(SELECT seq FROM PatientRecyclingOrder WHERE patientid=${id}) ORDER BY seq LIMIT 2"); + + statement.SetReadOnly(true); + statement.SetParameterType("id", ValueType_Integer64); + + Dictionary args; + args.SetIntegerValue("id", patient); + + statement.Execute(args); + + if (statement.IsDone()) + { + // The patient is protected, don't add it to the recycling order + return; + } + + seq = ReadInteger64(statement, 0); + + statement.Next(); + + if (statement.IsDone()) + { + // The patient is already at the end of the recycling order + // (because of the "LIMIT 2" above), no need to modify the table + return; + } + } + + // Delete the old position of the patient in the recycling order + + { + DatabaseManager::CachedStatement statement( + STATEMENT_FROM_HERE, manager_, + "DELETE FROM PatientRecyclingOrder WHERE seq=${seq}"); + + statement.SetParameterType("seq", ValueType_Integer64); + + Dictionary args; + args.SetIntegerValue("seq", seq); + + statement.Execute(args); + } + + // Add the patient to the end of the recycling order + + { + DatabaseManager::CachedStatement statement( + STATEMENT_FROM_HERE, manager_, + "INSERT INTO PatientRecyclingOrder VALUES(${}, ${id})"); + + statement.SetParameterType("id", ValueType_Integer64); + + Dictionary args; + args.SetIntegerValue("id", patient); + + statement.Execute(args); + } + } }
--- a/Framework/Plugins/IndexBackend.h Thu Jan 17 11:42:24 2019 +0100 +++ b/Framework/Plugins/IndexBackend.h Thu Jan 17 11:42:42 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 <typename T> @@ -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<std::string>& childrenPublicIds, int64_t id); + +#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 + // New primitive since Orthanc 1.5.2 + virtual void LookupResources(const std::vector<Orthanc::DatabaseConstraint>& 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 + + // New primitive since Orthanc 1.5.2 + virtual void GetChildrenMetadata(std::list<std::string>& target, + int64_t resourceId, + int32_t metadata); + + virtual void TagMostRecentPatient(int64_t patient); }; }
--- a/Framework/Plugins/OrthancCppDatabasePlugin.h Thu Jan 17 11:42:24 2019 +0100 +++ b/Framework/Plugins/OrthancCppDatabasePlugin.h Thu Jan 17 11:42:42 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 <orthanc/OrthancCDatabasePlugin.h> + #include <Core/OrthancException.h> +#include <OrthancServer/Search/DatabaseConstraint.h> + #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,49 @@ OrthancPluginStorageArea* storageArea) = 0; virtual void ClearMainDicomTags(int64_t internalId) = 0; + + virtual bool HasCreateInstance() const + { + return false; + } + +#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 + virtual void LookupResources(const std::vector<Orthanc::DatabaseConstraint>& 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 + + + virtual void GetChildrenMetadata(std::list<std::string>& target, + int64_t resourceId, + int32_t metadata) = 0; + + virtual int64_t GetLastChangeIndex() = 0; + + virtual void TagMostRecentPatient(int64_t patientId) = 0; }; @@ -1383,6 +1470,7 @@ void* payload) { IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); try { @@ -1398,6 +1486,7 @@ OrthancPluginStorageArea* storageArea) { IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); try { @@ -1412,6 +1501,7 @@ int64_t internalId) { IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); try { @@ -1421,7 +1511,145 @@ 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<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_MatchingResource); + + try + { + std::vector<Orthanc::DatabaseConstraint> 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<IDatabaseBackend*>(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<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); + + try + { + backend->SetResourcesContent(countIdentifierTags, identifierTags, + countMainDicomTags, mainDicomTags, + countMetadata, metadata); + return OrthancPluginErrorCode_Success; + } + ORTHANC_PLUGINS_DATABASE_CATCH + } +#endif + + // New primitive since Orthanc 1.5.2 + static OrthancPluginErrorCode GetChildrenMetadata(OrthancPluginDatabaseContext* context, + void* payload, + int64_t resourceId, + int32_t metadata) + { + IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); + + try + { + std::list<std::string> values; + backend->GetChildrenMetadata(values, resourceId, metadata); + + for (std::list<std::string>::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 + } + + + // New primitive since Orthanc 1.5.2 + static OrthancPluginErrorCode GetLastChangeIndex(int64_t* result, + void* payload) + { + IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); + + try + { + *result = backend->GetLastChangeIndex(); + return OrthancPluginErrorCode_Success; + } + ORTHANC_PLUGINS_DATABASE_CATCH + } + + + // New primitive since Orthanc 1.5.2 + static OrthancPluginErrorCode TagMostRecentPatient(void* payload, + int64_t patientId) + { + IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); + + try + { + backend->TagMostRecentPatient(patientId); + return OrthancPluginErrorCode_Success; + } + ORTHANC_PLUGINS_DATABASE_CATCH + } + + public: /** * Register a custom database back-end written in C++. @@ -1468,7 +1696,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 +1718,40 @@ 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; + extensions.getLastChangeIndex = GetLastChangeIndex; + extensions.tagMostRecentPatient = TagMostRecentPatient; + + 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);
--- a/MySQL/CMakeLists.txt Thu Jan 17 11:42:24 2019 +0100 +++ b/MySQL/CMakeLists.txt Thu Jan 17 11:42:42 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,8 @@ EmbedResources( - MYSQL_PREPARE_INDEX ${CMAKE_SOURCE_DIR}/Plugins/PrepareIndex.sql + MYSQL_PREPARE_INDEX ${CMAKE_SOURCE_DIR}/Plugins/PrepareIndex.sql + MYSQL_GET_LAST_CHANGE_INDEX ${CMAKE_SOURCE_DIR}/Plugins/GetLastChangeIndex.sql ) add_library(OrthancMySQLIndex SHARED @@ -78,7 +80,6 @@ add_definitions( -DORTHANC_PLUGIN_VERSION="${ORTHANC_PLUGIN_VERSION}" - -DHAS_ORTHANC_EXCEPTION=1 ) set_target_properties(OrthancMySQLStorage PROPERTIES
--- a/MySQL/NEWS Thu Jan 17 11:42:24 2019 +0100 +++ b/MySQL/NEWS Thu Jan 17 11:42:42 2019 +0100 @@ -1,6 +1,7 @@ Pending changes in the mainline =============================== +* Database optimizations by implementing new primitives of Orthanc SDK 1.5.2 * Characters "$" and "_" are allowed in MySQL database identifiers * Fix serialization of jobs if many of them
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MySQL/Plugins/GetLastChangeIndex.sql Thu Jan 17 11:42:42 2019 +0100 @@ -0,0 +1,16 @@ +CREATE TABLE GlobalIntegers( + property INTEGER PRIMARY KEY, + value BIGINT + ); + + +INSERT INTO GlobalIntegers +SELECT 0, COALESCE(MAX(seq), 0) FROM Changes; + + +CREATE TRIGGER ChangeAdded +AFTER INSERT ON Changes +FOR EACH ROW +BEGIN + UPDATE GlobalIntegers SET value = new.seq WHERE property = 0@ +END;
--- a/MySQL/Plugins/MySQLIndex.cpp Thu Jan 17 11:42:24 2019 +0100 +++ b/MySQL/Plugins/MySQLIndex.cpp Thu Jan 17 11:42:42 2019 +0100 @@ -128,7 +128,19 @@ SetGlobalIntegerProperty(*db, t, Orthanc::GlobalProperty_DatabasePatchLevel, revision); } - if (revision != 2) + if (revision == 2) + { + std::string query; + + Orthanc::EmbeddedResources::GetFileResource + (query, Orthanc::EmbeddedResources::MYSQL_GET_LAST_CHANGE_INDEX); + db->Execute(query, true); + + revision = 3; + SetGlobalIntegerProperty(*db, t, Orthanc::GlobalProperty_DatabasePatchLevel, revision); + } + + if (revision != 3) { LOG(ERROR) << "MySQL plugin is incompatible with database schema revision: " << revision; throw Orthanc::OrthancException(Orthanc::ErrorCode_Database); @@ -261,4 +273,17 @@ SignalDeletedFiles(); } + + + int64_t MySQLIndex::GetLastChangeIndex() + { + DatabaseManager::CachedStatement statement( + STATEMENT_FROM_HERE, GetManager(), + "SELECT value FROM GlobalIntegers WHERE property = 0"); + + statement.SetReadOnly(true); + statement.Execute(); + + return ReadInteger64(statement, 0); + } }
--- a/MySQL/Plugins/MySQLIndex.h Thu Jan 17 11:42:24 2019 +0100 +++ b/MySQL/Plugins/MySQLIndex.h Thu Jan 17 11:42:42 2019 +0100 @@ -74,5 +74,7 @@ OrthancPluginResourceType type); virtual void DeleteResource(int64_t id); + + virtual int64_t GetLastChangeIndex(); }; }
--- a/PostgreSQL/CMakeLists.txt Thu Jan 17 11:42:24 2019 +0100 +++ b/PostgreSQL/CMakeLists.txt Thu Jan 17 11:42:42 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,11 @@ 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 + POSTGRESQL_GET_LAST_CHANGE_INDEX ${CMAKE_SOURCE_DIR}/Plugins/GetLastChangeIndex.sql ) add_library(OrthancPostgreSQLIndex SHARED @@ -78,7 +83,6 @@ add_definitions( -DORTHANC_PLUGIN_VERSION="${ORTHANC_PLUGIN_VERSION}" - -DHAS_ORTHANC_EXCEPTION=1 ) set_target_properties(OrthancPostgreSQLStorage PROPERTIES
--- a/PostgreSQL/NEWS Thu Jan 17 11:42:24 2019 +0100 +++ b/PostgreSQL/NEWS Thu Jan 17 11:42:42 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))
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/PostgreSQL/Plugins/CreateInstance.sql Thu Jan 17 11:42:42 2019 +0100 @@ -0,0 +1,85 @@ +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$ + +DECLARE + patientSeq BIGINT; + countRecycling BIGINT; + +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; + + -- Move the patient to the end of the recycling order + SELECT seq FROM PatientRecyclingOrder WHERE patientId = patientKey INTO patientSeq; + + IF NOT (patientSeq IS NULL) THEN + -- The patient is not protected + SELECT COUNT(*) FROM (SELECT * FROM PatientRecyclingOrder WHERE seq >= patientSeq LIMIT 2) AS tmp INTO countRecycling; + IF countRecycling = 2 THEN + -- The patient was not at the end of the recycling order + DELETE FROM PatientRecyclingOrder WHERE seq = patientSeq; + INSERT INTO PatientRecyclingOrder VALUES(DEFAULT, patientKey); + END IF; + END IF; + END IF; +END; + +$body$ LANGUAGE plpgsql;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/PostgreSQL/Plugins/FastCountResources.sql Thu Jan 17 11:42:42 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();
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/PostgreSQL/Plugins/FastTotalSize.sql Thu Jan 17 11:42:42 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();
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/PostgreSQL/Plugins/GetLastChangeIndex.sql Thu Jan 17 11:42:42 2019 +0100 @@ -0,0 +1,27 @@ +-- In PostgreSQL, the most straightforward query would be to run: + +-- SELECT currval(pg_get_serial_sequence('Changes', 'seq'))". + +-- Unfortunately, this raises the error message "currval of sequence +-- "changes_seq_seq" is not yet defined in this session" if no change +-- has been inserted before the SELECT. We thus track the sequence +-- index with a trigger. +-- http://www.neilconway.org/docs/sequences/ + +INSERT INTO GlobalIntegers +SELECT 6, CAST(COALESCE(MAX(seq), 0) AS BIGINT) FROM Changes; + + +CREATE FUNCTION InsertedChangeFunc() +RETURNS TRIGGER AS $body$ +BEGIN + UPDATE GlobalIntegers SET value = new.seq WHERE key = 6; + RETURN NULL; +END; +$body$ LANGUAGE plpgsql; + + +CREATE TRIGGER InsertedChange +AFTER INSERT ON Changes +FOR EACH ROW +EXECUTE PROCEDURE InsertedChangeFunc();
--- a/PostgreSQL/Plugins/PostgreSQLIndex.cpp Thu Jan 17 11:42:24 2019 +0100 +++ b/PostgreSQL/Plugins/PostgreSQLIndex.cpp Thu Jan 17 11:42:42 2019 +0100 @@ -35,6 +35,9 @@ { // 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; + static const GlobalProperty GlobalProperty_GetLastChangeIndex = GlobalProperty_DatabaseInternal3; } @@ -126,7 +129,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 +166,89 @@ << "PostgreSQL server, e.g. on Debian: sudo apt install postgresql-contrib"; } } + else + { + t.Commit(); + } + } + + { + PostgreSQLTransaction t(*db); + + int property = 0; + if (!LookupGlobalIntegerProperty(property, *db, t, + Orthanc::GlobalProperty_HasCreateInstance) || + property != 2) + { + LOG(INFO) << "Installing the CreateInstance extension"; + + if (property == 1) + { + // Drop older, experimental versions of this extension + db->Execute("DROP FUNCTION CreateInstance(" + "IN patient TEXT, IN study TEXT, IN series TEXT, in instance TEXT)"); + } + + std::string query; + Orthanc::EmbeddedResources::GetFileResource + (query, Orthanc::EmbeddedResources::POSTGRESQL_CREATE_INSTANCE); + db->Execute(query); + + SetGlobalIntegerProperty(*db, t, Orthanc::GlobalProperty_HasCreateInstance, 2); + } + + + if (!LookupGlobalIntegerProperty(property, *db, t, + Orthanc::GlobalProperty_GetTotalSizeIsFast) || + property != 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); + } + + + // Installing this extension requires the "GlobalIntegers" table + // created by the "FastTotalSize" extension + 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); + } + + + // Installing this extension requires the "GlobalIntegers" table + // created by the "GetLastChangeIndex" extension + property = 0; + if (!LookupGlobalIntegerProperty(property, *db, t, + Orthanc::GlobalProperty_GetLastChangeIndex) || + property != 1) + { + LOG(INFO) << "Installing the GetLastChangeIndex extension"; + + std::string query; + Orthanc::EmbeddedResources::GetFileResource + (query, Orthanc::EmbeddedResources::POSTGRESQL_GET_LAST_CHANGE_INDEX); + db->Execute(query); + + SetGlobalIntegerProperty(*db, t, Orthanc::GlobalProperty_GetLastChangeIndex, 1); + } + + t.Commit(); } return db.release(); @@ -195,4 +282,152 @@ 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<uint64_t>(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<uint64_t>(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<int>(resourceType + 2)); + + statement.SetReadOnly(true); + statement.Execute(args); + + result = static_cast<uint64_t>(ReadInteger64(statement, 0)); + } + + assert(result == IndexBackend::GetResourceCount(resourceType)); + return result; + } + + + int64_t PostgreSQLIndex::GetLastChangeIndex() + { + DatabaseManager::CachedStatement statement( + STATEMENT_FROM_HERE, GetManager(), + "SELECT value FROM GlobalIntegers WHERE key = 6"); + + statement.SetReadOnly(true); + statement.Execute(); + + return ReadInteger64(statement, 0); + } + + + void PostgreSQLIndex::TagMostRecentPatient(int64_t patient) + { + // This behavior is implemented in "CreateInstance()", and no + // backward compatibility is necessary + throw Orthanc::OrthancException(Orthanc::ErrorCode_Database); + } }
--- a/PostgreSQL/Plugins/PostgreSQLIndex.h Thu Jan 17 11:42:24 2019 +0100 +++ b/PostgreSQL/Plugins/PostgreSQLIndex.h Thu Jan 17 11:42:42 2019 +0100 @@ -71,6 +71,32 @@ } 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; + + virtual int64_t GetLastChangeIndex() ORTHANC_OVERRIDE; + + virtual void TagMostRecentPatient(int64_t patient) ORTHANC_OVERRIDE; }; }
--- a/PostgreSQL/UnitTests/PostgreSQLTests.cpp Thu Jan 17 11:42:24 2019 +0100 +++ b/PostgreSQL/UnitTests/PostgreSQLTests.cpp Thu Jan 17 11:42:42 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
--- a/Resources/CMake/DatabasesPluginConfiguration.cmake Thu Jan 17 11:42:24 2019 +0100 +++ b/Resources/CMake/DatabasesPluginConfiguration.cmake Thu Jan 17 11:42:42 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 )
--- a/SQLite/CMakeLists.txt Thu Jan 17 11:42:24 2019 +0100 +++ b/SQLite/CMakeLists.txt Thu Jan 17 11:42:42 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
--- a/SQLite/Plugins/SQLiteIndex.cpp Thu Jan 17 11:42:24 2019 +0100 +++ b/SQLite/Plugins/SQLiteIndex.cpp Thu Jan 17 11:42:42 2019 +0100 @@ -21,6 +21,7 @@ #include "SQLiteIndex.h" +#include "../../Framework/Common/Integer64Value.h" #include "../../Framework/Plugins/GlobalProperties.h" #include "../../Framework/SQLite/SQLiteDatabase.h" #include "../../Framework/SQLite/SQLiteTransaction.h" @@ -173,4 +174,35 @@ return dynamic_cast<SQLiteDatabase&>(statement.GetDatabase()).GetLastInsertRowId(); } + + + int64_t SQLiteIndex::GetLastChangeIndex() + { + DatabaseManager::CachedStatement statement( + STATEMENT_FROM_HERE, GetManager(), + "SELECT seq FROM sqlite_sequence WHERE name='Changes'"); + + statement.SetReadOnly(true); + statement.Execute(); + + if (statement.IsDone()) + { + // No change has been recorded so far in the database + return 0; + } + else + { + const IValue& value = statement.GetResultField(0); + + switch (value.GetType()) + { + case ValueType_Integer64: + return dynamic_cast<const Integer64Value&>(value).GetValue(); + + default: + //LOG(ERROR) << value.Format(); + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + } + } }
--- a/SQLite/Plugins/SQLiteIndex.h Thu Jan 17 11:42:24 2019 +0100 +++ b/SQLite/Plugins/SQLiteIndex.h Thu Jan 17 11:42:42 2019 +0100 @@ -73,5 +73,8 @@ virtual int64_t CreateResource(const char* publicId, OrthancPluginResourceType type); + + // New primitive since Orthanc 1.5.2 + virtual int64_t GetLastChangeIndex(); }; }