Mercurial > hg > orthanc
changeset 5950:bfadfbcca13e default
tools/find: fix query by ModalitiesInStudy with pagination
author | Alain Mazy <am@orthanc.team> |
---|---|
date | Wed, 08 Jan 2025 15:18:00 +0100 |
parents | 81c78bbc9c97 |
children | |
files | NEWS OrthancServer/Sources/Database/MainDicomTagsRegistry.cpp OrthancServer/Sources/Database/MainDicomTagsRegistry.h OrthancServer/Sources/Database/PrepareDatabase.sql OrthancServer/Sources/OrthancFindRequestHandler.cpp OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp OrthancServer/Sources/OrthancWebDav.cpp OrthancServer/Sources/ResourceFinder.cpp OrthancServer/Sources/ResourceFinder.h OrthancServer/Sources/Search/ISqlLookupFormatter.cpp |
diffstat | 10 files changed, 126 insertions(+), 64 deletions(-) [+] |
line wrap: on
line diff
--- a/NEWS Mon Jan 06 17:10:09 2025 +0100 +++ b/NEWS Wed Jan 08 15:18:00 2025 +0100 @@ -4,6 +4,9 @@ Maintenance ----------- +* Fix: With "ExtendedFind", using tools/find while querying against ModalitiesInStudy + was not compatible with pagination which prevented, amongst others, to use the + modality filter in OE2. * When the "HttpsCACertificates" configuration is empty. Orthanc will now use the operating system native CA store (if any). This is equivalent to the --ca-native curl option.
--- a/OrthancServer/Sources/Database/MainDicomTagsRegistry.cpp Mon Jan 06 17:10:09 2025 +0100 +++ b/OrthancServer/Sources/Database/MainDicomTagsRegistry.cpp Wed Jan 08 15:18:00 2025 +0100 @@ -98,11 +98,14 @@ } - bool MainDicomTagsRegistry::NormalizeLookup(DatabaseDicomTagConstraints& target, + bool MainDicomTagsRegistry::NormalizeLookup(bool& canBeFullyPerformedInDb, + DatabaseDicomTagConstraints& target, const DatabaseLookup& source, - ResourceType queryLevel) const + ResourceType queryLevel, + bool allowChildrenExistsQueries) const { bool isEquivalentLookup = true; + canBeFullyPerformedInDb = true; target.Clear(); @@ -110,8 +113,10 @@ { ResourceType level; DicomTagType type; + const DicomTag& tag = source.GetConstraint(i).GetTag(); - LookupTag(level, type, source.GetConstraint(i).GetTag()); + LookupTag(level, type, tag); + if (type == DicomTagType_Identifier || type == DicomTagType_Main) @@ -124,6 +129,13 @@ } bool isEquivalentConstraint; + + // DicomIdentifiers are stored UPPERCASE -> as soon as a case senstive search happens, it is currently not possible to perform it in DB only + if (type == DicomTagType_Identifier && source.GetConstraint(i).IsCaseSensitive()) + { + canBeFullyPerformedInDb = false; + } + target.AddConstraint(source.GetConstraint(i).ConvertToDatabaseConstraint(isEquivalentConstraint, level, type)); if (!isEquivalentConstraint) @@ -133,7 +145,26 @@ } else { - isEquivalentLookup = false; + if (allowChildrenExistsQueries && + tag == DICOM_TAG_MODALITIES_IN_STUDY && queryLevel == ResourceType_Study) + { + // transform the query into a children query + std::vector<std::string> values(source.GetConstraint(i).GetValues().begin(), source.GetConstraint(i).GetValues().end()); + + std::unique_ptr<DatabaseDicomTagConstraint> constraint(new DatabaseDicomTagConstraint(ResourceType_Series, + DICOM_TAG_MODALITY, + false, + ConstraintType_List, + values, + false, + true)); + target.AddConstraint(constraint.release()); + } + else + { + isEquivalentLookup = false; + canBeFullyPerformedInDb = false; + } } }
--- a/OrthancServer/Sources/Database/MainDicomTagsRegistry.h Mon Jan 06 17:10:09 2025 +0100 +++ b/OrthancServer/Sources/Database/MainDicomTagsRegistry.h Wed Jan 08 15:18:00 2025 +0100 @@ -82,8 +82,10 @@ * constraints are less strict than the original DatabaseLookup, * so more resources will match them. **/ - bool NormalizeLookup(DatabaseDicomTagConstraints& target, + bool NormalizeLookup(bool& canBeFullyPerformedInDb, + DatabaseDicomTagConstraints& target, const DatabaseLookup& source, - ResourceType queryLevel) const; + ResourceType queryLevel, + bool allowChildrenExistsQueries) const; }; }
--- a/OrthancServer/Sources/Database/PrepareDatabase.sql Mon Jan 06 17:10:09 2025 +0100 +++ b/OrthancServer/Sources/Database/PrepareDatabase.sql Wed Jan 08 15:18:00 2025 +0100 @@ -40,6 +40,9 @@ ); -- The following table was added in Orthanc 0.8.5 (database v5) +-- It contains only the DICOM Tags that are commonly used for searches. +-- All these tags are converted to UPPERCASE ! +-- These tags are also stored in the MainDicomTags table without casing modificiation. CREATE TABLE DicomIdentifiers( id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE, tagGroup INTEGER,
--- a/OrthancServer/Sources/OrthancFindRequestHandler.cpp Mon Jan 06 17:10:09 2025 +0100 +++ b/OrthancServer/Sources/OrthancFindRequestHandler.cpp Wed Jan 08 15:18:00 2025 +0100 @@ -444,7 +444,7 @@ * Run the query. **/ - ResourceFinder finder(level, ResponseContentFlags_ID, context_.GetFindStorageAccessMode()); + ResourceFinder finder(level, ResponseContentFlags_ID, context_.GetFindStorageAccessMode(), context_.GetIndex().HasFindSupport()); finder.SetDatabaseLookup(lookup); finder.AddRequestedTags(requestedTags);
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Mon Jan 06 17:10:09 2025 +0100 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Wed Jan 08 15:18:00 2025 +0100 @@ -154,7 +154,7 @@ responseContent = static_cast<ResponseContentFlags>(static_cast<uint32_t>(responseContent) | ResponseContentFlags_Metadata); } - ResourceFinder finder(level, responseContent, context.GetFindStorageAccessMode()); + ResourceFinder finder(level, responseContent, context.GetFindStorageAccessMode(), context.GetIndex().HasFindSupport()); finder.SetOrthancId(level, identifier); finder.SetRetrieveMetadata(retrieveMetadata); @@ -194,7 +194,10 @@ OrthancRestApi::GetRequestedTags(requestedTags, call); OrthancRestApi::GetResponseContentAndExpand(responseContent, call); - ResourceFinder finder(resourceType, responseContent, OrthancRestApi::GetContext(call).GetFindStorageAccessMode()); + ResourceFinder finder(resourceType, + responseContent, + OrthancRestApi::GetContext(call).GetFindStorageAccessMode(), + OrthancRestApi::GetContext(call).GetIndex().HasFindSupport()); finder.AddRequestedTags(requestedTags); if (call.HasArgument("limit") || @@ -252,7 +255,10 @@ const DicomToJsonFormat format = OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human); - ResourceFinder finder(resourceType, ResponseContentFlags_ExpandTrue, OrthancRestApi::GetContext(call).GetFindStorageAccessMode()); + ResourceFinder finder(resourceType, + ResponseContentFlags_ExpandTrue, + OrthancRestApi::GetContext(call).GetFindStorageAccessMode(), + OrthancRestApi::GetContext(call).GetIndex().HasFindSupport()); finder.AddRequestedTags(requestedTags); finder.SetOrthancId(resourceType, call.GetUriComponent("id", "")); @@ -3072,25 +3078,6 @@ FindType_Count }; - static bool CanBePerformedInDb(const DatabaseLookup& lookup) - { - std::set<DicomTag> nonMainDicomTags; - if (!lookup.HasOnlyMainDicomTags(nonMainDicomTags)) - { - // TODO: prepared work for https://github.com/orthanc-server/orthanc-explorer-2/issues/73 - - // // filtering on ModalitiesInStudy is actually performed in DB too although the tag is not stored in the MainDicomTags - // if (nonMainDicomTags.size() == 1 && nonMainDicomTags.find(DICOM_TAG_MODALITIES_IN_STUDY) != nonMainDicomTags.end()) - // { - // return true; - // } - - return false; - } - - return true; - } - template <enum FindType requestType> static void Find(RestApiPostCall& call) @@ -3292,7 +3279,7 @@ const ResourceType level = StringToResourceType(request[KEY_LEVEL].asCString()); - ResourceFinder finder(level, responseContent, context.GetFindStorageAccessMode()); + ResourceFinder finder(level, responseContent, context.GetFindStorageAccessMode(), context.GetIndex().HasFindSupport()); DatabaseLookup dicomTagLookup; @@ -3339,12 +3326,6 @@ } } - if (requestType == FindType_Count && !CanBePerformedInDb(dicomTagLookup)) - { - throw OrthancException(ErrorCode_BadRequest, - "Unable to count resources when querying tags that are not stored as MainDicomTags in the Database"); - } - finder.SetDatabaseLookup(dicomTagLookup); } @@ -3444,12 +3425,11 @@ finder.SetDatabaseLimits(context.GetDatabaseLimits(level)); - if (((request.isMember(KEY_LIMIT) && request[KEY_LIMIT].asInt64() != 0) || - (request.isMember(KEY_SINCE) && request[KEY_SINCE].asInt64() != 0)) && - !CanBePerformedInDb(dicomTagLookup)) + if ((request.isMember(KEY_SINCE) && request[KEY_SINCE].asInt64() != 0) && + !finder.CanBeFullyPerformedInDb()) { throw OrthancException(ErrorCode_BadRequest, - "Unable to use " + std::string(KEY_LIMIT) + " or " + std::string(KEY_SINCE) + " in tools/find when querying tags that are not stored as MainDicomTags in the Database"); + "Unable to use '" + std::string(KEY_SINCE) + "' in tools/find when querying tags that are not stored as MainDicomTags in the Database"); } if (request.isMember(KEY_LIMIT)) @@ -3625,7 +3605,10 @@ std::set<DicomTag> requestedTags; OrthancRestApi::GetRequestedTags(requestedTags, call); - ResourceFinder finder(end, (expand ? ResponseContentFlags_ExpandTrue : ResponseContentFlags_ID), OrthancRestApi::GetContext(call).GetFindStorageAccessMode()); + ResourceFinder finder(end, + (expand ? ResponseContentFlags_ExpandTrue : ResponseContentFlags_ID), + OrthancRestApi::GetContext(call).GetFindStorageAccessMode(), + OrthancRestApi::GetContext(call).GetIndex().HasFindSupport()); finder.SetOrthancId(start, call.GetUriComponent("id", "")); finder.AddRequestedTags(requestedTags);
--- a/OrthancServer/Sources/OrthancWebDav.cpp Mon Jan 06 17:10:09 2025 +0100 +++ b/OrthancServer/Sources/OrthancWebDav.cpp Wed Jan 08 15:18:00 2025 +0100 @@ -938,7 +938,7 @@ Visitor visitor(resources); - ResourceFinder finder(ResourceType_Study, ResponseContentFlags_ID, GetContext().GetFindStorageAccessMode()); + ResourceFinder finder(ResourceType_Study, ResponseContentFlags_ID, GetContext().GetFindStorageAccessMode(), GetContext().GetIndex().HasFindSupport()); finder.SetDatabaseLookup(query); finder.Execute(visitor, GetContext()); } @@ -1016,7 +1016,7 @@ Visitor visitor; - ResourceFinder finder(ResourceType_Study, ResponseContentFlags_ID, context_.GetFindStorageAccessMode()); + ResourceFinder finder(ResourceType_Study, ResponseContentFlags_ID, context_.GetFindStorageAccessMode(), context_.GetIndex().HasFindSupport()); finder.SetDatabaseLookup(query); finder.Execute(visitor, context_); @@ -1394,7 +1394,7 @@ return false; } - ResourceFinder finder(level, ResponseContentFlags_ID, context_.GetFindStorageAccessMode()); + ResourceFinder finder(level, ResponseContentFlags_ID, context_.GetFindStorageAccessMode(), context_.GetIndex().HasFindSupport()); finder.SetDatabaseLookup(query); finder.SetRetrieveMetadata(true); @@ -1445,7 +1445,7 @@ ResourceType level, const DatabaseLookup& query) { - ResourceFinder finder(level, ResponseContentFlags_ExpandTrue, context.GetFindStorageAccessMode()); + ResourceFinder finder(level, ResponseContentFlags_ExpandTrue, context.GetFindStorageAccessMode(), context.GetIndex().HasFindSupport()); finder.SetDatabaseLookup(query); Json::Value expanded; @@ -1515,7 +1515,7 @@ mime = MimeType_Dicom; - ResourceFinder finder(ResourceType_Instance, ResponseContentFlags_ID, context_.GetFindStorageAccessMode()); + ResourceFinder finder(ResourceType_Instance, ResponseContentFlags_ID, context_.GetFindStorageAccessMode(), context_.GetIndex().HasFindSupport()); finder.SetDatabaseLookup(query); finder.SetRetrieveMetadata(true); finder.SetRetrieveAttachments(true); @@ -1645,7 +1645,7 @@ DicomDeleteVisitor visitor(context_, level); - ResourceFinder finder(level, ResponseContentFlags_ID, context_.GetFindStorageAccessMode()); + ResourceFinder finder(level, ResponseContentFlags_ID, context_.GetFindStorageAccessMode(), context_.GetIndex().HasFindSupport()); finder.SetDatabaseLookup(query); finder.Execute(visitor, context_); return true;
--- a/OrthancServer/Sources/ResourceFinder.cpp Mon Jan 06 17:10:09 2025 +0100 +++ b/OrthancServer/Sources/ResourceFinder.cpp Wed Jan 08 15:18:00 2025 +0100 @@ -517,7 +517,7 @@ } if (lookup_.get() != NULL && - isSimpleLookup_ && + canBeFullyPerformedInDb_ && (hasLimitsSince_ || hasLimitsCount_)) { /** @@ -544,10 +544,12 @@ ResourceFinder::ResourceFinder(ResourceType level, ResponseContentFlags responseContent, - FindStorageAccessMode storageAccessMode) : + FindStorageAccessMode storageAccessMode, + bool supportsChildExistQueries) : request_(level), databaseLimits_(0), isSimpleLookup_(true), + canBeFullyPerformedInDb_(true), pagingMode_(PagingMode_FullManual), hasLimitsSince_(false), hasLimitsCount_(false), @@ -555,6 +557,7 @@ limitsCount_(0), responseContent_(responseContent), storageAccessMode_(storageAccessMode), + supportsChildExistQueries_(supportsChildExistQueries), isWarning002Enabled_(false), isWarning004Enabled_(false), isWarning005Enabled_(false) @@ -664,7 +667,7 @@ } } - isSimpleLookup_ = registry.NormalizeLookup(request_.GetDicomTagConstraints(), lookup, request_.GetLevel()); + isSimpleLookup_ = registry.NormalizeLookup(canBeFullyPerformedInDb_, request_.GetDicomTagConstraints(), lookup, request_.GetLevel(), supportsChildExistQueries_); // "request_.GetDicomTagConstraints()" only contains constraints on main DICOM tags @@ -681,12 +684,15 @@ } else { - LOG(WARNING) << "Executing a database lookup at level " << EnumerationToString(request_.GetLevel()) - << " on main DICOM tag " << constraint.GetTag().Format() << " from an inferior level (" - << EnumerationToString(constraint.GetLevel()) << "), this will return no result"; + if (!supportsChildExistQueries_ || (constraint.GetLevel() != ResourceType_Series && constraint.GetTag() != DICOM_TAG_MODALITIES_IN_STUDY)) + { + LOG(WARNING) << "Executing a database lookup at level " << EnumerationToString(request_.GetLevel()) + << " on main DICOM tag " << constraint.GetTag().Format() << " from an inferior level (" + << EnumerationToString(constraint.GetLevel()) << "), this will return no result"; + } } - if (IsComputedTag(constraint.GetTag())) + if (IsComputedTag(constraint.GetTag()) && constraint.GetTag() != DICOM_TAG_MODALITIES_IN_STUDY) { // Sanity check throw OrthancException(ErrorCode_InternalError); @@ -1028,6 +1034,12 @@ uint64_t ResourceFinder::Count(ServerContext& context) const { + if (!canBeFullyPerformedInDb_) + { + throw OrthancException(ErrorCode_BadRequest, + "Unable to count resources when querying tags that are not stored as MainDicomTags in the Database or when using case sensitive queries."); + } + uint64_t count = 0; context.GetIndex().ExecuteCount(count, request_); return count; @@ -1039,6 +1051,13 @@ { UpdateRequestLimits(context); + if ((request_.HasLimits() && request_.GetLimitsSince() > 0) && + !canBeFullyPerformedInDb_) + { + throw OrthancException(ErrorCode_BadRequest, + "nable to use 'Since' when finding resources when querying against Dicom Tags that are not in the MainDicomTags or when using CaseSenstive queries."); + } + bool isWarning002Enabled = false; bool isWarning004Enabled = false; bool isWarning006Enabled = false;
--- a/OrthancServer/Sources/ResourceFinder.h Mon Jan 06 17:10:09 2025 +0100 +++ b/OrthancServer/Sources/ResourceFinder.h Wed Jan 08 15:18:00 2025 +0100 @@ -60,6 +60,7 @@ uint64_t databaseLimits_; std::unique_ptr<DatabaseLookup> lookup_; bool isSimpleLookup_; + bool canBeFullyPerformedInDb_; PagingMode pagingMode_; bool hasLimitsSince_; bool hasLimitsCount_; @@ -67,6 +68,7 @@ uint64_t limitsCount_; ResponseContentFlags responseContent_; FindStorageAccessMode storageAccessMode_; + bool supportsChildExistQueries_; std::set<DicomTag> requestedTags_; std::set<DicomTag> requestedComputedTags_; @@ -106,7 +108,8 @@ public: ResourceFinder(ResourceType level, ResponseContentFlags responseContent, - FindStorageAccessMode storageAccessMode); + FindStorageAccessMode storageAccessMode, + bool supportsChildExistQueries); void SetDatabaseLimits(uint64_t limits); @@ -195,5 +198,10 @@ bool includeAllMetadata); uint64_t Count(ServerContext& context) const; + + bool CanBeFullyPerformedInDb() const + { + return canBeFullyPerformedInDb_; + } }; }
--- a/OrthancServer/Sources/Search/ISqlLookupFormatter.cpp Mon Jan 06 17:10:09 2025 +0100 +++ b/OrthancServer/Sources/Search/ISqlLookupFormatter.cpp Wed Jan 08 15:18:00 2025 +0100 @@ -856,13 +856,25 @@ { std::string join; FormatJoin(join, constraint, count); - joins += join; + + if (constraint.GetLevel() <= queryLevel) + { + joins += join; + } + else if (constraint.GetLevel() == queryLevel + 1 && !comparison.empty()) + { + // new in v 1.12.6, the constraints on child tags are actually looking for one child with this value + comparison = " EXISTS (SELECT 1 FROM Resources AS " + FormatLevel(static_cast<ResourceType>(queryLevel + 1)) + + join + + " WHERE " + comparison + " AND " + + FormatLevel(static_cast<ResourceType>(queryLevel + 1)) + ".parentId = " + FormatLevel(static_cast<ResourceType>(queryLevel)) + ".internalId) "; + } if (!comparison.empty()) { comparisons += " AND " + comparison; } - + count ++; } } @@ -894,13 +906,14 @@ 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"); - } + // disabled in v 1.12.6 now that the child levels are considered as "is there at least one child that meets this constraint" + // 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(strQueryLevel + ".resourceType = " +