Mercurial > hg > orthanc
changeset 6562:d4a3c883f6c1
restored ApplyLookupResources and make sure GenericFind handles Labels filtering against 'None' and [] correctly
| author | Alain Mazy <am@orthanc.team> |
|---|---|
| date | Thu, 15 Jan 2026 15:25:30 +0100 |
| parents | f2de0250584c |
| children | 62ebe3e9e8bf |
| files | NEWS OrthancServer/Sources/Database/Compatibility/GenericFind.cpp OrthancServer/Sources/Database/FindRequest.cpp OrthancServer/Sources/Database/FindRequest.h OrthancServer/Sources/Database/IDatabaseWrapper.h OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h OrthancServer/Sources/Search/ISqlLookupFormatter.cpp OrthancServer/Sources/Search/ISqlLookupFormatter.h |
| diffstat | 9 files changed, 185 insertions(+), 17 deletions(-) [+] |
line wrap: on
line diff
--- a/NEWS Wed Jan 14 19:06:30 2026 +0100 +++ b/NEWS Thu Jan 15 15:25:30 2026 +0100 @@ -4,7 +4,7 @@ General ------- - +TODO: re-enable ExtendedFind in SQLite REST API -------- @@ -15,7 +15,7 @@ through the `filename` argument of "/.../file" or "/.../archive" routes. * In tools/find, filtering against "LabelsConstraint": "None" with an empty "Labels" list now returns all resources that do not have any labels attached instead of returning all resources. - This applies to the default SQLite DB and will apply to the next PostgreSQL plugin (vXXX) + This applies to the default SQLite DB and will apply to the next PostgreSQL plugin (v 10.1) Maintenance
--- a/OrthancServer/Sources/Database/Compatibility/GenericFind.cpp Wed Jan 14 19:06:30 2026 +0100 +++ b/OrthancServer/Sources/Database/Compatibility/GenericFind.cpp Thu Jan 15 15:25:30 2026 +0100 @@ -110,7 +110,7 @@ const IDatabaseWrapper::Capabilities& capabilities, const FindRequest& request) { - if (!request.GetLabels().empty() && + if (request.HasLabelsConstraint() && !capabilities.HasLabelsSupport()) { throw OrthancException(ErrorCode_NotImplemented, "The database backend doesn't support labels");
--- a/OrthancServer/Sources/Database/FindRequest.cpp Wed Jan 14 19:06:30 2026 +0100 +++ b/OrthancServer/Sources/Database/FindRequest.cpp Thu Jan 15 15:25:30 2026 +0100 @@ -285,11 +285,16 @@ } } + bool FindRequest::HasLabelsConstraint() const + { + return !GetLabels().empty() || GetLabelsConstraint() == LabelsConstraint_None; // from 1.12.11, 'None' with an empty labels list means "list all resources without any labels" + } + bool FindRequest::HasConstraints() const { return (!GetDicomTagConstraints().IsEmpty() || GetMetadataConstraintsCount() != 0 || - !GetLabels().empty() || + HasLabelsConstraint() || !GetOrdering().empty()); }
--- a/OrthancServer/Sources/Database/FindRequest.h Wed Jan 14 19:06:30 2026 +0100 +++ b/OrthancServer/Sources/Database/FindRequest.h Thu Jan 15 15:25:30 2026 +0100 @@ -465,6 +465,8 @@ bool HasConstraints() const; + bool HasLabelsConstraint() const; + bool IsTrivialFind(std::string& publicId /* out */) const; }; }
--- a/OrthancServer/Sources/Database/IDatabaseWrapper.h Wed Jan 14 19:06:30 2026 +0100 +++ b/OrthancServer/Sources/Database/IDatabaseWrapper.h Thu Jan 15 15:25:30 2026 +0100 @@ -497,7 +497,7 @@ }; - // TODO-FIND: Could this interface be removed? + // This interface is still used by old plugins that do not implement the ExtendedFind. class ICompatibilityTransaction : public boost::noncopyable { public:
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp Wed Jan 14 19:06:30 2026 +0100 +++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp Thu Jan 15 15:25:30 2026 +0100 @@ -441,6 +441,48 @@ s.Run(); } + virtual void ApplyLookupResources(std::list<std::string>& resourcesId, + std::list<std::string>* instancesId, + const DatabaseDicomTagConstraints& lookup, + ResourceType queryLevel, + const std::set<std::string>& labels, + LabelsConstraint labelsConstraint, + uint32_t limit) ORTHANC_OVERRIDE + { + LookupFormatter formatter; + + std::string sql; + LookupFormatter::Apply(sql, formatter, lookup, queryLevel, labels, labelsConstraint, limit); + + sql = "CREATE TEMPORARY TABLE Lookup AS " + sql; // TODO-FIND: use a CTE (or is this method obsolete ?) + + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "DROP TABLE IF EXISTS Lookup"); + s.Run(); + } + + { + SQLite::Statement statement(db_, sql); + formatter.Bind(statement); + statement.Run(); + } + + if (instancesId != NULL) + { + AnswerLookup(resourcesId, *instancesId, queryLevel); + } + else + { + resourcesId.clear(); + + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Lookup"); + + while (s.Step()) + { + resourcesId.push_back(s.ColumnString(0)); + } + } + } #define C0_QUERY_ID 0 #define C1_INTERNAL_ID 1
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h Wed Jan 14 19:06:30 2026 +0100 +++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h Thu Jan 15 15:25:30 2026 +0100 @@ -103,7 +103,8 @@ virtual bool HasIntegratedFind() const ORTHANC_OVERRIDE { return true; // => This uses specialized SQL commands - //return false; // => This uses Compatibility/GenericFind + // return false; // => This uses Compatibility/GenericFind and this is useful to keep this implementation in SQLite to test the compatibility layer without using an external plugin ! + // hence the derivation from BaseCompatibilityTransaction } /** @@ -112,7 +113,7 @@ * "UnitTestsTransaction" give access to additional information * about the underlying SQLite database to be used in unit tests. **/ - class UnitTestsTransaction : public BaseCompatibilityTransaction // TODO: replace by IDatabaseWrapper::ITransaction and remove all compatibility methods from the SQLiteDatabaseWrapper ? + class UnitTestsTransaction : public BaseCompatibilityTransaction // We keep the compatibility transaction in SQLite in order to facilitate testing/development of the CompatibilityLayer (GenericFind) without using an external DB plugin { protected: SQLite::Connection& db_; @@ -145,16 +146,6 @@ const DicomTag& tag, const std::string& value); - virtual void ApplyLookupResources(std::list<std::string>& resourcesId, - std::list<std::string>* instancesId, - const DatabaseDicomTagConstraints& lookup, - ResourceType queryLevel, - const std::set<std::string>& labels, - LabelsConstraint labelsConstraint, - uint32_t limit) ORTHANC_OVERRIDE - { - throw OrthancException(ErrorCode_BadSequenceOfCalls); // this function is not supposed to be called with the SQLite engine - } }; }; }
--- a/OrthancServer/Sources/Search/ISqlLookupFormatter.cpp Wed Jan 14 19:06:30 2026 +0100 +++ b/OrthancServer/Sources/Search/ISqlLookupFormatter.cpp Thu Jan 15 15:25:30 2026 +0100 @@ -607,6 +607,125 @@ } + // Note: this is used only when disabling ExtendedFind in SQLite in order to validate the GenericFind compatibility layer + void ISqlLookupFormatter::Apply(std::string& sql, + ISqlLookupFormatter& formatter, + const DatabaseDicomTagConstraints& lookup, + ResourceType queryLevel, + const std::set<std::string>& labels, + LabelsConstraint labelsConstraint, + size_t limit) + { + // get the limit levels of the DICOM Tags lookup + ResourceType lowerLevel, upperLevel; + GetLookupLevels(lowerLevel, upperLevel, queryLevel, lookup); + + assert(upperLevel <= queryLevel && + queryLevel <= lowerLevel); + + const bool escapeBrackets = formatter.IsEscapeBrackets(); + + std::string joins, comparisons; + + size_t count = 0; + + for (size_t i = 0; i < lookup.GetSize(); i++) + { + const DatabaseDicomTagConstraint& constraint = lookup.GetConstraint(i); + + std::string comparison; + + if (FormatComparison(comparison, formatter, constraint, count, escapeBrackets)) + { + std::string join; + FormatJoin(join, constraint, count); + joins += join; + + if (!comparison.empty()) + { + comparisons += " AND " + comparison; + } + + count ++; + } + } + + sql = ("SELECT " + + FormatLevel(queryLevel) + ".publicId, " + + FormatLevel(queryLevel) + ".internalId" + + " FROM Resources AS " + FormatLevel(queryLevel)); + + for (int level = queryLevel - 1; level >= upperLevel; level--) + { + sql += (" INNER JOIN Resources " + + FormatLevel(static_cast<ResourceType>(level)) + " ON " + + FormatLevel(static_cast<ResourceType>(level)) + ".internalId=" + + FormatLevel(static_cast<ResourceType>(level + 1)) + ".parentId"); + } + + for (int level = queryLevel + 1; level <= lowerLevel; level++) + { + sql += (" INNER JOIN Resources " + + FormatLevel(static_cast<ResourceType>(level)) + " ON " + + FormatLevel(static_cast<ResourceType>(level - 1)) + ".internalId=" + + FormatLevel(static_cast<ResourceType>(level)) + ".parentId"); + } + + std::list<std::string> where; + where.push_back(FormatLevel(queryLevel) + ".resourceType = " + + formatter.FormatResourceType(queryLevel) + comparisons); + + if (!labels.empty()) + { + /** + * "In SQL Server, NOT EXISTS and NOT IN predicates are the best + * way to search for missing values, as long as both columns in + * question are NOT NULL." + * https://explainextended.com/2009/09/15/not-in-vs-not-exists-vs-left-join-is-null-sql-server/ + **/ + + std::list<std::string> formattedLabels; + for (std::set<std::string>::const_iterator it = labels.begin(); it != labels.end(); ++it) + { + formattedLabels.push_back(formatter.GenerateParameter(*it)); + } + + std::string condition; + switch (labelsConstraint) + { + case LabelsConstraint_Any: + condition = "> 0"; + break; + + case LabelsConstraint_All: + condition = "= " + boost::lexical_cast<std::string>(labels.size()); + break; + + case LabelsConstraint_None: + condition = "= 0"; + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + where.push_back("(SELECT COUNT(1) FROM Labels AS selectedLabels WHERE selectedLabels.id = " + FormatLevel(queryLevel) + + ".internalId AND selectedLabels.label IN (" + Join(formattedLabels, "", ", ") + ")) " + condition); + } + else if (labelsConstraint == LabelsConstraint_None) // from 1.12.11, 'None' with an empty labels list means "list all resources without any labels" + { + where.push_back("(SELECT COUNT(1) FROM Labels WHERE id = " + FormatLevel(queryLevel) + ".internalId) = 0"); + } + + sql += joins + Join(where, " WHERE ", " AND "); + + if (limit != 0) + { + sql += " LIMIT " + boost::lexical_cast<std::string>(limit); + } + } + + void ISqlLookupFormatter::Apply(std::string& sql, ISqlLookupFormatter& formatter, const FindRequest& request)
--- a/OrthancServer/Sources/Search/ISqlLookupFormatter.h Wed Jan 14 19:06:30 2026 +0100 +++ b/OrthancServer/Sources/Search/ISqlLookupFormatter.h Thu Jan 15 15:25:30 2026 +0100 @@ -68,6 +68,15 @@ const ResourceType& queryLevel, const DatabaseDicomTagConstraints& lookup); + // used only for the compatibility mode when disabling ExtendedFind for dev + static void Apply(std::string& sql, + ISqlLookupFormatter& formatter, + const DatabaseDicomTagConstraints& lookup, + ResourceType queryLevel, + const std::set<std::string>& labels, // New in Orthanc 1.12.0 + LabelsConstraint labelsConstraint, // New in Orthanc 1.12.0 + size_t limit); + static void ApplySingleLevel(std::string& sql, ISqlLookupFormatter& formatter, const DatabaseDicomTagConstraints& lookup,
