Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package orthanc-postgresql for openSUSE:Factory checked in at 2025-03-04 18:31:37 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/orthanc-postgresql (Old) and /work/SRC/openSUSE:Factory/.orthanc-postgresql.new.19136 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "orthanc-postgresql" Tue Mar 4 18:31:37 2025 rev:13 rq:1249860 version:7.2 Changes: -------- --- /work/SRC/openSUSE:Factory/orthanc-postgresql/orthanc-postgresql.changes 2025-01-28 14:59:41.827342706 +0100 +++ /work/SRC/openSUSE:Factory/.orthanc-postgresql.new.19136/orthanc-postgresql.changes 2025-03-04 18:31:45.119863031 +0100 @@ -1,0 +2,16 @@ +Thu Feb 27 18:10:57 UTC 2025 - Axel Braun <axel.br...@gmx.de> + +- version 7.2 + * Optimized the SQL query that is maintaing the childCount + when new instances are ingested. + * New configuration "AllowInconsistentChildCounts" to speed + up childCount computation. If set to true, childCount values + of recently ingested resources will be incorrect until the next + execution of the DB Housekeeping thread (defined by "HousekeepingInterval" + - default value is 1s) + * Fixed high memory usage due to caching of too many + prepared SQL statement when using since & limit. + * Removed duplicate comparison in find SQL queries. + * Now returning results when e.g, ordering instances against a metadata they don't have. + +------------------------------------------------------------------- Old: ---- OrthancPostgreSQL-7.1.tar.gz New: ---- OrthancPostgreSQL-7.2.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ orthanc-postgresql.spec ++++++ --- /var/tmp/diff_new_pack.K5yC18/_old 2025-03-04 18:31:46.519921582 +0100 +++ /var/tmp/diff_new_pack.K5yC18/_new 2025-03-04 18:31:46.535922251 +0100 @@ -21,7 +21,7 @@ Summary: Database plugin for Orthanc License: AGPL-3.0-or-later Group: Productivity/Databases/Tools -Version: 7.1 +Version: 7.2 Release: 0 URL: https://orthanc-server.com Source0: https://orthanc.uclouvain.be/downloads/sources/%{name}/OrthancPostgreSQL-%{version}.tar.gz ++++++ OrthancPostgreSQL-7.1.tar.gz -> OrthancPostgreSQL-7.2.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OrthancPostgreSQL-7.1/.hg_archival.txt new/OrthancPostgreSQL-7.2/.hg_archival.txt --- old/OrthancPostgreSQL-7.1/.hg_archival.txt 2025-01-23 16:27:05.000000000 +0100 +++ new/OrthancPostgreSQL-7.2/.hg_archival.txt 2025-02-27 08:59:27.000000000 +0100 @@ -1,6 +1,6 @@ repo: 7cea966b682978aa285eb9b3a7a9cff81df464b3 -node: 197898aeeba7010d84df63593ad95156c7362d7c -branch: OrthancPostgreSQL-7.1 +node: ddc98844d931d753321da2a8086674ab4613de99 +branch: OrthancPostgreSQL-7.2 latesttag: null -latesttagdistance: 530 -changessincelatesttag: 591 +latesttagdistance: 542 +changessincelatesttag: 603 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OrthancPostgreSQL-7.1/Framework/Common/DatabaseManager.cpp new/OrthancPostgreSQL-7.2/Framework/Common/DatabaseManager.cpp --- old/OrthancPostgreSQL-7.1/Framework/Common/DatabaseManager.cpp 2025-01-23 16:27:05.000000000 +0100 +++ new/OrthancPostgreSQL-7.2/Framework/Common/DatabaseManager.cpp 2025-02-27 08:59:27.000000000 +0100 @@ -550,18 +550,35 @@ } } - DatabaseManager::CachedStatement::CachedStatement(const StatementId& statementId, DatabaseManager& manager, const std::string& sql) : StatementBase(manager), statementId_(statementId) { + Query::Parameters emptyParameters; + Setup(sql, emptyParameters); + } + + + DatabaseManager::CachedStatement::CachedStatement(const StatementId& statementId, + DatabaseManager& manager, + const std::string& sql, + const Query::Parameters& parametersTypes) : + StatementBase(manager), + statementId_(statementId) + { + Setup(sql, parametersTypes); + } + + void DatabaseManager::CachedStatement::Setup(const std::string& sql, + const Query::Parameters& parametersTypes) + { statement_ = GetManager().LookupCachedStatement(statementId_); if (statement_ == NULL) { - SetQuery(new Query(sql)); + SetQuery(new Query(sql, parametersTypes)); } else { @@ -628,7 +645,17 @@ const std::string& sql) : StatementBase(manager) { - SetQuery(new Query(sql)); + Query::Parameters emptyParameters; + SetQuery(new Query(sql, emptyParameters)); + } + + + DatabaseManager::StandaloneStatement::StandaloneStatement(DatabaseManager& manager, + const std::string& sql, + const Query::Parameters& parametersTypes) : + StatementBase(manager) + { + SetQuery(new Query(sql, parametersTypes)); } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OrthancPostgreSQL-7.1/Framework/Common/DatabaseManager.h new/OrthancPostgreSQL-7.2/Framework/Common/DatabaseManager.h --- old/OrthancPostgreSQL-7.1/Framework/Common/DatabaseManager.h 2025-01-23 16:27:05.000000000 +0100 +++ new/OrthancPostgreSQL-7.2/Framework/Common/DatabaseManager.h 2025-02-27 08:59:27.000000000 +0100 @@ -212,11 +212,19 @@ StatementId statementId_; IPrecompiledStatement* statement_; + void Setup(const std::string& sql, + const Query::Parameters& parametersTypes); + public: CachedStatement(const StatementId& statementId, DatabaseManager& manager, const std::string& sql); + CachedStatement(const StatementId& statementId, + DatabaseManager& manager, + const std::string& sql, + const Query::Parameters& parametersTypes); + void Execute() { Dictionary parameters; @@ -247,6 +255,10 @@ StandaloneStatement(DatabaseManager& manager, const std::string& sql); + StandaloneStatement(DatabaseManager& manager, + const std::string& sql, + const Query::Parameters& parametersTypes); + virtual ~StandaloneStatement(); void Execute() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OrthancPostgreSQL-7.1/Framework/Common/Dictionary.cpp new/OrthancPostgreSQL-7.2/Framework/Common/Dictionary.cpp --- old/OrthancPostgreSQL-7.1/Framework/Common/Dictionary.cpp 2025-01-23 16:27:05.000000000 +0100 +++ new/OrthancPostgreSQL-7.2/Framework/Common/Dictionary.cpp 2025-02-27 08:59:27.000000000 +0100 @@ -155,4 +155,15 @@ return *found->second; } } + + void Dictionary::GetParametersType(Query::Parameters& target) const + { + target.clear(); + + for (Values::const_iterator it = values_.begin(); + it != values_.end(); ++it) + { + target[it->first] = it->second->GetType(); + } + } } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OrthancPostgreSQL-7.1/Framework/Common/Dictionary.h new/OrthancPostgreSQL-7.2/Framework/Common/Dictionary.h --- old/OrthancPostgreSQL-7.1/Framework/Common/Dictionary.h 2025-01-23 16:27:05.000000000 +0100 +++ new/OrthancPostgreSQL-7.2/Framework/Common/Dictionary.h 2025-02-27 08:59:27.000000000 +0100 @@ -27,6 +27,7 @@ #include <map> #include <stdint.h> +#include "Query.h" namespace OrthancDatabases { @@ -74,5 +75,7 @@ void SetNullValue(const std::string& key); const IValue& GetValue(const std::string& key) const; + + void GetParametersType(Query::Parameters& target) const; }; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OrthancPostgreSQL-7.1/Framework/Common/Query.cpp new/OrthancPostgreSQL-7.2/Framework/Common/Query.cpp --- old/OrthancPostgreSQL-7.1/Framework/Common/Query.cpp 2025-01-23 16:27:05.000000000 +0100 +++ new/OrthancPostgreSQL-7.2/Framework/Common/Query.cpp 2025-02-27 08:59:27.000000000 +0100 @@ -56,7 +56,7 @@ }; - void Query::Setup(const std::string& sql) + void Query::Setup(const std::string& sql, const Query::Parameters& parametersTypes) { boost::regex regex("\\$\\{(.*?)\\}"); @@ -76,7 +76,17 @@ parameter = parameter.substr(2, parameter.size() - 3); tokens_.push_back(new Token(true, parameter)); - parameters_[parameter] = ValueType_Utf8String; + + // when we already know the type of the parameter, set it directly to the right value + Query::Parameters::const_iterator itp = parametersTypes.find(parameter); + if (itp != parametersTypes.end()) + { + parameters_[parameter] = itp->second; + } + else + { + parameters_[parameter] = ValueType_Utf8String; + } last = it->second; @@ -93,7 +103,15 @@ Query::Query(const std::string& sql) : readOnly_(false) { - Setup(sql); + Query::Parameters emptyParameters; + Setup(sql, emptyParameters); + } + + Query::Query(const std::string& sql, + const Query::Parameters& parametersTypes) : + readOnly_(false) + { + Setup(sql, parametersTypes); } @@ -101,7 +119,16 @@ bool readOnly) : readOnly_(readOnly) { - Setup(sql); + Query::Parameters emptyParameters; + Setup(sql, emptyParameters); + } + + Query::Query(const std::string& sql, + bool readOnly, + const Query::Parameters& parametersTypes) : + readOnly_(readOnly) + { + Setup(sql, parametersTypes); } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OrthancPostgreSQL-7.1/Framework/Common/Query.h new/OrthancPostgreSQL-7.2/Framework/Common/Query.h --- old/OrthancPostgreSQL-7.1/Framework/Common/Query.h 2025-01-23 16:27:05.000000000 +0100 +++ new/OrthancPostgreSQL-7.2/Framework/Common/Query.h 2025-02-27 08:59:27.000000000 +0100 @@ -48,8 +48,8 @@ ValueType type) = 0; }; - private: typedef std::map<std::string, ValueType> Parameters; + private: class Token; @@ -57,14 +57,19 @@ Parameters parameters_; bool readOnly_; - void Setup(const std::string& sql); + void Setup(const std::string& sql, const Query::Parameters& parametersTypes); public: explicit Query(const std::string& sql); + explicit Query(const std::string& sql, const Parameters& parametersType); Query(const std::string& sql, bool isReadOnly); + Query(const std::string& sql, + bool isReadOnly, + const Parameters& parametersType); + ~Query(); bool IsReadOnly() const diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OrthancPostgreSQL-7.1/Framework/Plugins/ISqlLookupFormatter.cpp new/OrthancPostgreSQL-7.2/Framework/Plugins/ISqlLookupFormatter.cpp --- old/OrthancPostgreSQL-7.1/Framework/Plugins/ISqlLookupFormatter.cpp 2025-01-23 16:27:05.000000000 +0100 +++ new/OrthancPostgreSQL-7.2/Framework/Plugins/ISqlLookupFormatter.cpp 2025-02-27 08:59:27.000000000 +0100 @@ -473,7 +473,7 @@ { std::string arg = "order" + boost::lexical_cast<std::string>(index); - target = " INNER JOIN Metadata " + arg + " ON " + arg + ".id = " + FormatLevel(requestLevel) + + target = " LEFT JOIN Metadata " + arg + " ON " + arg + ".id = " + FormatLevel(requestLevel) + ".internalId AND " + arg + ".type = " + boost::lexical_cast<std::string>(metadata); } @@ -1039,11 +1039,6 @@ } if (!comparison.empty()) - { - comparisons += " AND " + comparison; - } - - if (!comparison.empty()) { comparisons += " AND " + comparison; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OrthancPostgreSQL-7.1/Framework/Plugins/ISqlLookupFormatter.h new/OrthancPostgreSQL-7.2/Framework/Plugins/ISqlLookupFormatter.h --- old/OrthancPostgreSQL-7.1/Framework/Plugins/ISqlLookupFormatter.h 2025-01-23 16:27:05.000000000 +0100 +++ new/OrthancPostgreSQL-7.2/Framework/Plugins/ISqlLookupFormatter.h 2025-02-27 08:59:27.000000000 +0100 @@ -56,6 +56,8 @@ virtual std::string GenerateParameter(const std::string& value) = 0; + virtual std::string GenerateParameter(const int64_t& value) = 0; + virtual std::string FormatResourceType(Orthanc::ResourceType level) = 0; virtual std::string FormatWildcardEscape() = 0; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OrthancPostgreSQL-7.1/Framework/Plugins/IndexBackend.cpp new/OrthancPostgreSQL-7.2/Framework/Plugins/IndexBackend.cpp --- old/OrthancPostgreSQL-7.1/Framework/Plugins/IndexBackend.cpp 2025-01-23 16:27:05.000000000 +0100 +++ new/OrthancPostgreSQL-7.2/Framework/Plugins/IndexBackend.cpp 2025-02-27 08:59:27.000000000 +0100 @@ -312,9 +312,11 @@ IndexBackend::IndexBackend(OrthancPluginContext* context, - bool readOnly) : + bool readOnly, + bool allowInconsistentChildCounts) : context_(context), - readOnly_(readOnly) + readOnly_(readOnly), + allowInconsistentChildCounts_(allowInconsistentChildCounts) { } @@ -2180,6 +2182,16 @@ return "${" + key + "}"; } + virtual std::string GenerateParameter(const int64_t& value) + { + const std::string key = FormatParameter(count_); + + count_ ++; + dictionary_.SetIntegerValue(key, value); + + return "${" + key + "}"; + } + virtual std::string FormatResourceType(Orthanc::ResourceType level) { return boost::lexical_cast<std::string>(MessagesToolbox::ConvertToPlainC(level)); @@ -2229,11 +2241,13 @@ { if (count > 0 || since > 0) { - sql += " OFFSET " + boost::lexical_cast<std::string>(since) + " ROWS "; + std::string parameterSince = GenerateParameter(since); + sql += " OFFSET " + parameterSince + " ROWS "; } if (count > 0) { - sql += " FETCH NEXT " + boost::lexical_cast<std::string>(count) + " ROWS ONLY "; + std::string parameterCount = GenerateParameter(count); + sql += " FETCH NEXT " + parameterCount + " ROWS ONLY "; } }; break; case Dialect_SQLite: @@ -2241,26 +2255,32 @@ { if (count > 0) { - sql += " LIMIT " + boost::lexical_cast<std::string>(count); + std::string parameterCount = GenerateParameter(count); + sql += " LIMIT " + parameterCount; } if (since > 0) { - sql += " OFFSET " + boost::lexical_cast<std::string>(since); + std::string parameterSince = GenerateParameter(since); + sql += " OFFSET " + parameterSince; } }; break; case Dialect_MySQL: { if (count > 0 && since > 0) { - sql += " LIMIT " + boost::lexical_cast<std::string>(since) + ", " + boost::lexical_cast<std::string>(count); + std::string parameterCount = GenerateParameter(count); + std::string parameterSince = GenerateParameter(since); + sql += " LIMIT " + parameterSince + ", " + parameterCount; } else if (count > 0) { - sql += " LIMIT " + boost::lexical_cast<std::string>(count); + std::string parameterCount = GenerateParameter(count); + sql += " LIMIT " + parameterCount; } else if (since > 0) { - sql += " LIMIT " + boost::lexical_cast<std::string>(since) + ", 18446744073709551615"; // max uint64 value when you don't want any limit + std::string parameterSince = GenerateParameter(since); + sql += " LIMIT " + parameterSince + ", 18446744073709551615"; // max uint64 value when you don't want any limit } }; break; default: @@ -2314,15 +2334,6 @@ } } - 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 { @@ -2439,9 +2450,10 @@ } } - DatabaseManager::StandaloneStatement statement(manager, sql); - formatter.PrepareStatement(statement); + Query::Parameters parametersTypes; + formatter.GetDictionary().GetParametersType(parametersTypes); + DatabaseManager::StandaloneStatement statement(manager, sql, parametersTypes); statement.Execute(formatter.GetDictionary()); while (!statement.IsDone()) @@ -3286,7 +3298,10 @@ sql = "WITH Lookup AS (" + lookupSql + ") SELECT COUNT(*) FROM Lookup"; - DatabaseManager::CachedStatement statement(STATEMENT_FROM_HERE_DYNAMIC(sql), manager, sql); + Query::Parameters parametersTypes; + formatter.GetDictionary().GetParametersType(parametersTypes); + + DatabaseManager::CachedStatement statement(STATEMENT_FROM_HERE_DYNAMIC(sql), manager, sql, parametersTypes); statement.Execute(formatter.GetDictionary()); response.mutable_count_resources()->set_count(statement.ReadInteger64(0)); } @@ -3303,6 +3318,29 @@ // why we have generic column names. // So, at the end we'll have only one very big query ! + const Orthanc::DatabasePluginMessages::Find_Request_ChildrenSpecification* childrenSpec = NULL; + const Orthanc::DatabasePluginMessages::Find_Request_ChildrenSpecification* grandchildrenSpec = NULL; + const Orthanc::DatabasePluginMessages::Find_Request_ChildrenSpecification* grandgrandchildrenSpec = NULL; + + switch (request.level()) + { + case Orthanc::DatabasePluginMessages::RESOURCE_PATIENT: + childrenSpec = &(request.children_studies()); + grandchildrenSpec = &(request.children_series()); + grandgrandchildrenSpec = &(request.children_instances()); + break; + case Orthanc::DatabasePluginMessages::RESOURCE_STUDY: + childrenSpec = &(request.children_series()); + grandchildrenSpec = &(request.children_instances()); + break; + case Orthanc::DatabasePluginMessages::RESOURCE_SERIES: + childrenSpec = &(request.children_instances()); + break; + + default: + break; + } + std::string sql; // extract the resource id of interest by executing the lookup in a CTE @@ -3315,6 +3353,7 @@ std::string oneInstanceSqlCTE; + // OneInstance CTE if (request.level() != Orthanc::DatabasePluginMessages::RESOURCE_INSTANCE && request.retrieve_one_instance_metadata_and_attachments()) { @@ -3348,10 +3387,47 @@ sql += ", OneInstance AS (SELECT parentInternalId, instancePublicId, instanceInternalId FROM _OneInstance WHERE rowNum = 1) "; // this is a generic way to implement DISTINCT ON } - // if (!oneInstanceSqlCTE.empty() && (manager.GetDialect() == Dialect_MySQL || manager.GetDialect() == Dialect_SQLite)) - // { // all CTEs must be declared first in some dialects - // } - + // GrandChildCount CTE in some specific cases + bool hasGrandChildCountCTE = false; + if (HasChildCountColumn() && !allowInconsistentChildCounts_ && + grandchildrenSpec != NULL && + !grandchildrenSpec->retrieve_identifiers() && // no need to count if we have retrieved the list of identifiers + grandchildrenSpec->retrieve_count()) + { + sql += ", ValidGrandChildCounts AS (SELECT thisLevel.internalId, COALESCE(SUM(childLevel.childCount), 0) AS totalGrandChildCount " + " FROM Resources AS thisLevel " + " LEFT JOIN Resources AS childLevel ON childLevel.parentId = thisLevel.internalId " + " WHERE NOT EXISTS (SELECT 1 FROM InvalidChildCounts WHERE id = thisLevel.internalId) " + " AND NOT EXISTS (SELECT 1 FROM Resources AS child " + " JOIN InvalidChildCounts ON InvalidChildCounts.id = child.internalId " + " WHERE child.parentId = thisLevel.internalId) " + " GROUP BY thisLevel.internalId) "; + hasGrandChildCountCTE = true; + } + + // GrandGrandChildCount CTE in some specific cases + bool hasGrandGrandChildCountCTE = false; + if (HasChildCountColumn() && !allowInconsistentChildCounts_ && + grandgrandchildrenSpec != NULL && + !grandgrandchildrenSpec->retrieve_identifiers() && // no need to count if we have retrieved the list of identifiers + grandgrandchildrenSpec->retrieve_count()) + { + sql += ", ValidGrandGrandChildCounts AS (SELECT thisLevel.internalId, COALESCE(SUM(grandChildLevel.childCount), 0) AS totalGrandGrandChildCount " + " FROM Resources AS thisLevel " + " LEFT JOIN Resources AS childLevel ON childLevel.parentId = thisLevel.internalId " + " LEFT JOIN Resources AS grandChildLevel ON grandChildLevel.parentId = childLevel.internalId " + " WHERE NOT EXISTS (SELECT 1 FROM InvalidChildCounts WHERE id = thisLevel.internalId) " + " AND NOT EXISTS (SELECT 1 FROM Resources AS child " + " JOIN InvalidChildCounts ON InvalidChildCounts.id = child.internalId " + " WHERE child.parentId = thisLevel.internalId) " + " AND NOT EXISTS (SELECT 1 FROM Resources AS grandChild " + " JOIN Resources AS child ON grandChild.parentId = child.internalId " + " JOIN InvalidChildCounts ON InvalidChildCounts.id = grandChild.internalId " + " WHERE child.parentId = thisLevel.internalId) " + " GROUP BY thisLevel.internalId) "; + hasGrandGrandChildCountCTE = true; + } + std::string revisionInC7; if (HasRevisionsSupport()) { @@ -3573,22 +3649,7 @@ // need MainDicomTags from children ? if (request.level() <= Orthanc::DatabasePluginMessages::RESOURCE_SERIES) { - const Orthanc::DatabasePluginMessages::Find_Request_ChildrenSpecification* childrenSpec = NULL; - switch (request.level()) - { - case Orthanc::DatabasePluginMessages::RESOURCE_PATIENT: - childrenSpec = &(request.children_studies()); - break; - case Orthanc::DatabasePluginMessages::RESOURCE_STUDY: - childrenSpec = &(request.children_series()); - break; - case Orthanc::DatabasePluginMessages::RESOURCE_SERIES: - childrenSpec = &(request.children_instances()); - break; - - default: - break; - } + assert(childrenSpec != NULL); if (childrenSpec->retrieve_main_dicom_tags_size() > 0) { @@ -3629,30 +3690,24 @@ } else if (childrenSpec->retrieve_count()) // no need to count if we have retrieved the list of identifiers { - if (HasChildCountTable()) // TODO: rename in HasChildCountColumn ? + if (HasChildCountColumn()) { - // // we get the count value either from the childCount table if it has been computed or from the Resources table - // sql += "UNION ALL SELECT " - // " " TOSTRING(QUERY_CHILDREN_COUNT) " AS c0_queryId, " - // " Lookup.internalId AS c1_internalId, " - // " " + formatter.FormatNull("BIGINT") + " AS c2_rowNumber, " - // " " + formatter.FormatNull("TEXT") + " AS c3_string1, " - // " " + formatter.FormatNull("TEXT") + " AS c4_string2, " - // " " + formatter.FormatNull("TEXT") + " AS c5_string3, " - // " " + formatter.FormatNull("INT") + " AS c6_int1, " - // " " + formatter.FormatNull("INT") + " AS c7_int2, " - // " " + formatter.FormatNull("INT") + " AS c8_int3, " - // " COALESCE(" - // " (ChildCount.childCount)," - // " (SELECT COUNT(childLevel.internalId)" - // " FROM Resources AS childLevel" - // " WHERE Lookup.internalId = childLevel.parentId" - // " )) AS c9_big_int1, " - // " " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 " - // "FROM Lookup " - // "LEFT JOIN ChildCount ON Lookup.internalId = ChildCount.parentId "; - // we get the count value either from the childCount column if it has been computed or from the Resources table + std::string getResourcesChildCount; + + if (allowInconsistentChildCounts_) // allow approximate childCount if childCount is populated but has not yet been updated + { + getResourcesChildCount = "Resources.childCount"; // read the last known value without taking into account the InvalidChildCounts table to speed up the query + } + else + { + // read the last known value only if it has not been invalidated by an entry in the InvalidChildCounts table -> more complex/slower query + getResourcesChildCount = "SELECT childCount " + "FROM Resources AS thisLevel " + "WHERE thisLevel.internalId = lookup.internalId " + " AND NOT EXISTS (SELECT 1 FROM Invalidchildcounts WHERE id = thisLevel.internalId)"; + } + sql += "UNION ALL SELECT " " " TOSTRING(QUERY_CHILDREN_COUNT) " AS c0_queryId, " " Lookup.internalId AS c1_internalId, " @@ -3664,7 +3719,7 @@ " " + formatter.FormatNull("INT") + " AS c7_int2, " " " + formatter.FormatNull("INT") + " AS c8_int3, " " COALESCE(" - " (Resources.childCount)," + " (" + getResourcesChildCount + ")," " (SELECT COUNT(childLevel.internalId)" " FROM Resources AS childLevel" " WHERE Lookup.internalId = childLevel.parentId" @@ -3713,19 +3768,7 @@ if (request.level() <= Orthanc::DatabasePluginMessages::RESOURCE_STUDY) { - const Orthanc::DatabasePluginMessages::Find_Request_ChildrenSpecification* grandchildrenSpec = NULL; - switch (request.level()) - { - case Orthanc::DatabasePluginMessages::RESOURCE_PATIENT: - grandchildrenSpec = &(request.children_series()); - break; - case Orthanc::DatabasePluginMessages::RESOURCE_STUDY: - grandchildrenSpec = &(request.children_instances()); - break; - - default: - break; - } + assert(grandchildrenSpec != NULL); // need grand children identifiers ? if (grandchildrenSpec->retrieve_identifiers()) @@ -3748,54 +3791,48 @@ } else if (grandchildrenSpec->retrieve_count()) // no need to count if we have retrieved the list of identifiers { - if (HasChildCountTable()) + if (HasChildCountColumn()) { - // // we get the count value either from the childCount table if it has been computed or from the Resources table - // sql += "UNION ALL SELECT " - // " " TOSTRING(QUERY_GRAND_CHILDREN_COUNT) " AS c0_queryId, " - // " Lookup.internalId AS c1_internalId, " - // " " + formatter.FormatNull("BIGINT") + " AS c2_rowNumber, " - // " " + formatter.FormatNull("TEXT") + " AS c3_string1, " - // " " + formatter.FormatNull("TEXT") + " AS c4_string2, " - // " " + formatter.FormatNull("TEXT") + " AS c5_string3, " - // " " + formatter.FormatNull("INT") + " AS c6_int1, " - // " " + formatter.FormatNull("INT") + " AS c7_int2, " - // " " + formatter.FormatNull("INT") + " AS c8_int3, " - // " COALESCE(" - // " (SELECT SUM(ChildCount.childCount)" - // " FROM ChildCount" - // " INNER JOIN Resources AS childLevel ON childLevel.parentId = Lookup.internalId" - // " WHERE ChildCount.parentId = childLevel.internalId)," - // " (SELECT COUNT(grandChildLevel.internalId)" - // " FROM Resources AS childLevel" - // " INNER JOIN Resources AS grandChildLevel ON childLevel.internalId = grandChildLevel.parentId" - // " WHERE Lookup.internalId = childLevel.parentId" - // " )) AS c9_big_int1, " - // " " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 " - // "FROM Lookup "; + // we get the count value either from the childCount column if it has been computed or from the Resources table + std::string getResourcesGrandChildCount; + + if (allowInconsistentChildCounts_) // allow approximate childCount if childCount is populated but has not yet been updated + { + // read the last known value without taking into account the InvalidChildCounts table to speed up the query + getResourcesGrandChildCount = "SELECT SUM(childLevel.childCount) " + "FROM Resources AS childLevel " + "WHERE childLevel.parentId = Lookup.internalId"; + } + else + { + assert(hasGrandChildCountCTE); + // read the last known value only if it has not been invalidated by an entry in the InvalidChildCounts table -> more complex/slower query + getResourcesGrandChildCount = "SELECT totalGrandChildCount " + "FROM ValidGrandChildCounts " + "WHERE internalId = lookup.internalId"; + //" AND NOT EXISTS (SELECT 1 FROM Invalidchildcounts WHERE id = childLevel.internalId OR id = lookup.internalId)"; + } // we get the count value either from the childCount column if it has been computed or from the Resources table sql += "UNION ALL SELECT " - " " TOSTRING(QUERY_GRAND_CHILDREN_COUNT) " AS c0_queryId, " - " Lookup.internalId AS c1_internalId, " - " " + formatter.FormatNull("BIGINT") + " AS c2_rowNumber, " - " " + formatter.FormatNull("TEXT") + " AS c3_string1, " - " " + formatter.FormatNull("TEXT") + " AS c4_string2, " - " " + formatter.FormatNull("TEXT") + " AS c5_string3, " - " " + formatter.FormatNull("INT") + " AS c6_int1, " - " " + formatter.FormatNull("INT") + " AS c7_int2, " - " " + formatter.FormatNull("INT") + " AS c8_int3, " - " COALESCE(" - " (SELECT SUM(childLevel.childCount)" - " FROM Resources AS childLevel" - " WHERE childLevel.parentId = Lookup.internalId)," - " (SELECT COUNT(grandChildLevel.internalId)" - " FROM Resources AS childLevel" - " INNER JOIN Resources AS grandChildLevel ON childLevel.internalId = grandChildLevel.parentId" - " WHERE Lookup.internalId = childLevel.parentId" - " )) AS c9_big_int1, " - " " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 " - "FROM Lookup "; + " " TOSTRING(QUERY_GRAND_CHILDREN_COUNT) " AS c0_queryId, " + " Lookup.internalId AS c1_internalId, " + " " + formatter.FormatNull("BIGINT") + " AS c2_rowNumber, " + " " + formatter.FormatNull("TEXT") + " AS c3_string1, " + " " + formatter.FormatNull("TEXT") + " AS c4_string2, " + " " + formatter.FormatNull("TEXT") + " AS c5_string3, " + " " + formatter.FormatNull("INT") + " AS c6_int1, " + " " + formatter.FormatNull("INT") + " AS c7_int2, " + " " + formatter.FormatNull("INT") + " AS c8_int3, " + " COALESCE(" + " (" + getResourcesGrandChildCount + ")," + " (SELECT COUNT(grandChildLevel.internalId)" + " FROM Resources AS childLevel" + " INNER JOIN Resources AS grandChildLevel ON childLevel.internalId = grandChildLevel.parentId" + " WHERE Lookup.internalId = childLevel.parentId" + " )) AS c9_big_int1, " + " " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 " + "FROM Lookup "; } else { @@ -3859,7 +3896,7 @@ if (request.level() == Orthanc::DatabasePluginMessages::RESOURCE_PATIENT) { - const Orthanc::DatabasePluginMessages::Find_Request_ChildrenSpecification* grandgrandchildrenSpec = &(request.children_instances()); + assert(grandgrandchildrenSpec != NULL); // need grand children identifiers ? if (grandgrandchildrenSpec->retrieve_identifiers()) @@ -3883,33 +3920,29 @@ } else if (grandgrandchildrenSpec->retrieve_count()) // no need to count if we have retrieved the list of identifiers { - if (HasChildCountTable()) + if (HasChildCountColumn()) { - // // we get the count value either from the childCount table if it has been computed or from the Resources table - // sql += "UNION ALL SELECT " - // " " TOSTRING(QUERY_GRAND_GRAND_CHILDREN_COUNT) " AS c0_queryId, " - // " Lookup.internalId AS c1_internalId, " - // " " + formatter.FormatNull("BIGINT") + " AS c2_rowNumber, " - // " " + formatter.FormatNull("TEXT") + " AS c3_string1, " - // " " + formatter.FormatNull("TEXT") + " AS c4_string2, " - // " " + formatter.FormatNull("TEXT") + " AS c5_string3, " - // " " + formatter.FormatNull("INT") + " AS c6_int1, " - // " " + formatter.FormatNull("INT") + " AS c7_int2, " - // " " + formatter.FormatNull("INT") + " AS c8_int3, " - // " COALESCE(" - // " (SELECT SUM(ChildCount.childCount)" - // " FROM ChildCount" - // " INNER JOIN Resources AS childLevel ON childLevel.parentId = Lookup.internalId" - // " INNER JOIN Resources AS grandChildLevel ON grandChildLevel.parentId = childLevel.internalId" - // " WHERE ChildCount.parentId = grandChildLevel.internalId)," - // " (SELECT COUNT(grandGrandChildLevel.internalId)" - // " FROM Resources AS childLevel" - // " INNER JOIN Resources AS grandChildLevel ON childLevel.internalId = grandChildLevel.parentId" - // " INNER JOIN Resources AS grandGrandChildLevel ON grandChildLevel.internalId = grandGrandChildLevel.parentId" - // " WHERE Lookup.internalId = childLevel.parentId" - // " )) AS c9_big_int1, " - // " " + formatter.FormatNull("BIGINT") + " AS c10_big_int2 " - // "FROM Lookup "; + std::string getResourcesGrandGrandChildCount; + if (allowInconsistentChildCounts_) // allow approximate childCount if childCount is populated but has not yet been updated + { + // read the last known value without taking into account the InvalidChildCounts table to speed up the query + getResourcesGrandGrandChildCount = "SELECT SUM(grandChildLevel.childCount)" + "FROM Resources AS grandChildLevel" + "INNER JOIN Resources AS childLevel ON childLevel.parentId = Lookup.internalId" + "WHERE grandChildLevel.parentId = childLevel.internalId"; + } + else + { + assert(hasGrandGrandChildCountCTE); + // read the last known value only if it has not been invalidated by an entry in the InvalidChildCounts table -> more complex/slower query + getResourcesGrandGrandChildCount = "SELECT totalGrandGrandChildCount " + "FROM ValidGrandGrandChildCounts " + "WHERE internalId = lookup.internalId"; + // "INNER JOIN Resources AS childLevel ON childLevel.parentId = Lookup.internalId" + // "WHERE grandChildLevel.parentId = childLevel.internalId" + // " AND NOT EXISTS (SELECT 1 FROM Invalidchildcounts WHERE id = grandChildLevel.internalId OR id = childLevel.internalId OR id = lookup.internalId)"; + } + // we get the count value either from the childCount column if it has been computed or from the Resources table sql += "UNION ALL SELECT " @@ -3923,10 +3956,7 @@ " " + formatter.FormatNull("INT") + " AS c7_int2, " " " + formatter.FormatNull("INT") + " AS c8_int3, " " COALESCE(" - " (SELECT SUM(grandChildLevel.childCount)" - " FROM Resources AS grandChildLevel" - " INNER JOIN Resources AS childLevel ON childLevel.parentId = Lookup.internalId" - " WHERE grandChildLevel.parentId = childLevel.internalId)," + " (" + getResourcesGrandGrandChildCount + ")," " (SELECT COUNT(grandGrandChildLevel.internalId)" " FROM Resources AS childLevel" " INNER JOIN Resources AS grandChildLevel ON childLevel.internalId = grandChildLevel.parentId" @@ -4035,13 +4065,16 @@ sql += " ORDER BY c0_queryId, c2_rowNumber"; // this is really important to make sure that the Lookup query is the first one to provide results since we use it to create the responses element ! std::unique_ptr<DatabaseManager::StatementBase> statement; + + Query::Parameters parametersTypes; + formatter.GetDictionary().GetParametersType(parametersTypes); if (manager.GetDialect() == Dialect_MySQL) { // TODO: investigate why "complex" cached statement do not seem to work properly in MySQL - statement.reset(new DatabaseManager::StandaloneStatement(manager, sql)); + statement.reset(new DatabaseManager::StandaloneStatement(manager, sql, parametersTypes)); } else { - statement.reset(new DatabaseManager::CachedStatement(STATEMENT_FROM_HERE_DYNAMIC(sql), manager, sql)); + statement.reset(new DatabaseManager::CachedStatement(STATEMENT_FROM_HERE_DYNAMIC(sql), manager, sql, parametersTypes)); } statement->Execute(formatter.GetDictionary()); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OrthancPostgreSQL-7.1/Framework/Plugins/IndexBackend.h new/OrthancPostgreSQL-7.2/Framework/Plugins/IndexBackend.h --- old/OrthancPostgreSQL-7.1/Framework/Plugins/IndexBackend.h 2025-01-23 16:27:05.000000000 +0100 +++ new/OrthancPostgreSQL-7.2/Framework/Plugins/IndexBackend.h 2025-02-27 08:59:27.000000000 +0100 @@ -43,18 +43,20 @@ OrthancPluginContext* context_; bool readOnly_; + bool allowInconsistentChildCounts_; boost::shared_mutex outputFactoryMutex_; std::unique_ptr<IDatabaseBackendOutput::IFactory> outputFactory_; protected: + virtual void ClearDeletedFiles(DatabaseManager& manager); virtual void ClearDeletedResources(DatabaseManager& manager); virtual void ClearRemainingAncestor(DatabaseManager& manager); - virtual bool HasChildCountTable() const = 0; + virtual bool HasChildCountColumn() const = 0; void SignalDeletedFiles(IDatabaseBackendOutput& output, DatabaseManager& manager); @@ -84,7 +86,8 @@ public: explicit IndexBackend(OrthancPluginContext* context, - bool readOnly); + bool readOnly, + bool allowInconsistentChildCounts); virtual OrthancPluginContext* GetContext() ORTHANC_OVERRIDE { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OrthancPostgreSQL-7.1/Framework/Plugins/IndexConnectionsPool.cpp new/OrthancPostgreSQL-7.2/Framework/Plugins/IndexConnectionsPool.cpp --- old/OrthancPostgreSQL-7.1/Framework/Plugins/IndexConnectionsPool.cpp 2025-01-23 16:27:05.000000000 +0100 +++ new/OrthancPostgreSQL-7.2/Framework/Plugins/IndexConnectionsPool.cpp 2025-02-27 08:59:27.000000000 +0100 @@ -49,6 +49,10 @@ void IndexConnectionsPool::HousekeepingThread(IndexConnectionsPool* that) { +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 2) + OrthancPluginSetCurrentThreadName(OrthancPlugins::GetGlobalContext(), "DB HOUSEKEEPING"); +#endif + boost::posix_time::ptime lastInvocation = boost::posix_time::second_clock::local_time(); while (that->housekeepingContinue_) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OrthancPostgreSQL-7.1/Framework/PostgreSQL/PostgreSQLParameters.cpp new/OrthancPostgreSQL-7.2/Framework/PostgreSQL/PostgreSQLParameters.cpp --- old/OrthancPostgreSQL-7.1/Framework/PostgreSQL/PostgreSQLParameters.cpp 2025-01-23 16:27:05.000000000 +0100 +++ new/OrthancPostgreSQL-7.2/Framework/PostgreSQL/PostgreSQLParameters.cpp 2025-02-27 08:59:27.000000000 +0100 @@ -98,6 +98,7 @@ lock_ = configuration.GetBooleanValue("Lock", true); // Use locking by default isVerboseEnabled_ = configuration.GetBooleanValue("EnableVerboseLogs", false); + allowInconsistentChildCounts_ = configuration.GetBooleanValue("AllowInconsistentChildCounts", false); maxConnectionRetries_ = configuration.GetUnsignedIntegerValue("MaximumConnectionRetries", 10); connectionRetryInterval_ = configuration.GetUnsignedIntegerValue("ConnectionRetryInterval", 5); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OrthancPostgreSQL-7.1/Framework/PostgreSQL/PostgreSQLParameters.h new/OrthancPostgreSQL-7.2/Framework/PostgreSQL/PostgreSQLParameters.h --- old/OrthancPostgreSQL-7.1/Framework/PostgreSQL/PostgreSQLParameters.h 2025-01-23 16:27:05.000000000 +0100 +++ new/OrthancPostgreSQL-7.2/Framework/PostgreSQL/PostgreSQLParameters.h 2025-02-27 08:59:27.000000000 +0100 @@ -51,6 +51,7 @@ unsigned int maxConnectionRetries_; unsigned int connectionRetryInterval_; bool isVerboseEnabled_; + bool allowInconsistentChildCounts_; IsolationMode isolationMode_; void Reset(); @@ -152,6 +153,10 @@ return isVerboseEnabled_; } + bool GetAllowInconsistentChildCounts() const + { + return allowInconsistentChildCounts_; + } void Format(std::string& target) const; }; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OrthancPostgreSQL-7.1/PostgreSQL/CMakeLists.txt new/OrthancPostgreSQL-7.2/PostgreSQL/CMakeLists.txt --- old/OrthancPostgreSQL-7.1/PostgreSQL/CMakeLists.txt 2025-01-23 16:27:05.000000000 +0100 +++ new/OrthancPostgreSQL-7.2/PostgreSQL/CMakeLists.txt 2025-02-27 08:59:27.000000000 +0100 @@ -22,7 +22,7 @@ cmake_minimum_required(VERSION 2.8) project(OrthancPostgreSQL) -set(ORTHANC_PLUGIN_VERSION "7.1") +set(ORTHANC_PLUGIN_VERSION "7.2") # This is the preferred version of the Orthanc SDK for this plugin set(ORTHANC_SDK_DEFAULT_VERSION "1.12.5") @@ -93,6 +93,7 @@ POSTGRESQL_UPGRADE_UNKNOWN_TO_REV1 ${CMAKE_SOURCE_DIR}/Plugins/SQL/Upgrades/UnknownToRev1.sql POSTGRESQL_UPGRADE_REV1_TO_REV2 ${CMAKE_SOURCE_DIR}/Plugins/SQL/Upgrades/Rev1ToRev2.sql POSTGRESQL_UPGRADE_REV2_TO_REV3 ${CMAKE_SOURCE_DIR}/Plugins/SQL/Upgrades/Rev2ToRev3.sql + POSTGRESQL_UPGRADE_REV3_TO_REV4 ${CMAKE_SOURCE_DIR}/Plugins/SQL/Upgrades/Rev3ToRev4.sql ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OrthancPostgreSQL-7.1/PostgreSQL/NEWS new/OrthancPostgreSQL-7.2/PostgreSQL/NEWS --- old/OrthancPostgreSQL-7.1/PostgreSQL/NEWS 2025-01-23 16:27:05.000000000 +0100 +++ new/OrthancPostgreSQL-7.2/PostgreSQL/NEWS 2025-02-27 08:59:27.000000000 +0100 @@ -1,3 +1,31 @@ +Release 7.2 (2025-02-27) +======================== + +DB schema revision: 4 +Minimum plugin SDK (for build): 1.12.5 +Optimal plugin SDK (for build): 1.12.5 +Minimum Orthanc runtime: 1.12.5 +Optimal Orthanc runtime: 1.12.6 + +Minimal Postgresql Server version: 9 +Optimal Postgresql Server version: 11+ + + +Maintenance: +* Optimized the SQL query that is maintaing the childCount + when new instances are ingested. +* New configuration "AllowInconsistentChildCounts" to speed + up childCount computation. If set to true, childCount values + of recently ingested resources will be incorrect until the next + execution of the DB Housekeeping thread (defined by "HousekeepingInterval" + - default value is 1s) +* Fixed high memory usage due to caching of too many + prepared SQL statement when using since & limit. +* Removed duplicate comparison in find SQL queries. +* Now returning results when e.g, ordering instances against a metadata they don't have. + + + Release 7.1 (2025-01-23) ======================== diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OrthancPostgreSQL-7.1/PostgreSQL/Plugins/PostgreSQLIndex.cpp new/OrthancPostgreSQL-7.2/PostgreSQL/Plugins/PostgreSQLIndex.cpp --- old/OrthancPostgreSQL-7.1/PostgreSQL/Plugins/PostgreSQLIndex.cpp 2025-01-23 16:27:05.000000000 +0100 +++ new/OrthancPostgreSQL-7.2/PostgreSQL/Plugins/PostgreSQLIndex.cpp 2025-02-27 08:59:27.000000000 +0100 @@ -49,13 +49,14 @@ static const GlobalProperty GlobalProperty_HasComputeStatisticsReadOnly = GlobalProperty_DatabaseInternal4; } +#define CURRENT_DB_REVISION 4 namespace OrthancDatabases { PostgreSQLIndex::PostgreSQLIndex(OrthancPluginContext* context, const PostgreSQLParameters& parameters, bool readOnly) : - IndexBackend(context, readOnly), + IndexBackend(context, readOnly, parameters.GetAllowInconsistentChildCounts()), parameters_(parameters), clearAll_(false), hkHasComputedAllMissingChildCount_(false) @@ -122,6 +123,7 @@ { DatabaseManager::Transaction t(manager, TransactionType_ReadWrite); + bool hasAppliedAnUpgrade = false; if (!t.GetDatabaseTransaction().DoesTableExist("Resources")) { @@ -147,31 +149,12 @@ throw Orthanc::OrthancException(Orthanc::ErrorCode_Database); } - bool applyUpgradeFromUnknownToV1 = false; - bool applyUpgradeV1toV2 = false; - bool applyUpgradeV2toV3 = false; - bool applyPrepareIndex = false; - - int revision; - if (!LookupGlobalIntegerProperty(revision, manager, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabasePatchLevel)) - { - LOG(WARNING) << "No DatabasePatchLevel found, assuming it's 1"; - revision = 1; - applyUpgradeFromUnknownToV1 = true; - applyUpgradeV1toV2 = true; - applyUpgradeV2toV3 = true; - } - else if (revision == 1) - { - LOG(WARNING) << "DatabasePatchLevel is 1"; - applyUpgradeV1toV2 = true; - applyUpgradeV2toV3 = true; - } - else if (revision == 2) + int currentRevision = 0; + if (!LookupGlobalIntegerProperty(currentRevision, manager, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabasePatchLevel)) { - LOG(WARNING) << "DatabasePatchLevel is 2"; - applyUpgradeV2toV3 = true; + LOG(WARNING) << "No Database revision found"; } + LOG(WARNING) << "Current Database revision is " << currentRevision; int hasTrigram = 0; if (!LookupGlobalIntegerProperty(hasTrigram, manager, MISSING_SERVER_IDENTIFIER, @@ -180,9 +163,12 @@ { // We've observed 9 minutes on DB with 100000 studies LOG(WARNING) << "The DB schema update will try to enable trigram matching on the PostgreSQL database " - << "to speed up wildcard searches. This may take several minutes"; - applyUpgradeV1toV2 = true; - applyUpgradeV2toV3 = true; + << "to speed up wildcard searches. This may take several minutes. "; + if (currentRevision > 0) + { + LOG(WARNING) << "Considering current revision is 1"; + currentRevision = 1; + } } int property = 0; @@ -190,15 +176,18 @@ Orthanc::GlobalProperty_GetLastChangeIndex) || property != 1) { - applyUpgradeV1toV2 = true; - applyUpgradeV2toV3 = true; + LOG(WARNING) << "The DB schema does not contain the GetLastChangeIndex update. "; + if (currentRevision > 0) + { + LOG(WARNING) << "Considering current revision is 1"; + currentRevision = 1; + } + } // If you add new tests here, update the test in the "ReadOnly" code below - applyPrepareIndex = applyUpgradeV2toV3; - - if (applyUpgradeFromUnknownToV1) + if (currentRevision == 0) { LOG(WARNING) << "Upgrading DB schema from unknown to revision 1"; std::string query; @@ -206,9 +195,10 @@ Orthanc::EmbeddedResources::GetFileResource (query, Orthanc::EmbeddedResources::POSTGRESQL_UPGRADE_UNKNOWN_TO_REV1); t.GetDatabaseTransaction().ExecuteMultiLines(query); + currentRevision = 1; } - if (applyUpgradeV1toV2) + if (currentRevision == 1) { LOG(WARNING) << "Upgrading DB schema from revision 1 to revision 2"; @@ -217,9 +207,10 @@ Orthanc::EmbeddedResources::GetFileResource (query, Orthanc::EmbeddedResources::POSTGRESQL_UPGRADE_REV1_TO_REV2); t.GetDatabaseTransaction().ExecuteMultiLines(query); + currentRevision = 2; } - if (applyUpgradeV2toV3) + if (currentRevision == 2) { LOG(WARNING) << "Upgrading DB schema from revision 2 to revision 3"; @@ -228,17 +219,61 @@ Orthanc::EmbeddedResources::GetFileResource (query, Orthanc::EmbeddedResources::POSTGRESQL_UPGRADE_REV2_TO_REV3); t.GetDatabaseTransaction().ExecuteMultiLines(query); + currentRevision = 3; } - if (applyPrepareIndex) + if (currentRevision == 3) { + LOG(WARNING) << "Upgrading DB schema from revision 3 to revision 4"; + + std::string query; + + Orthanc::EmbeddedResources::GetFileResource + (query, Orthanc::EmbeddedResources::POSTGRESQL_UPGRADE_REV3_TO_REV4); + t.GetDatabaseTransaction().ExecuteMultiLines(query); + hasAppliedAnUpgrade = true; + currentRevision = 4; + } + + if (hasAppliedAnUpgrade) + { + LOG(WARNING) << "Upgrading DB schema by applying PrepareIndex.sql"; // apply all idempotent changes that are in the PrepareIndex.sql ApplyPrepareIndex(t, manager); + + if (!LookupGlobalIntegerProperty(currentRevision, manager, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabasePatchLevel)) + { + LOG(ERROR) << "No Database revision found after the upgrade !"; + throw Orthanc::OrthancException(Orthanc::ErrorCode_Database); + } + + LOG(WARNING) << "Database revision after the upgrade is " << currentRevision; + + if (currentRevision != CURRENT_DB_REVISION) + { + LOG(ERROR) << "Invalid database revision after the upgrade !"; + throw Orthanc::OrthancException(Orthanc::ErrorCode_Database); + } } } t.Commit(); + + if (hasAppliedAnUpgrade) + { + int currentRevision = 0; + + LookupGlobalIntegerProperty(currentRevision, manager, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabasePatchLevel); + LOG(WARNING) << "Database revision after the upgrade is " << currentRevision; + + if (currentRevision != CURRENT_DB_REVISION) + { + LOG(ERROR) << "Invalid database revision after the upgrade !"; + throw Orthanc::OrthancException(Orthanc::ErrorCode_Database); + } + } + } } else @@ -250,7 +285,7 @@ // test if the latest "extension" has been installed int revision; if (!LookupGlobalIntegerProperty(revision, manager, MISSING_SERVER_IDENTIFIER, Orthanc::GlobalProperty_DatabasePatchLevel) - || revision != 3) + || revision != CURRENT_DB_REVISION) { LOG(ERROR) << "READ-ONLY SYSTEM: the DB does not have the correct schema to run with this version of the plugin"; throw Orthanc::OrthancException(Orthanc::ErrorCode_Database); @@ -768,5 +803,26 @@ int64_t patientsCount, studiesCount, seriesCount, instancesCount, compressedSize, uncompressedSize; UpdateAndGetStatistics(manager, patientsCount, studiesCount, seriesCount, instancesCount, compressedSize, uncompressedSize); } + + // Update the invalidated childCounts + try + { + DatabaseManager::CachedStatement statement( + STATEMENT_FROM_HERE, manager, + "SELECT * FROM UpdateInvalidChildCounts()"); + + statement.Execute(); + + int64_t updatedCount = statement.ReadInteger64(0); + if (updatedCount > 0) + { + LOG(INFO) << "Updated " << updatedCount << " invalid ChildCount entries"; + } + } + catch (Orthanc::OrthancException&) + { + // the statement may fail in case of temporary deadlock -> it will be retried at the next HK + LOG(INFO) << "Updat of invalid ChildCount entries has failed (will be retried)"; + } } } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OrthancPostgreSQL-7.1/PostgreSQL/Plugins/PostgreSQLIndex.h new/OrthancPostgreSQL-7.2/PostgreSQL/Plugins/PostgreSQLIndex.h --- old/OrthancPostgreSQL-7.1/PostgreSQL/Plugins/PostgreSQLIndex.h 2025-01-23 16:27:05.000000000 +0100 +++ new/OrthancPostgreSQL-7.2/PostgreSQL/Plugins/PostgreSQLIndex.h 2025-02-27 08:59:27.000000000 +0100 @@ -44,7 +44,7 @@ virtual void ClearRemainingAncestor(DatabaseManager& manager) ORTHANC_OVERRIDE; - virtual bool HasChildCountTable() const ORTHANC_OVERRIDE + virtual bool HasChildCountColumn() const ORTHANC_OVERRIDE { return true; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OrthancPostgreSQL-7.1/PostgreSQL/Plugins/SQL/Downgrades/Rev4ToRev3.sql new/OrthancPostgreSQL-7.2/PostgreSQL/Plugins/SQL/Downgrades/Rev4ToRev3.sql --- old/OrthancPostgreSQL-7.1/PostgreSQL/Plugins/SQL/Downgrades/Rev4ToRev3.sql 1970-01-01 01:00:00.000000000 +0100 +++ new/OrthancPostgreSQL-7.2/PostgreSQL/Plugins/SQL/Downgrades/Rev4ToRev3.sql 2025-02-27 08:59:27.000000000 +0100 @@ -0,0 +1,87 @@ +-- This file contains an SQL procedure to downgrade from schema Rev4 to Rev3 (version = 6). + -- It re-installs the old childcount trigger mechanisms + +DO $$ +DECLARE + current_revision TEXT; + expected_revision TEXT; +BEGIN + expected_revision := '4'; + + SELECT value INTO current_revision FROM GlobalProperties WHERE property = 4; -- GlobalProperty_DatabasePatchLevel + + IF current_revision != expected_revision THEN + RAISE EXCEPTION 'Unexpected schema revision % to run this script. Expected revision = %', current_revision, expected_revision; + END IF; +END $$; + +--- + +DROP TRIGGER IF EXISTS IncrementChildCount on Resources; +DROP TRIGGER IF EXISTS DecrementChildCount on Resources; + +CREATE OR REPLACE FUNCTION UpdateChildCount() +RETURNS TRIGGER AS $body$ +BEGIN + IF TG_OP = 'INSERT' THEN + IF new.parentId IS NOT NULL THEN + -- try to increment the childcount from the parent + -- note that we already have the lock on this row because the parent is locked in CreateInstance + UPDATE Resources + SET childCount = childCount + 1 + WHERE internalId = new.parentId AND childCount IS NOT NULL; + + -- this should only happen for old studies whose childCount has not yet been computed + -- note: this child has already been added so it will be counted + IF NOT FOUND THEN + UPDATE Resources + SET childCount = (SELECT COUNT(*) + FROM Resources + WHERE internalId = new.parentId) + WHERE internalId = new.parentId; + END IF; + END IF; + + ELSIF TG_OP = 'DELETE' THEN + + IF old.parentId IS NOT NULL THEN + + -- Decrement the child count for the parent + -- note that we already have the lock on this row because the parent is locked in DeleteResource + UPDATE Resources + SET childCount = childCount - 1 + WHERE internalId = old.parentId AND childCount IS NOT NULL; + + -- this should only happen for old studies whose childCount has not yet been computed + -- note: this child has already been removed so it will not be counted + IF NOT FOUND THEN + UPDATE Resources + SET childCount = (SELECT COUNT(*) + FROM Resources + WHERE internalId = new.parentId) + WHERE internalId = new.parentId; + END IF; + END IF; + + END IF; + RETURN NULL; +END; +$body$ LANGUAGE plpgsql; + +CREATE TRIGGER IncrementChildCount +AFTER INSERT ON Resources +FOR EACH ROW +EXECUTE PROCEDURE UpdateChildCount(); + +CREATE TRIGGER DecrementChildCount +AFTER DELETE ON Resources +FOR EACH ROW +WHEN (OLD.parentId IS NOT NULL) +EXECUTE PROCEDURE UpdateChildCount(); + +----------- + +-- set the global properties that actually documents the DB version, revision and some of the capabilities +-- modify only the ones that have changed +DELETE FROM GlobalProperties WHERE property IN (4, 11); +INSERT INTO GlobalProperties VALUES (4, 3); -- GlobalProperty_DatabasePatchLevel diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OrthancPostgreSQL-7.1/PostgreSQL/Plugins/SQL/PrepareIndex.sql new/OrthancPostgreSQL-7.2/PostgreSQL/Plugins/SQL/PrepareIndex.sql --- old/OrthancPostgreSQL-7.1/PostgreSQL/Plugins/SQL/PrepareIndex.sql 2025-01-23 16:27:05.000000000 +0100 +++ new/OrthancPostgreSQL-7.2/PostgreSQL/Plugins/SQL/PrepareIndex.sql 2025-02-27 08:59:27.000000000 +0100 @@ -626,6 +626,55 @@ $body$ LANGUAGE plpgsql; +-- -- new in rev4 + +-- This table records all resource entries whose childCount column is currently invalid +-- because of recent addition/removal of a child. +-- This way, each transaction that is adding/removing a child can add row independently +-- in this table without having to lock the parent resource row. +-- At regular interval, the DB housekeeping thread updates the childCount column of +-- resources with an entry in this table. +CREATE TABLE IF NOT EXISTS InvalidChildCounts( + id BIGINT REFERENCES Resources(internalId) ON DELETE CASCADE, + updatedAt TIMESTAMP DEFAULT NOW()); + +-- Updates the Resources.childCount column with the delta that have not been committed yet. +-- A thread will call this function at regular interval to update all pending values. +CREATE OR REPLACE FUNCTION UpdateInvalidChildCounts( + OUT updated_rows_count BIGINT +) RETURNS BIGINT AS $body$ +DECLARE + locked_resources_ids BIGINT[]; +BEGIN + + -- Lock the resources rows asap to prevent deadlocks + -- that will need to be retried + SELECT ARRAY(SELECT internalId + FROM Resources + WHERE internalId IN (SELECT DISTINCT id FROM InvalidChildCounts) + FOR UPDATE) + INTO locked_resources_ids; + + -- New rows can be added in the meantime, they won't be taken into account this time. + WITH deleted_rows AS ( + DELETE FROM InvalidChildCounts + WHERE id = ANY(locked_resources_ids) + RETURNING id + ) + + UPDATE Resources + SET childCount = (SELECT COUNT(childLevel.internalId) + FROM Resources AS childLevel + WHERE childLevel.parentId = Resources.internalId) + WHERE internalid = ANY(locked_resources_ids); + + -- Get the number of rows affected + GET DIAGNOSTICS updated_rows_count = ROW_COUNT; + +END; +$body$ LANGUAGE plpgsql; + + DROP TRIGGER IF EXISTS IncrementChildCount on Resources; DROP TRIGGER IF EXISTS DecrementChildCount on Resources; @@ -635,42 +684,20 @@ BEGIN IF TG_OP = 'INSERT' THEN IF new.parentId IS NOT NULL THEN - -- try to increment the childcount from the parent - -- note that we already have the lock on this row because the parent is locked in CreateInstance - UPDATE Resources - SET childCount = childCount + 1 - WHERE internalId = new.parentId AND childCount IS NOT NULL; - - -- this should only happen for old studies whose childCount has not yet been computed - -- note: this child has already been added so it will be counted - IF NOT FOUND THEN - UPDATE Resources - SET childCount = (SELECT COUNT(*) - FROM Resources - WHERE internalId = new.parentId) - WHERE internalId = new.parentId; - END IF; + -- mark the parent's childCount as invalid + INSERT INTO InvalidChildCounts VALUES(new.parentId); END IF; ELSIF TG_OP = 'DELETE' THEN IF old.parentId IS NOT NULL THEN - - -- Decrement the child count for the parent - -- note that we already have the lock on this row because the parent is locked in DeleteResource - UPDATE Resources - SET childCount = childCount - 1 - WHERE internalId = old.parentId AND childCount IS NOT NULL; - - -- this should only happen for old studies whose childCount has not yet been computed - -- note: this child has already been removed so it will not be counted - IF NOT FOUND THEN - UPDATE Resources - SET childCount = (SELECT COUNT(*) - FROM Resources - WHERE internalId = new.parentId) - WHERE internalId = new.parentId; - END IF; + BEGIN + -- mark the parent's childCount as invalid + INSERT INTO InvalidChildCounts VALUES(old.parentId); + EXCEPTION + -- when deleting the last child of a parent, the insert will fail (this is expected) + WHEN foreign_key_violation THEN NULL; + END; END IF; END IF; @@ -694,7 +721,7 @@ -- set the global properties that actually documents the DB version, revision and some of the capabilities DELETE FROM GlobalProperties WHERE property IN (1, 4, 6, 10, 11, 12, 13, 14); INSERT INTO GlobalProperties VALUES (1, 6); -- GlobalProperty_DatabaseSchemaVersion -INSERT INTO GlobalProperties VALUES (4, 3); -- GlobalProperty_DatabasePatchLevel +INSERT INTO GlobalProperties VALUES (4, 4); -- GlobalProperty_DatabasePatchLevel INSERT INTO GlobalProperties VALUES (6, 1); -- GlobalProperty_GetTotalSizeIsFast INSERT INTO GlobalProperties VALUES (10, 1); -- GlobalProperty_HasTrigramIndex INSERT INTO GlobalProperties VALUES (11, 3); -- GlobalProperty_HasCreateInstance -- this is actually the 3rd version of HasCreateInstance diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OrthancPostgreSQL-7.1/PostgreSQL/Plugins/SQL/Upgrades/Rev2ToRev3.sql new/OrthancPostgreSQL-7.2/PostgreSQL/Plugins/SQL/Upgrades/Rev2ToRev3.sql --- old/OrthancPostgreSQL-7.1/PostgreSQL/Plugins/SQL/Upgrades/Rev2ToRev3.sql 2025-01-23 16:27:05.000000000 +0100 +++ new/OrthancPostgreSQL-7.2/PostgreSQL/Plugins/SQL/Upgrades/Rev2ToRev3.sql 2025-02-27 08:59:27.000000000 +0100 @@ -40,5 +40,5 @@ -- other changes performed in PrepareIndex.sql: - -- add ChildCount tables and triggers + -- add ChildCount triggers diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OrthancPostgreSQL-7.1/PostgreSQL/Plugins/SQL/Upgrades/Rev3ToRev4.sql new/OrthancPostgreSQL-7.2/PostgreSQL/Plugins/SQL/Upgrades/Rev3ToRev4.sql --- old/OrthancPostgreSQL-7.1/PostgreSQL/Plugins/SQL/Upgrades/Rev3ToRev4.sql 1970-01-01 01:00:00.000000000 +0100 +++ new/OrthancPostgreSQL-7.2/PostgreSQL/Plugins/SQL/Upgrades/Rev3ToRev4.sql 2025-02-27 08:59:27.000000000 +0100 @@ -0,0 +1,2 @@ +-- everything is performed in PrepareIndex.sql +SELECT 1; \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OrthancPostgreSQL-7.1/TODO new/OrthancPostgreSQL-7.2/TODO --- old/OrthancPostgreSQL-7.1/TODO 2025-01-23 16:27:05.000000000 +0100 +++ new/OrthancPostgreSQL-7.2/TODO 2025-02-27 08:59:27.000000000 +0100 @@ -41,6 +41,9 @@ a DB where there was a 'AttachedFiles' table in the public schema and another one in a 'MyPacs' schema and Orthanc was actually using the 'MyPacs.AttachedFiles' table !!! Orthanc was then seeing only the most recent attached files !!! + We should also be able to use other schemas: + https://discourse.orthanc-server.org/t/orthanc-container-unable-to-connect-to-specified-postgresql-database/5471 + * Have a separate "thread" to UpdateStatistics to avoid large computations ?