From 48caff30d944c5675d55f5f58a1b4ebd758f9210 Mon Sep 17 00:00:00 2001 From: JasonRoberts-smile <85363818+JasonRoberts-smile@users.noreply.github.com> Date: Thu, 20 Jan 2022 16:22:02 -0500 Subject: [PATCH] Improve performance of chained queries into contained resources (#3312) * base restructuring of query * fix unit tests * suppress unnecessary resource type parameter * pass the resource type used to fetch the search param as part of the chain, so later we do not need to guess what it was * add query structure tests * changelog * fix test failures * got one of the branches wrong in the 3-reference case --- ...chained-query-performance-improvement.yaml | 5 + .../fhir/jpa/search/builder/QueryStack.java | 537 ++++++++++++++---- .../ResourceLinkPredicateBuilder.java | 2 +- .../ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java | 4 + .../fhir/jpa/dao/r4/ChainingR4SearchTest.java | 415 +++++++++++++- .../search/SearchCoordinatorSvcImplTest.java | 7 + 6 files changed, 860 insertions(+), 110 deletions(-) create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_7_0/3312-chained-query-performance-improvement.yaml diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_7_0/3312-chained-query-performance-improvement.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_7_0/3312-chained-query-performance-improvement.yaml new file mode 100644 index 00000000000..b64dfb9f1f8 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_7_0/3312-chained-query-performance-improvement.yaml @@ -0,0 +1,5 @@ +--- +type: perf +issue: 3312 +title: "Improves the performance of the query for searching by chained search parameter +when the `Index Contained Resources` feature is enabled." diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/QueryStack.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/QueryStack.java index e4f82dcf6be..854ea77fcc6 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/QueryStack.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/QueryStack.java @@ -81,6 +81,8 @@ import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; import com.healthmarketscience.sqlbuilder.BinaryCondition; import com.healthmarketscience.sqlbuilder.ComboCondition; import com.healthmarketscience.sqlbuilder.Condition; @@ -88,7 +90,9 @@ import com.healthmarketscience.sqlbuilder.Expression; import com.healthmarketscience.sqlbuilder.InCondition; import com.healthmarketscience.sqlbuilder.OrderObject; import com.healthmarketscience.sqlbuilder.SelectQuery; +import com.healthmarketscience.sqlbuilder.SetOperationQuery; import com.healthmarketscience.sqlbuilder.Subquery; +import com.healthmarketscience.sqlbuilder.UnionQuery; import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn; import org.apache.commons.collections4.BidiMap; import org.apache.commons.collections4.bidimap.DualHashBidiMap; @@ -112,12 +116,15 @@ import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Set; import java.util.function.Supplier; import java.util.stream.Collectors; import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; +import static org.apache.commons.lang3.StringUtils.split; public class QueryStack { @@ -287,6 +294,10 @@ public class QueryStack { } private Condition createPredicateComposite(@Nullable DbColumn theSourceJoinColumn, String theResourceName, String theSpnamePrefix, RuntimeSearchParam theParamDef, List theNextAnd, RequestPartitionId theRequestPartitionId) { + return createPredicateComposite(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParamDef, theNextAnd, theRequestPartitionId, mySqlBuilder); + } + + private Condition createPredicateComposite(@Nullable DbColumn theSourceJoinColumn, String theResourceName, String theSpnamePrefix, RuntimeSearchParam theParamDef, List theNextAnd, RequestPartitionId theRequestPartitionId, SearchQueryBuilder theSqlBuilder) { Condition orCondidtion = null; for (IQueryParameterType next : theNextAnd) { @@ -299,11 +310,11 @@ public class QueryStack { List componentParams = JpaParamUtil.resolveComponentParameters(mySearchParamRegistry, theParamDef); RuntimeSearchParam left = componentParams.get(0); IQueryParameterType leftValue = cp.getLeftValue(); - Condition leftPredicate = createPredicateCompositePart(theSourceJoinColumn, theResourceName, theSpnamePrefix, left, leftValue, theRequestPartitionId); + Condition leftPredicate = createPredicateCompositePart(theSourceJoinColumn, theResourceName, theSpnamePrefix, left, leftValue, theRequestPartitionId, theSqlBuilder); RuntimeSearchParam right = componentParams.get(1); IQueryParameterType rightValue = cp.getRightValue(); - Condition rightPredicate = createPredicateCompositePart(theSourceJoinColumn, theResourceName, theSpnamePrefix, right, rightValue, theRequestPartitionId); + Condition rightPredicate = createPredicateCompositePart(theSourceJoinColumn, theResourceName, theSpnamePrefix, right, rightValue, theRequestPartitionId, theSqlBuilder); Condition andCondition = toAndPredicate(leftPredicate, rightPredicate); @@ -318,19 +329,23 @@ public class QueryStack { } private Condition createPredicateCompositePart(@Nullable DbColumn theSourceJoinColumn, String theResourceName, String theSpnamePrefix, RuntimeSearchParam theParam, IQueryParameterType theParamValue, RequestPartitionId theRequestPartitionId) { + return createPredicateCompositePart(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParam, theParamValue, theRequestPartitionId, mySqlBuilder); + } + + private Condition createPredicateCompositePart(@Nullable DbColumn theSourceJoinColumn, String theResourceName, String theSpnamePrefix, RuntimeSearchParam theParam, IQueryParameterType theParamValue, RequestPartitionId theRequestPartitionId, SearchQueryBuilder theSqlBuilder) { switch (theParam.getParamType()) { case STRING: { - return createPredicateString(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParam, Collections.singletonList(theParamValue), null, theRequestPartitionId); + return createPredicateString(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParam, Collections.singletonList(theParamValue), null, theRequestPartitionId, theSqlBuilder); } case TOKEN: { - return createPredicateToken(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParam, Collections.singletonList(theParamValue), null, theRequestPartitionId); + return createPredicateToken(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParam, Collections.singletonList(theParamValue), null, theRequestPartitionId, theSqlBuilder); } case DATE: { - return createPredicateDate(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParam, Collections.singletonList(theParamValue), toOperation(((DateParam) theParamValue).getPrefix()), theRequestPartitionId); + return createPredicateDate(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParam, Collections.singletonList(theParamValue), toOperation(((DateParam) theParamValue).getPrefix()), theRequestPartitionId, theSqlBuilder); } case QUANTITY: { - return createPredicateQuantity(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParam, Collections.singletonList(theParamValue), null, theRequestPartitionId); + return createPredicateQuantity(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParam, Collections.singletonList(theParamValue), null, theRequestPartitionId, theSqlBuilder); } case NUMBER: case REFERENCE: @@ -368,10 +383,15 @@ public class QueryStack { public Condition createPredicateDate(@Nullable DbColumn theSourceJoinColumn, String theResourceName, String theSpnamePrefix, RuntimeSearchParam theSearchParam, List theList, SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId) { + return createPredicateDate(theSourceJoinColumn, theResourceName, theSpnamePrefix, theSearchParam, theList, theOperation, theRequestPartitionId, mySqlBuilder); + } + public Condition createPredicateDate(@Nullable DbColumn theSourceJoinColumn, String theResourceName, + String theSpnamePrefix, RuntimeSearchParam theSearchParam, List theList, + SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId, SearchQueryBuilder theSqlBuilder) { String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName()); - PredicateBuilderCacheLookupResult predicateBuilderLookupResult = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.DATE, theSourceJoinColumn, paramName, () -> mySqlBuilder.addDatePredicateBuilder(theSourceJoinColumn)); + PredicateBuilderCacheLookupResult predicateBuilderLookupResult = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.DATE, theSourceJoinColumn, paramName, () -> theSqlBuilder.addDatePredicateBuilder(theSourceJoinColumn)); DatePredicateBuilder predicateBuilder = predicateBuilderLookupResult.getResult(); boolean cacheHit = predicateBuilderLookupResult.isCacheHit(); @@ -577,10 +597,16 @@ public class QueryStack { public Condition createPredicateNumber(@Nullable DbColumn theSourceJoinColumn, String theResourceName, String theSpnamePrefix, RuntimeSearchParam theSearchParam, List theList, SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId) { + return createPredicateNumber(theSourceJoinColumn, theResourceName, theSpnamePrefix, theSearchParam, theList, theOperation, theRequestPartitionId, mySqlBuilder); + } + + public Condition createPredicateNumber(@Nullable DbColumn theSourceJoinColumn, String theResourceName, + String theSpnamePrefix, RuntimeSearchParam theSearchParam, List theList, + SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId, SearchQueryBuilder theSqlBuilder) { String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName()); - NumberPredicateBuilder join = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.NUMBER, theSourceJoinColumn, paramName, () -> mySqlBuilder.addNumberPredicateBuilder(theSourceJoinColumn)).getResult(); + NumberPredicateBuilder join = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.NUMBER, theSourceJoinColumn, paramName, () -> theSqlBuilder.addNumberPredicateBuilder(theSourceJoinColumn)).getResult(); if (theList.get(0).getMissing() != null) { return join.createPredicateParamMissingForNonReference(theResourceName, paramName, theList.get(0).getMissing(), theRequestPartitionId); @@ -618,11 +644,17 @@ public class QueryStack { public Condition createPredicateQuantity(@Nullable DbColumn theSourceJoinColumn, String theResourceName, String theSpnamePrefix, RuntimeSearchParam theSearchParam, List theList, SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId) { + return createPredicateQuantity(theSourceJoinColumn, theResourceName, theSpnamePrefix, theSearchParam, theList, theOperation, theRequestPartitionId, mySqlBuilder); + } + + public Condition createPredicateQuantity(@Nullable DbColumn theSourceJoinColumn, String theResourceName, + String theSpnamePrefix, RuntimeSearchParam theSearchParam, List theList, + SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId, SearchQueryBuilder theSqlBuilder) { String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName()); if (theList.get(0).getMissing() != null) { - QuantityBasePredicateBuilder join = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.QUANTITY, theSourceJoinColumn, theSearchParam.getName(), () -> mySqlBuilder.addQuantityPredicateBuilder(theSourceJoinColumn)).getResult(); + QuantityBasePredicateBuilder join = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.QUANTITY, theSourceJoinColumn, theSearchParam.getName(), () -> theSqlBuilder.addQuantityPredicateBuilder(theSourceJoinColumn)).getResult(); return join.createPredicateParamMissingForNonReference(theResourceName, paramName, theList.get(0).getMissing(), theRequestPartitionId); } @@ -641,13 +673,13 @@ public class QueryStack { .collect(Collectors.toList()); if (normalizedQuantityParams.size() == quantityParams.size()) { - join = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.QUANTITY, theSourceJoinColumn, paramName, () -> mySqlBuilder.addQuantityNormalizedPredicateBuilder(theSourceJoinColumn)).getResult(); + join = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.QUANTITY, theSourceJoinColumn, paramName, () -> theSqlBuilder.addQuantityNormalizedPredicateBuilder(theSourceJoinColumn)).getResult(); quantityParams = normalizedQuantityParams; } } if (join == null) { - join = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.QUANTITY, theSourceJoinColumn, paramName, () -> mySqlBuilder.addQuantityPredicateBuilder(theSourceJoinColumn)).getResult(); + join = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.QUANTITY, theSourceJoinColumn, paramName, () -> theSqlBuilder.addQuantityPredicateBuilder(theSourceJoinColumn)).getResult(); } List codePredicates = new ArrayList<>(); @@ -667,6 +699,17 @@ public class QueryStack { SearchFilterParser.CompareOperation theOperation, RequestDetails theRequest, RequestPartitionId theRequestPartitionId) { + return createPredicateReference(theSourceJoinColumn, theResourceName, theParamName, theQualifiers, theList, theOperation, theRequest, theRequestPartitionId, mySqlBuilder); + } + + public Condition createPredicateReference(@Nullable DbColumn theSourceJoinColumn, + String theResourceName, + String theParamName, + List theQualifiers, + List theList, + SearchFilterParser.CompareOperation theOperation, + RequestDetails theRequest, + RequestPartitionId theRequestPartitionId, SearchQueryBuilder theSqlBuilder) { if ((theOperation != null) && (theOperation != SearchFilterParser.CompareOperation.eq) && @@ -675,140 +718,407 @@ public class QueryStack { } if (theList.get(0).getMissing() != null) { - SearchParamPresentPredicateBuilder join = mySqlBuilder.addSearchParamPresentPredicateBuilder(theSourceJoinColumn); + SearchParamPresentPredicateBuilder join = theSqlBuilder.addSearchParamPresentPredicateBuilder(theSourceJoinColumn); return join.createPredicateParamMissingForReference(theResourceName, theParamName, theList.get(0).getMissing(), theRequestPartitionId); } - ResourceLinkPredicateBuilder predicateBuilder = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.REFERENCE, theSourceJoinColumn, theParamName, () -> mySqlBuilder.addReferencePredicateBuilder(this, theSourceJoinColumn)).getResult(); + ResourceLinkPredicateBuilder predicateBuilder = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.REFERENCE, theSourceJoinColumn, theParamName, () -> theSqlBuilder.addReferencePredicateBuilder(this, theSourceJoinColumn)).getResult(); return predicateBuilder.createPredicate(theRequest, theResourceName, theParamName, theQualifiers, theList, theOperation, theRequestPartitionId); } + private class ChainElement { + private final String myResourceType; + private final RuntimeSearchParam mySearchParam; + + public ChainElement(String theResourceType, RuntimeSearchParam theSearchParam) { + this.myResourceType = theResourceType; + this.mySearchParam = theSearchParam; + } + + public String getResourceType() { + return myResourceType; + } + + public RuntimeSearchParam getSearchParam() { + return mySearchParam; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ChainElement that = (ChainElement) o; + return myResourceType.equals(that.myResourceType) && mySearchParam.equals(that.mySearchParam); + } + + @Override + public int hashCode() { + return Objects.hash(myResourceType, mySearchParam); + } + } + + private class ReferenceChainExtractor { + private final Map,Set> myChains = Maps.newHashMap(); + + public Map,Set> getChains() { return myChains; } + + private boolean isReferenceParamValid(ReferenceParam theReferenceParam) { + return split(theReferenceParam.getChain(), '.').length <= 3; + } + + public void deriveChains(String theResourceType, RuntimeSearchParam theSearchParam, List theList) { + List searchParams = Lists.newArrayList(); + searchParams.add(new ChainElement(theResourceType, theSearchParam)); + for (IQueryParameterType nextOr : theList) { + String targetValue = nextOr.getValueAsQueryToken(myFhirContext); + if (nextOr instanceof ReferenceParam) { + ReferenceParam referenceParam = (ReferenceParam) nextOr; + + if (!isReferenceParamValid(referenceParam)) { + throw new InvalidRequestException( + "The search chain " + theSearchParam.getName() + "." + referenceParam.getChain() + + " is too long. Only chains up to three references are supported."); + } + + String targetChain = referenceParam.getChain(); + List qualifiers = Lists.newArrayList(referenceParam.getResourceType()); + + processNextLinkInChain(searchParams, theSearchParam, targetChain, targetValue, qualifiers, referenceParam.getResourceType()); + + } + } + } + + private void processNextLinkInChain(List theSearchParams, RuntimeSearchParam thePreviousSearchParam, String theChain, String theTargetValue, List theQualifiers, String theResourceType) { + + String nextParamName = theChain; + String nextChain = null; + String nextQualifier = null; + int linkIndex = theChain.indexOf('.'); + if (linkIndex != -1) { + nextParamName = theChain.substring(0, linkIndex); + nextChain = theChain.substring(linkIndex+1); + } + + int qualifierIndex = nextParamName.indexOf(':'); + if (qualifierIndex != -1) { + nextParamName = nextParamName.substring(0, qualifierIndex); + nextQualifier = nextParamName.substring(qualifierIndex); + } + + List qualifiersBranch = Lists.newArrayList(); + qualifiersBranch.addAll(theQualifiers); + qualifiersBranch.add(nextQualifier); + + boolean searchParamFound = false; + for (String nextTarget : thePreviousSearchParam.getTargets()) { + RuntimeSearchParam nextSearchParam = null; + if (StringUtils.isBlank(theResourceType) || theResourceType.equals(nextTarget)) { + nextSearchParam = mySearchParamRegistry.getActiveSearchParam(nextTarget, nextParamName); + } + if (nextSearchParam != null) { + searchParamFound = true; + // If we find a search param on this resource type for this parameter name, keep iterating + // Otherwise, abandon this branch and carry on to the next one + List searchParamBranch = Lists.newArrayList(); + searchParamBranch.addAll(theSearchParams); + + if (StringUtils.isEmpty(nextChain)) { + // We've reached the end of the chain + ArrayList orValues = Lists.newArrayList(); + + if (RestSearchParameterTypeEnum.REFERENCE.equals(nextSearchParam.getParamType())) { + orValues.add(new ReferenceParam(nextQualifier, "", theTargetValue)); + } else { + IQueryParameterType qp = toParameterType(nextSearchParam); + qp.setValueAsQueryToken(myFhirContext, nextSearchParam.getName(), null, theTargetValue); + orValues.add(qp); + } + + Set leafNodes = myChains.get(searchParamBranch); + if (leafNodes == null) { + leafNodes = Sets.newHashSet(); + myChains.put(searchParamBranch, leafNodes); + } + leafNodes.add(new LeafNodeDefinition(nextSearchParam, orValues, nextTarget, nextParamName, "", qualifiersBranch)); + } else { + + searchParamBranch.add(new ChainElement(nextTarget, nextSearchParam)); + processNextLinkInChain(searchParamBranch, nextSearchParam, nextChain, theTargetValue, qualifiersBranch, nextQualifier); + } + } + } + if (!searchParamFound) { + throw new InvalidRequestException(myFhirContext.getLocalizer().getMessage(BaseStorageDao.class, "invalidParameterChain", thePreviousSearchParam.getName() + '.' + theChain)); + } + } + } + + private static class LeafNodeDefinition { + private final RuntimeSearchParam myParamDefinition; + private final ArrayList myOrValues; + private final String myLeafTarget; + private final String myLeafParamName; + private final String myLeafPathPrefix; + private final List myQualifiers; + + public LeafNodeDefinition(RuntimeSearchParam theParamDefinition, ArrayList theOrValues, String theLeafTarget, String theLeafParamName, String theLeafPathPrefix, List theQualifiers) { + myParamDefinition = theParamDefinition; + myOrValues = theOrValues; + myLeafTarget = theLeafTarget; + myLeafParamName = theLeafParamName; + myLeafPathPrefix = theLeafPathPrefix; + myQualifiers = theQualifiers; + } + + public RuntimeSearchParam getParamDefinition() { + return myParamDefinition; + } + + public ArrayList getOrValues() { + return myOrValues; + } + + public String getLeafTarget() { + return myLeafTarget; + } + + public String getLeafParamName() { + return myLeafParamName; + } + + public String getLeafPathPrefix() { + return myLeafPathPrefix; + } + + public List getQualifiers() { + return myQualifiers; + } + + public LeafNodeDefinition withPathPrefix(String theResourceType, String theName) { + return new LeafNodeDefinition(myParamDefinition, myOrValues, theResourceType, myLeafParamName, theName, myQualifiers); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + LeafNodeDefinition that = (LeafNodeDefinition) o; + return Objects.equals(myParamDefinition, that.myParamDefinition) && Objects.equals(myOrValues, that.myOrValues) && Objects.equals(myLeafTarget, that.myLeafTarget) && Objects.equals(myLeafParamName, that.myLeafParamName) && Objects.equals(myLeafPathPrefix, that.myLeafPathPrefix) && Objects.equals(myQualifiers, that.myQualifiers); + } + + @Override + public int hashCode() { + return Objects.hash(myParamDefinition, myOrValues, myLeafTarget, myLeafParamName, myLeafPathPrefix, myQualifiers); + } + } + public Condition createPredicateReferenceForContainedResource(@Nullable DbColumn theSourceJoinColumn, String theResourceName, String theParamName, List theQualifiers, RuntimeSearchParam theSearchParam, List theList, SearchFilterParser.CompareOperation theOperation, RequestDetails theRequest, RequestPartitionId theRequestPartitionId) { + // A bit of a hack, but we need to turn off cache reuse while in this method so that we don't try to reuse builders across different subselects + EnumSet cachedReusePredicateBuilderTypes = EnumSet.copyOf(myReusePredicateBuilderTypes); + myReusePredicateBuilderTypes.clear(); - String spnamePrefix = theParamName; + UnionQuery union = new UnionQuery(SetOperationQuery.Type.UNION); - String targetChain = null; - String targetParamName = null; - String headQualifier = null; - String targetQualifier = null; - String targetValue = null; + ReferenceChainExtractor chainExtractor = new ReferenceChainExtractor(); + chainExtractor.deriveChains(theResourceName, theSearchParam, theList); + Map,Set> chains = chainExtractor.getChains(); - RuntimeSearchParam targetParamDefinition = null; + Map,Set> referenceLinks = Maps.newHashMap(); + for (List nextChain : chains.keySet()) { + Set leafNodes = chains.get(nextChain); - ArrayList orValues = Lists.newArrayList(); - List trimmedParameters = Lists.newArrayList(); - IQueryParameterType qp = null; + collateChainedSearchOptions(referenceLinks, nextChain, leafNodes); + } - for (int orIdx = 0; orIdx < theList.size(); orIdx++) { + for (List nextReferenceLink: referenceLinks.keySet()) { + for (LeafNodeDefinition leafNodeDefinition : referenceLinks.get(nextReferenceLink)) { + SearchQueryBuilder builder = mySqlBuilder.newChildSqlBuilder(); + DbColumn previousJoinColumn = null; - IQueryParameterType nextOr = theList.get(orIdx); - - if (nextOr instanceof ReferenceParam) { - - ReferenceParam referenceParam = (ReferenceParam) nextOr; - - // 1. Find out the parameter, qualifier and the value - targetChain = referenceParam.getChain(); - targetParamName = targetChain; - targetValue = nextOr.getValueAsQueryToken(myFhirContext); - headQualifier = referenceParam.getResourceType(); - - String targetNextChain = null; - int linkIndex = targetChain.indexOf('.'); - if (linkIndex != -1) { - targetParamName = targetChain.substring(0, linkIndex); - targetNextChain = targetChain.substring(linkIndex+1); + // Create a reference link predicate to the subselect for every link but the last one + for (String nextLink : nextReferenceLink) { + // We don't want to call createPredicateReference() here, because the whole point is to avoid the recursion. + // TODO: Are we missing any important business logic from that method? All tests are passing. + ResourceLinkPredicateBuilder resourceLinkPredicateBuilder = builder.addReferencePredicateBuilder(this, previousJoinColumn); + builder.addPredicate(resourceLinkPredicateBuilder.createPredicateSourcePaths(Lists.newArrayList(nextLink))); + previousJoinColumn = resourceLinkPredicateBuilder.getColumnTargetResourceId(); } - int qualifierIndex = targetParamName.indexOf(':'); - if (qualifierIndex != -1) { - targetParamName = targetParamName.substring(0, qualifierIndex); - targetQualifier = targetParamName.substring(qualifierIndex); - } - trimmedParameters.add(new ReferenceParam(targetQualifier, targetNextChain, referenceParam.getValue())); + Condition containedCondition = createIndexPredicate( + previousJoinColumn, + leafNodeDefinition.getLeafTarget(), + leafNodeDefinition.getLeafPathPrefix(), + leafNodeDefinition.getLeafParamName(), + leafNodeDefinition.getParamDefinition(), + leafNodeDefinition.getOrValues(), + theOperation, + leafNodeDefinition.getQualifiers(), + theRequest, + theRequestPartitionId, + builder); - // 2. find out the data type - if (targetParamDefinition == null) { - for (String nextTarget : theSearchParam.getTargets()) { - if (!referenceParam.hasResourceType() || referenceParam.getResourceType().equals(nextTarget)) { - targetParamDefinition = mySearchParamRegistry.getActiveSearchParam(nextTarget, targetParamName); - } - if (targetParamDefinition != null) - break; - } - } + builder.addPredicate(containedCondition); - if (targetParamDefinition == null) { - throw new InvalidRequestException("Unknown search parameter name: " + theSearchParam.getName() + '.' + targetParamName + "."); - } - - if (RestSearchParameterTypeEnum.REFERENCE.equals(targetParamDefinition.getParamType())) { - continue; - } - - qp = toParameterType(targetParamDefinition); - qp.setValueAsQueryToken(myFhirContext, targetParamName, targetQualifier, targetValue); - orValues.add(qp); + union.addQueries(builder.getSelect()); } } - if (targetParamDefinition == null) { - throw new InvalidRequestException("Unknown search parameter name: " + theSearchParam.getName() + "."); + InCondition inCondition; + if (theSourceJoinColumn == null) { + inCondition = new InCondition(mySqlBuilder.getOrCreateFirstPredicateBuilder(false).getResourceIdColumn(), union); + } else { + //-- for the resource link, need join with target_resource_id + inCondition = new InCondition(theSourceJoinColumn, union); } - theQualifiers.add(headQualifier); + // restore the state of this collection to turn caching back on before we exit + myReusePredicateBuilderTypes.addAll(cachedReusePredicateBuilderTypes); + return inCondition; + } - // 3. create the query + private void collateChainedSearchOptions(Map, Set> referenceLinks, List nextChain, Set leafNodes) { + // Manually collapse the chain using all possible variants of contained resource patterns. + // This is a bit excruciating to extend beyond three references. Do we want to find a way to automate this someday? + // Note: the first element in each chain is assumed to be discrete. This may need to change when we add proper support for `_contained` + if (nextChain.size() == 1) { + // discrete -> discrete + updateMapOfReferenceLinks(referenceLinks, Lists.newArrayList(nextChain.get(0).getSearchParam().getPath()), leafNodes); + // discrete -> contained + updateMapOfReferenceLinks(referenceLinks, Lists.newArrayList(), + leafNodes + .stream() + .map(t -> t.withPathPrefix(nextChain.get(0).getResourceType(), nextChain.get(0).getSearchParam().getName())) + .collect(Collectors.toSet())); + } else if (nextChain.size() == 2) { + // discrete -> discrete -> discrete + updateMapOfReferenceLinks(referenceLinks, Lists.newArrayList(nextChain.get(0).getSearchParam().getPath(), nextChain.get(1).getSearchParam().getPath()), leafNodes); + // discrete -> discrete -> contained + updateMapOfReferenceLinks(referenceLinks, Lists.newArrayList(nextChain.get(0).getSearchParam().getPath()), + leafNodes + .stream() + .map(t -> t.withPathPrefix(nextChain.get(1).getResourceType(), nextChain.get(1).getSearchParam().getName())) + .collect(Collectors.toSet())); + // discrete -> contained -> discrete + updateMapOfReferenceLinks(referenceLinks, Lists.newArrayList(mergePaths(nextChain.get(0).getSearchParam().getPath(), nextChain.get(1).getSearchParam().getPath())), leafNodes); + if (myModelConfig.isIndexOnContainedResourcesRecursively()) { + // discrete -> contained -> contained + updateMapOfReferenceLinks(referenceLinks, Lists.newArrayList(), + leafNodes + .stream() + .map(t -> t.withPathPrefix(nextChain.get(0).getResourceType(), nextChain.get(0).getSearchParam().getName() + "." + nextChain.get(1).getSearchParam().getName())) + .collect(Collectors.toSet())); + } + } else if (nextChain.size() == 3) { + // discrete -> discrete -> discrete -> discrete + updateMapOfReferenceLinks(referenceLinks, Lists.newArrayList(nextChain.get(0).getSearchParam().getPath(), nextChain.get(1).getSearchParam().getPath(), nextChain.get(2).getSearchParam().getPath()), leafNodes); + // discrete -> discrete -> discrete -> contained + updateMapOfReferenceLinks(referenceLinks, Lists.newArrayList(nextChain.get(0).getSearchParam().getPath(), nextChain.get(1).getSearchParam().getPath()), + leafNodes + .stream() + .map(t -> t.withPathPrefix(nextChain.get(2).getResourceType(), nextChain.get(2).getSearchParam().getName())) + .collect(Collectors.toSet())); + // discrete -> discrete -> contained -> discrete + updateMapOfReferenceLinks(referenceLinks, Lists.newArrayList(nextChain.get(0).getSearchParam().getPath(), mergePaths(nextChain.get(1).getSearchParam().getPath(), nextChain.get(2).getSearchParam().getPath())), leafNodes); + // discrete -> contained -> discrete -> discrete + updateMapOfReferenceLinks(referenceLinks, Lists.newArrayList(mergePaths(nextChain.get(0).getSearchParam().getPath(), nextChain.get(1).getSearchParam().getPath()), nextChain.get(2).getSearchParam().getPath()), leafNodes); + // discrete -> contained -> discrete -> contained + updateMapOfReferenceLinks(referenceLinks, Lists.newArrayList(mergePaths(nextChain.get(0).getSearchParam().getPath(), nextChain.get(1).getSearchParam().getPath())), + leafNodes + .stream() + .map(t -> t.withPathPrefix(nextChain.get(2).getResourceType(), nextChain.get(2).getSearchParam().getName())) + .collect(Collectors.toSet())); + if (myModelConfig.isIndexOnContainedResourcesRecursively()) { + // discrete -> contained -> contained -> discrete + updateMapOfReferenceLinks(referenceLinks, Lists.newArrayList(mergePaths(nextChain.get(0).getSearchParam().getPath(), nextChain.get(1).getSearchParam().getPath(), nextChain.get(2).getSearchParam().getPath())), leafNodes); + // discrete -> discrete -> contained -> contained + updateMapOfReferenceLinks(referenceLinks, Lists.newArrayList(nextChain.get(0).getSearchParam().getPath()), + leafNodes + .stream() + .map(t -> t.withPathPrefix(nextChain.get(1).getResourceType(), nextChain.get(1).getSearchParam().getName() + "." + nextChain.get(2).getSearchParam().getName())) + .collect(Collectors.toSet())); + // discrete -> contained -> contained -> contained + updateMapOfReferenceLinks(referenceLinks, Lists.newArrayList(), + leafNodes + .stream() + .map(t -> t.withPathPrefix(nextChain.get(0).getResourceType(), nextChain.get(0).getSearchParam().getName() + "." + nextChain.get(1).getSearchParam().getName() + "." + nextChain.get(2).getSearchParam().getName())) + .collect(Collectors.toSet())); + } + } else { + // TODO: the chain is too long, it isn't practical to hard-code all the possible patterns. If anyone ever needs this, we should revisit the approach + throw new InvalidRequestException( + "The search chain is too long. Only chains of up to three references are supported."); + } + } + + private void updateMapOfReferenceLinks(Map, Set> theReferenceLinksMap, ArrayList thePath, Set theLeafNodesToAdd) { + Set leafNodes = theReferenceLinksMap.get(thePath); + if (leafNodes == null) { + leafNodes = Sets.newHashSet(); + theReferenceLinksMap.put(thePath, leafNodes); + } + leafNodes.addAll(theLeafNodesToAdd); + } + + private String mergePaths(String... paths) { + String result = ""; + for (String nextPath : paths) { + int separatorIndex = nextPath.indexOf('.'); + if (StringUtils.isEmpty(result)) { + result = nextPath; + } else { + result = result + nextPath.substring(separatorIndex); + } + } + return result; + } + + private Condition createIndexPredicate(DbColumn theSourceJoinColumn, String theResourceName, String theSpnamePrefix, String theParamName, RuntimeSearchParam theParamDefinition, ArrayList theOrValues, SearchFilterParser.CompareOperation theOperation, List theQualifiers, RequestDetails theRequest, RequestPartitionId theRequestPartitionId, SearchQueryBuilder theSqlBuilder) { Condition containedCondition = null; - switch (targetParamDefinition.getParamType()) { + switch (theParamDefinition.getParamType()) { case DATE: - containedCondition = createPredicateDate(theSourceJoinColumn, theResourceName, spnamePrefix, targetParamDefinition, - orValues, theOperation, theRequestPartitionId); + containedCondition = createPredicateDate(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParamDefinition, + theOrValues, theOperation, theRequestPartitionId, theSqlBuilder); break; case NUMBER: - containedCondition = createPredicateNumber(theSourceJoinColumn, theResourceName, spnamePrefix, targetParamDefinition, - orValues, theOperation, theRequestPartitionId); + containedCondition = createPredicateNumber(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParamDefinition, + theOrValues, theOperation, theRequestPartitionId, theSqlBuilder); break; case QUANTITY: - containedCondition = createPredicateQuantity(theSourceJoinColumn, theResourceName, spnamePrefix, targetParamDefinition, - orValues, theOperation, theRequestPartitionId); + containedCondition = createPredicateQuantity(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParamDefinition, + theOrValues, theOperation, theRequestPartitionId, theSqlBuilder); break; case STRING: - containedCondition = createPredicateString(theSourceJoinColumn, theResourceName, spnamePrefix, targetParamDefinition, - orValues, theOperation, theRequestPartitionId); + containedCondition = createPredicateString(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParamDefinition, + theOrValues, theOperation, theRequestPartitionId, theSqlBuilder); break; case TOKEN: - containedCondition = createPredicateToken(theSourceJoinColumn, theResourceName, spnamePrefix, targetParamDefinition, - orValues, theOperation, theRequestPartitionId); + containedCondition = createPredicateToken(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParamDefinition, + theOrValues, theOperation, theRequestPartitionId, theSqlBuilder); break; case COMPOSITE: - containedCondition = createPredicateComposite(theSourceJoinColumn, theResourceName, spnamePrefix, targetParamDefinition, - orValues, theRequestPartitionId); + containedCondition = createPredicateComposite(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParamDefinition, + theOrValues, theRequestPartitionId, theSqlBuilder); break; case URI: - containedCondition = createPredicateUri(theSourceJoinColumn, theResourceName, spnamePrefix, targetParamDefinition, - orValues, theOperation, theRequest, theRequestPartitionId); + containedCondition = createPredicateUri(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParamDefinition, + theOrValues, theOperation, theRequest, theRequestPartitionId, theSqlBuilder); break; case REFERENCE: - String chainedParamName = theParamName + "." + targetParamName; - containedCondition = createPredicateReference(theSourceJoinColumn, theResourceName, chainedParamName, theQualifiers, trimmedParameters, theOperation, theRequest, theRequestPartitionId); - if (myModelConfig.isIndexOnContainedResourcesRecursively()) { - containedCondition = toOrPredicate(containedCondition, - createPredicateReferenceForContainedResource(theSourceJoinColumn, theResourceName, chainedParamName, theQualifiers, theSearchParam, trimmedParameters, theOperation, theRequest, theRequestPartitionId)); - } + containedCondition = createPredicateReference(theSourceJoinColumn, theResourceName, StringUtils.isBlank(theSpnamePrefix) ? theParamName : theSpnamePrefix + "." + theParamName, theQualifiers, + theOrValues, theOperation, theRequest, theRequestPartitionId, theSqlBuilder); break; case HAS: case SPECIAL: default: throw new InvalidRequestException( - "The search type:" + targetParamDefinition.getParamType() + " is not supported."); + "The search type:" + theParamDefinition.getParamType() + " is not supported."); } - return containedCondition; } @@ -857,10 +1167,17 @@ public class QueryStack { public Condition createPredicateString(@Nullable DbColumn theSourceJoinColumn, String theResourceName, String theSpnamePrefix, RuntimeSearchParam theSearchParam, List theList, SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId) { + return createPredicateString(theSourceJoinColumn, theResourceName, theSpnamePrefix, theSearchParam, theList, theOperation, theRequestPartitionId, mySqlBuilder); + } + + public Condition createPredicateString(@Nullable DbColumn theSourceJoinColumn, String theResourceName, + String theSpnamePrefix, RuntimeSearchParam theSearchParam, List theList, + SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId, + SearchQueryBuilder theSqlBuilder) { String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName()); - StringPredicateBuilder join = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.STRING, theSourceJoinColumn, paramName, () -> mySqlBuilder.addStringPredicateBuilder(theSourceJoinColumn)).getResult(); + StringPredicateBuilder join = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.STRING, theSourceJoinColumn, paramName, () -> theSqlBuilder.addStringPredicateBuilder(theSourceJoinColumn)).getResult(); if (theList.get(0).getMissing() != null) { return join.createPredicateParamMissingForNonReference(theResourceName, paramName, theList.get(0).getMissing(), theRequestPartitionId); @@ -967,6 +1284,12 @@ public class QueryStack { public Condition createPredicateToken(@Nullable DbColumn theSourceJoinColumn, String theResourceName, String theSpnamePrefix, RuntimeSearchParam theSearchParam, List theList, SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId) { + return createPredicateToken(theSourceJoinColumn, theResourceName, theSpnamePrefix, theSearchParam, theList, theOperation, theRequestPartitionId, mySqlBuilder); + } + + public Condition createPredicateToken(@Nullable DbColumn theSourceJoinColumn, String theResourceName, + String theSpnamePrefix, RuntimeSearchParam theSearchParam, List theList, + SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId, SearchQueryBuilder theSqlBuilder) { List tokens = new ArrayList<>(); @@ -991,7 +1314,7 @@ public class QueryStack { throw new MethodNotAllowedException(msg); } - return createPredicateString(theSourceJoinColumn, theResourceName, theSpnamePrefix, theSearchParam, theList, null, theRequestPartitionId); + return createPredicateString(theSourceJoinColumn, theResourceName, theSpnamePrefix, theSearchParam, theList, null, theRequestPartitionId, theSqlBuilder); } modifier = id.getModifier(); @@ -1017,13 +1340,13 @@ public class QueryStack { BaseJoiningPredicateBuilder join; if (paramInverted) { - SearchQueryBuilder sqlBuilder = mySqlBuilder.newChildSqlBuilder(); + SearchQueryBuilder sqlBuilder = theSqlBuilder.newChildSqlBuilder(); TokenPredicateBuilder tokenSelector = sqlBuilder.addTokenPredicateBuilder(null); sqlBuilder.addPredicate(tokenSelector.createPredicateToken(tokens, theResourceName, theSpnamePrefix, theSearchParam, theRequestPartitionId)); SelectQuery sql = sqlBuilder.getSelect(); Expression subSelect = new Subquery(sql); - join = mySqlBuilder.getOrCreateFirstPredicateBuilder(); + join = theSqlBuilder.getOrCreateFirstPredicateBuilder(); if (theSourceJoinColumn == null) { predicate = new InCondition(join.getResourceIdColumn(), subSelect).setNegate(true); @@ -1034,7 +1357,7 @@ public class QueryStack { } else { - TokenPredicateBuilder tokenJoin = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.TOKEN, theSourceJoinColumn, paramName, () -> mySqlBuilder.addTokenPredicateBuilder(theSourceJoinColumn)).getResult(); + TokenPredicateBuilder tokenJoin = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.TOKEN, theSourceJoinColumn, paramName, () -> theSqlBuilder.addTokenPredicateBuilder(theSourceJoinColumn)).getResult(); if (theList.get(0).getMissing() != null) { return tokenJoin.createPredicateParamMissingForNonReference(theResourceName, paramName, theList.get(0).getMissing(), theRequestPartitionId); @@ -1051,10 +1374,17 @@ public class QueryStack { String theSpnamePrefix, RuntimeSearchParam theSearchParam, List theList, SearchFilterParser.CompareOperation theOperation, RequestDetails theRequestDetails, RequestPartitionId theRequestPartitionId) { + return createPredicateUri(theSourceJoinColumn, theResourceName, theSpnamePrefix, theSearchParam, theList, theOperation, theRequestDetails, theRequestPartitionId, mySqlBuilder); + } + + public Condition createPredicateUri(@Nullable DbColumn theSourceJoinColumn, String theResourceName, + String theSpnamePrefix, RuntimeSearchParam theSearchParam, List theList, + SearchFilterParser.CompareOperation theOperation, RequestDetails theRequestDetails, + RequestPartitionId theRequestPartitionId, SearchQueryBuilder theSqlBuilder) { String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName()); - UriPredicateBuilder join = mySqlBuilder.addUriPredicateBuilder(theSourceJoinColumn); + UriPredicateBuilder join = theSqlBuilder.addUriPredicateBuilder(theSourceJoinColumn); if (theList.get(0).getMissing() != null) { return join.createPredicateParamMissingForNonReference(theResourceName, paramName, theList.get(0).getMissing(), theRequestPartitionId); @@ -1140,10 +1470,7 @@ public class QueryStack { case REFERENCE: for (List nextAnd : theAndOrParams) { if (isEligibleForContainedResourceSearch(nextAnd)) { - andPredicates.add(toOrPredicate( - createPredicateReference(theSourceJoinColumn, theResourceName, theParamName, new ArrayList<>(), nextAnd, null, theRequest, theRequestPartitionId), - createPredicateReferenceForContainedResource(theSourceJoinColumn, theResourceName, theParamName, new ArrayList<>(), nextParamDef, nextAnd, null, theRequest, theRequestPartitionId) - )); + andPredicates.add(createPredicateReferenceForContainedResource(theSourceJoinColumn, theResourceName, theParamName, new ArrayList<>(), nextParamDef, nextAnd, null, theRequest, theRequestPartitionId)); } else { andPredicates.add(createPredicateReference(theSourceJoinColumn, theResourceName, theParamName, new ArrayList<>(), nextAnd, null, theRequest, theRequestPartitionId)); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/ResourceLinkPredicateBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/ResourceLinkPredicateBuilder.java index 092f3eb8851..a68f3c89b2a 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/ResourceLinkPredicateBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/ResourceLinkPredicateBuilder.java @@ -265,7 +265,7 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder { } @Nonnull - private Condition createPredicateSourcePaths(List thePathsToMatch) { + public Condition createPredicateSourcePaths(List thePathsToMatch) { return toEqualToOrInPredicate(myColumnSrcPath, generatePlaceholders(thePathsToMatch)); } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java index 01c42a9aa91..d5434c6c63b 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java @@ -140,6 +140,7 @@ import org.hl7.fhir.r4.model.NamingSystem; import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.OperationDefinition; import org.hl7.fhir.r4.model.Organization; +import org.hl7.fhir.r4.model.OrganizationAffiliation; import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Practitioner; import org.hl7.fhir.r4.model.PractitionerRole; @@ -370,6 +371,9 @@ public abstract class BaseJpaR4Test extends BaseJpaTest implements ITestDataBuil @Qualifier("myOrganizationDaoR4") protected IFhirResourceDao myOrganizationDao; @Autowired + @Qualifier("myOrganizationAffiliationDaoR4") + protected IFhirResourceDao myOrganizationAffiliationDao; + @Autowired protected DatabaseBackedPagingProvider myPagingProvider; @Autowired @Qualifier("myBinaryDaoR4") diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/ChainingR4SearchTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/ChainingR4SearchTest.java index cbc405309c6..069c2f56f62 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/ChainingR4SearchTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/ChainingR4SearchTest.java @@ -5,8 +5,10 @@ import ca.uhn.fhir.jpa.model.entity.ModelConfig; import ca.uhn.fhir.jpa.searchparam.MatchUrlService; import ca.uhn.fhir.jpa.searchparam.ResourceSearch; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.jpa.util.SqlQuery; import ca.uhn.fhir.parser.StrictErrorHandler; import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.Device; import org.hl7.fhir.r4.model.IdType; @@ -25,9 +27,11 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +import static org.apache.commons.lang3.StringUtils.countMatches; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; public class ChainingR4SearchTest extends BaseJpaR4Test { @@ -46,7 +50,6 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { myDaoConfig.setAllowContainsSearches(new DaoConfig().isAllowContainsSearches()); myDaoConfig.setIndexMissingFields(new DaoConfig().getIndexMissingFields()); - myModelConfig.setIndexOnContainedResources(false); myModelConfig.setIndexOnContainedResources(new ModelConfig().isIndexOnContainedResources()); myModelConfig.setIndexOnContainedResourcesRecursively(new ModelConfig().isIndexOnContainedResourcesRecursively()); } @@ -57,12 +60,11 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { myDaoConfig.setAllowMultipleDelete(true); myDaoConfig.setSearchPreFetchThresholds(new DaoConfig().getSearchPreFetchThresholds()); - myModelConfig.setIndexOnContainedResources(true); myDaoConfig.setReuseCachedSearchResultsForMillis(null); } @Test - public void testShouldResolveATwoLinkChainWithStandAloneResources() throws Exception { + public void testShouldResolveATwoLinkChainWithStandAloneResourcesWithoutContainedResourceIndexing() throws Exception { // setup IIdType oid1; @@ -78,6 +80,43 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { obs.getSubject().setReference(p.getId()); oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); + + // Create a dummy record so that an unconstrained query doesn't pass the test due to returning the only record + myObservationDao.create(new Observation(), mySrd); + } + + String url = "/Observation?subject.name=Smith"; + + // execute + List oids = searchAndReturnUnqualifiedVersionlessIdValues(url); + + // validate + assertEquals(1L, oids.size()); + assertThat(oids, contains(oid1.getIdPart())); + } + + @Test + public void testShouldResolveATwoLinkChainWithStandAloneResources() throws Exception { + + // setup + myModelConfig.setIndexOnContainedResources(true); + + IIdType oid1; + + { + Patient p = new Patient(); + p.setId(IdType.newRandomUuid()); + p.addName().setFamily("Smith").addGiven("John"); + myPatientDao.create(p, mySrd); + + Observation obs = new Observation(); + obs.getCode().setText("Observation 1"); + obs.getSubject().setReference(p.getId()); + + oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); + + // Create a dummy record so that an unconstrained query doesn't pass the test due to returning the only record + myObservationDao.create(new Observation(), mySrd); } String url = "/Observation?subject.name=Smith"; @@ -93,6 +132,8 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { @Test public void testShouldResolveATwoLinkChainWithAContainedResource() throws Exception { // setup + myModelConfig.setIndexOnContainedResources(true); + IIdType oid1; { @@ -107,6 +148,11 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { obs.getSubject().setReference("#pat"); oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); + + // Create a dummy record so that an unconstrained query doesn't pass the test due to returning the only record + myCaptureQueriesListener.clear(); + myObservationDao.create(new Observation(), mySrd); + myCaptureQueriesListener.logInsertQueries(); } String url = "/Observation?subject.name=Smith"; @@ -119,12 +165,45 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { assertThat(oids, contains(oid1.getIdPart())); } + @Test + public void testShouldNotResolveATwoLinkChainWithAContainedResourceWhenContainedResourceIndexingIsTurnedOff() throws Exception { + // setup + IIdType oid1; + + { + Patient p = new Patient(); + p.setId("pat"); + p.addName().setFamily("Smith").addGiven("John"); + + Observation obs = new Observation(); + obs.getContained().add(p); + obs.getCode().setText("Observation 1"); + obs.setValue(new StringType("Test")); + obs.getSubject().setReference("#pat"); + + oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); + + // Create a dummy record so that an unconstrained query doesn't pass the test due to returning the only record + myObservationDao.create(new Observation(), mySrd); + } + + String url = "/Observation?subject.name=Smith"; + + // execute + List oids = searchAndReturnUnqualifiedVersionlessIdValues(url); + + // validate + assertEquals(0L, oids.size()); + } + @Test @Disabled public void testShouldResolveATwoLinkChainWithQualifiersWithAContainedResource() throws Exception { // TODO: This test fails because of a known limitation in qualified searches over contained resources. // Type information for intermediate resources in the chain is not being retained in the indexes. // setup + myModelConfig.setIndexOnContainedResources(true); + IIdType oid1; { @@ -151,6 +230,9 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { obs2.getSubject().setReference("#loc"); myObservationDao.create(obs2, mySrd); + + // Create a dummy record so that an unconstrained query doesn't pass the test due to returning the only record + myObservationDao.create(new Observation(), mySrd); } String url = "/Observation?subject:Patient.name=Smith"; @@ -168,6 +250,8 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { // Adding support for this case in SMILE-3151 // setup + myModelConfig.setIndexOnContainedResources(true); + IIdType oid1; IIdType orgId; @@ -188,6 +272,9 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { obs.getSubject().setReference("#pat"); oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); + + // Create a dummy record so that an unconstrained query doesn't pass the test due to returning the only record + myObservationDao.create(new Observation(), mySrd); } String url = "/Observation?subject.organization=" + orgId.getValueAsString(); @@ -201,7 +288,49 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { } @Test - public void testShouldResolveAThreeLinkChainWhereAllResourcesStandAlone() throws Exception { + public void testShouldResolveATwoLinkChainToAStandAloneReference() throws Exception { + // Adding support for this case in SMILE-3151 + + // setup + myModelConfig.setIndexOnContainedResources(true); + + IIdType oid1; + IIdType orgId; + + { + Organization org = new Organization(); + org.setId(IdType.newRandomUuid()); + org.setName("HealthCo"); + orgId = myOrganizationDao.create(org, mySrd).getId(); + + Patient p = new Patient(); + p.addName().setFamily("Smith").addGiven("John"); + p.getManagingOrganization().setReference(org.getId()); + myPatientDao.create(p, mySrd); + + Observation obs = new Observation(); + obs.getContained().add(p); + obs.getCode().setText("Observation 1"); + obs.getSubject().setReference(p.getId()); + + oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); + + // Create a dummy record so that an unconstrained query doesn't pass the test due to returning the only record + myObservationDao.create(new Observation(), mySrd); + } + + String url = "/Observation?subject.organization=" + orgId.getValueAsString(); + + // execute + List oids = searchAndReturnUnqualifiedVersionlessIdValues(url); + + // validate + assertEquals(1L, oids.size()); + assertThat(oids, contains(oid1.getIdPart())); + } + + @Test + public void testShouldResolveAThreeLinkChainWhereAllResourcesStandAloneWithoutContainedResourceIndexing() throws Exception { // setup IIdType oid1; @@ -223,6 +352,77 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { obs.getSubject().setReference(p.getId()); oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); + + // Create a dummy record so that an unconstrained query doesn't pass the test due to returning the only record + Organization dummyOrg = new Organization(); + dummyOrg.setId(IdType.newRandomUuid()); + dummyOrg.setName("Dummy"); + myOrganizationDao.create(dummyOrg, mySrd); + + Patient dummyPatient = new Patient(); + dummyPatient.setId(IdType.newRandomUuid()); + dummyPatient.addName().setFamily("Jones").addGiven("Jane"); + dummyPatient.getManagingOrganization().setReference(dummyOrg.getId()); + myPatientDao.create(dummyPatient, mySrd); + + Observation dummyObs = new Observation(); + dummyObs.getCode().setText("Observation 2"); + dummyObs.getSubject().setReference(dummyPatient.getId()); + myObservationDao.create(dummyObs, mySrd); + } + + String url = "/Observation?subject.organization.name=HealthCo"; + + // execute + List oids = searchAndReturnUnqualifiedVersionlessIdValues(url); + + // validate + assertEquals(1L, oids.size()); + assertThat(oids, contains(oid1.getIdPart())); + } + + @Test + public void testShouldResolveAThreeLinkChainWhereAllResourcesStandAlone() throws Exception { + + // setup + myModelConfig.setIndexOnContainedResources(true); + + IIdType oid1; + + { + Organization org = new Organization(); + org.setId(IdType.newRandomUuid()); + org.setName("HealthCo"); + myOrganizationDao.create(org, mySrd); + + Patient p = new Patient(); + p.setId(IdType.newRandomUuid()); + p.addName().setFamily("Smith").addGiven("John"); + p.getManagingOrganization().setReference(org.getId()); + myPatientDao.create(p, mySrd); + + Observation obs = new Observation(); + obs.getCode().setText("Observation 1"); + obs.getSubject().setReference(p.getId()); + + oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); + + // Create a dummy record so that an unconstrained query doesn't pass the test due to returning the only record + Organization dummyOrg = new Organization(); + dummyOrg.setId(IdType.newRandomUuid()); + dummyOrg.setName("Dummy"); + myOrganizationDao.create(dummyOrg, mySrd); + + Patient dummyPatient = new Patient(); + dummyPatient.setId(IdType.newRandomUuid()); + dummyPatient.addName().setFamily("Jones").addGiven("Jane"); + dummyPatient.getManagingOrganization().setReference(dummyOrg.getId()); + myPatientDao.create(dummyPatient, mySrd); + + Observation dummyObs = new Observation(); + dummyObs.getCode().setText("Observation 2"); + dummyObs.getSubject().setReference(dummyPatient.getId()); + myObservationDao.create(dummyObs, mySrd); } String url = "/Observation?subject.organization.name=HealthCo"; @@ -240,6 +440,8 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { // This is the case that is most relevant to SMILE-2899 // setup + myModelConfig.setIndexOnContainedResources(true); + IIdType oid1; { @@ -259,6 +461,9 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { obs.getSubject().setReference(p.getId()); oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); + + // Create a dummy record so that an unconstrained query doesn't pass the test due to returning the only record + myObservationDao.create(new Observation(), mySrd); } String url = "/Observation?subject.organization.name=HealthCo"; @@ -276,6 +481,8 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { // Adding support for this case in SMILE-3151 // setup + myModelConfig.setIndexOnContainedResources(true); + IIdType oid1; { @@ -295,6 +502,9 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { obs.getSubject().setReference("#pat"); oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); + + // Create a dummy record so that an unconstrained query doesn't pass the test due to returning the only record + myObservationDao.create(new Observation(), mySrd); } String url = "/Observation?subject.organization.name=HealthCo"; @@ -307,10 +517,50 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { assertThat(oids, contains(oid1.getIdPart())); } + @Test + public void testShouldNotResolveAThreeLinkChainWithAllContainedResourcesWhenRecursiveContainedIndexesAreDisabled() throws Exception { + + // setup + myModelConfig.setIndexOnContainedResources(true); + + IIdType oid1; + + { + Organization org = new Organization(); + org.setId("org"); + org.setName("HealthCo"); + + Patient p = new Patient(); + p.setId("pat"); + p.addName().setFamily("Smith").addGiven("John"); + p.getManagingOrganization().setReference("#org"); + + Observation obs = new Observation(); + obs.getContained().add(p); + obs.getContained().add(org); + obs.getCode().setText("Observation 1"); + obs.getSubject().setReference("#pat"); + + oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); + + // Create a dummy record so that an unconstrained query doesn't pass the test due to returning the only record + myObservationDao.create(new Observation(), mySrd); + } + + String url = "/Observation?subject.organization.name=HealthCo"; + + // execute + List oids = searchAndReturnUnqualifiedVersionlessIdValues(url); + + // validate + assertEquals(0L, oids.size()); + } + @Test public void testShouldResolveAThreeLinkChainWithAllContainedResources() throws Exception { // setup + myModelConfig.setIndexOnContainedResources(true); myModelConfig.setIndexOnContainedResourcesRecursively(true); IIdType oid1; @@ -332,6 +582,9 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { obs.getSubject().setReference("#pat"); oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); + + // Create a dummy record so that an unconstrained query doesn't pass the test due to returning the only record + myObservationDao.create(new Observation(), mySrd); } String url = "/Observation?subject.organization.name=HealthCo"; @@ -350,6 +603,8 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { public void testShouldResolveAThreeLinkChainWithQualifiersWhereAllResourcesStandAlone() throws Exception { // setup + myModelConfig.setIndexOnContainedResources(true); + IIdType oid1; { @@ -379,6 +634,9 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { oid1 = myObservationDao.create(obs1, mySrd).getId().toUnqualifiedVersionless(); myObservationDao.create(obs2, mySrd); + + // Create a dummy record so that an unconstrained query doesn't pass the test due to returning the only record + myObservationDao.create(new Observation(), mySrd); } String url = "/Observation?subject:Patient.organization:Organization.name=HealthCo"; @@ -396,6 +654,8 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { // This is the case that is most relevant to SMILE-2899 // setup + myModelConfig.setIndexOnContainedResources(true); + IIdType oid1; { @@ -430,6 +690,9 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { obs2.getCode().setText("Observation 2"); obs2.getSubject().setReference(d.getId()); myObservationDao.create(obs2, mySrd); + + // Create a dummy record so that an unconstrained query doesn't pass the test due to returning the only record + myObservationDao.create(new Observation(), mySrd); } String url = "/Observation?subject:Patient.organization:Organization.name=HealthCo"; @@ -447,6 +710,8 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { // Adding support for this case in SMILE-3151 // setup + myModelConfig.setIndexOnContainedResources(true); + IIdType oid1; { @@ -477,6 +742,9 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { obs2.getSubject().setReference("#dev"); myObservationDao.create(obs2, mySrd); + + // Create a dummy record so that an unconstrained query doesn't pass the test due to returning the only record + myObservationDao.create(new Observation(), mySrd); } String url = "/Observation?subject:Patient.organization:Organization.name=HealthCo"; @@ -500,6 +768,8 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { // Adding support for this case in SMILE-3151 // setup + myModelConfig.setIndexOnContainedResources(true); + IIdType oid1; { @@ -530,6 +800,9 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { obs2.getSubject().setReference("#loc"); myObservationDao.create(obs2, mySrd); + + // Create a dummy record so that an unconstrained query doesn't pass the test due to returning the only record + myObservationDao.create(new Observation(), mySrd); } String url = "/Observation?subject:Patient.organization:Organization.name=HealthCo"; @@ -551,6 +824,7 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { // Type information for intermediate resources in the chain is not being retained in the indexes. // setup + myModelConfig.setIndexOnContainedResources(true); myModelConfig.setIndexOnContainedResourcesRecursively(true); IIdType oid1; @@ -588,6 +862,9 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { obs2.getSubject().setReference("#dev"); myObservationDao.create(obs2, mySrd); + + // Create a dummy record so that an unconstrained query doesn't pass the test due to returning the only record + myObservationDao.create(new Observation(), mySrd); } String url = "/Observation?subject:Patient.organization:Organization.name=HealthCo"; @@ -606,6 +883,8 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { public void testShouldResolveAFourLinkChainWhereAllResourcesStandAlone() throws Exception { // setup + myModelConfig.setIndexOnContainedResources(true); + IIdType oid1; { @@ -630,6 +909,9 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { obs.getSubject().setReference(p.getId()); oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); + + // Create a dummy record so that an unconstrained query doesn't pass the test due to returning the only record + myObservationDao.create(new Observation(), mySrd); } String url = "/Observation?subject.organization.partof.name=HealthCo"; @@ -646,6 +928,8 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { public void testShouldResolveAFourLinkChainWhereTheLastReferenceIsContained() throws Exception { // setup + myModelConfig.setIndexOnContainedResources(true); + IIdType oid1; { @@ -670,6 +954,9 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { obs.getSubject().setReference(p.getId()); oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); + + // Create a dummy record so that an unconstrained query doesn't pass the test due to returning the only record + myObservationDao.create(new Observation(), mySrd); } String url = "/Observation?subject.organization.partof.name=HealthCo"; @@ -686,6 +973,7 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { public void testShouldResolveAFourLinkChainWhereTheLastTwoReferencesAreContained() throws Exception { // setup + myModelConfig.setIndexOnContainedResources(true); myModelConfig.setIndexOnContainedResourcesRecursively(true); IIdType oid1; @@ -711,6 +999,9 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { obs.getSubject().setReference(p.getId()); oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); + + // Create a dummy record so that an unconstrained query doesn't pass the test due to returning the only record + myObservationDao.create(new Observation(), mySrd); } String url = "/Observation?subject.organization.partof.name=HealthCo"; @@ -727,6 +1018,8 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { public void testShouldResolveAFourLinkChainWithAContainedResourceInTheMiddle() throws Exception { // setup + myModelConfig.setIndexOnContainedResources(true); + IIdType oid1; { @@ -755,6 +1048,9 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); myCaptureQueriesListener.logInsertQueries(); + + // Create a dummy record so that an unconstrained query doesn't pass the test due to returning the only record + myObservationDao.create(new Observation(), mySrd); } String url = "/Observation?subject.organization.partof.name=HealthCo"; @@ -773,6 +1069,7 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { public void testShouldResolveAFourLinkChainWhereTheFirstTwoReferencesAreContained() throws Exception { // setup + myModelConfig.setIndexOnContainedResources(true); myModelConfig.setIndexOnContainedResourcesRecursively(true); IIdType oid1; @@ -799,6 +1096,54 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { obs.getSubject().setReference("#pat"); oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); + + // Create a dummy record so that an unconstrained query doesn't pass the test due to returning the only record + myObservationDao.create(new Observation(), mySrd); + } + + String url = "/Observation?subject.organization.partof.name=HealthCo"; + + // execute + List oids = searchAndReturnUnqualifiedVersionlessIdValues(url); + + // validate + assertEquals(1L, oids.size()); + assertThat(oids, contains(oid1.getIdPart())); + } + + @Test + public void testShouldResolveAFourLinkChainWhereTheFirstReferenceAndTheLastReferenceAreContained() throws Exception { + + // setup + myModelConfig.setIndexOnContainedResources(true); + myModelConfig.setIndexOnContainedResourcesRecursively(true); + IIdType oid1; + + { + Organization org = new Organization(); + org.setId("parent"); + org.setName("HealthCo"); + + Organization partOfOrg = new Organization(); + partOfOrg.getContained().add(org); + partOfOrg.setId(IdType.newRandomUuid()); + partOfOrg.getPartOf().setReference("#parent"); + myOrganizationDao.create(partOfOrg, mySrd); + + Patient p = new Patient(); + p.setId("pat"); + p.addName().setFamily("Smith").addGiven("John"); + p.getManagingOrganization().setReference(partOfOrg.getId()); + + Observation obs = new Observation(); + obs.getContained().add(p); + obs.getCode().setText("Observation 1"); + obs.getSubject().setReference("#pat"); + + oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); + + // Create a dummy record so that an unconstrained query doesn't pass the test due to returning the only record + myObservationDao.create(new Observation(), mySrd); } String url = "/Observation?subject.organization.partof.name=HealthCo"; @@ -815,6 +1160,7 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { public void testShouldResolveAFourLinkChainWhereAllReferencesAreContained() throws Exception { // setup + myModelConfig.setIndexOnContainedResources(true); myModelConfig.setIndexOnContainedResourcesRecursively(true); IIdType oid1; @@ -840,6 +1186,9 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { obs.getSubject().setReference("#pat"); oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); + + // Create a dummy record so that an unconstrained query doesn't pass the test due to returning the only record + myObservationDao.create(new Observation(), mySrd); } String url = "/Observation?subject.organization.partof.name=HealthCo"; @@ -852,6 +1201,64 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { assertThat(oids, contains(oid1.getIdPart())); } + @Test + public void testShouldThrowAnExceptionForAFiveLinkChain() throws Exception { + + // setup + myModelConfig.setIndexOnContainedResources(true); + myModelConfig.setIndexOnContainedResourcesRecursively(true); + + String url = "/Observation?subject.organization.partof.partof.name=HealthCo"; + + try { + // execute + searchAndReturnUnqualifiedVersionlessIdValues(url); + fail("Expected an exception to be thrown"); + } catch (InvalidRequestException e) { + assertEquals("The search chain subject.organization.partof.partof.name is too long. Only chains up to three references are supported.", e.getMessage()); + } + } + + @Test + public void testQueryStructure() throws Exception { + + // With indexing of contained resources turned off, we should not see UNION clauses in the query + countUnionStatementsInGeneratedQuery("/Observation?patient.name=Smith", 0); + countUnionStatementsInGeneratedQuery("/Observation?patient.organization.name=Smith", 0); + countUnionStatementsInGeneratedQuery("/Observation?patient.organization.partof.name=Smith", 0); + + // With indexing of contained resources turned on, we take the UNION of several subselects that handle the different patterns of containment + // Keeping in mind that the number of clauses is one greater than the number of UNION keywords, + // this increases as the chain grows longer according to the Fibonacci sequence: (2, 3, 5, 8, 13) + myModelConfig.setIndexOnContainedResources(true); + countUnionStatementsInGeneratedQuery("/Observation?patient.name=Smith", 1); + countUnionStatementsInGeneratedQuery("/Observation?patient.organization.name=Smith", 2); + countUnionStatementsInGeneratedQuery("/Observation?patient.organization.partof.name=Smith", 4); + + // With recursive indexing of contained resources turned on, even more containment patterns are considered + // This increases as the chain grows longer as powers of 2: (2, 4, 8, 16, 32) + myModelConfig.setIndexOnContainedResourcesRecursively(true); + countUnionStatementsInGeneratedQuery("/Observation?patient.name=Smith", 1); + countUnionStatementsInGeneratedQuery("/Observation?patient.organization.name=Smith", 3); + countUnionStatementsInGeneratedQuery("/Observation?patient.organization.partof.name=Smith", 7); + + // If a reference in the chain has multiple potential target resource types, the number of subselects increases + countUnionStatementsInGeneratedQuery("/Observation?subject.name=Smith", 3); + + // If such a reference if qualified to restrict the type, the number goes back down + countUnionStatementsInGeneratedQuery("/Observation?subject:Location.name=Smith", 1); + } + + private void countUnionStatementsInGeneratedQuery(String theUrl, int theExpectedNumberOfUnions) throws IOException { + myCaptureQueriesListener.clear(); + searchAndReturnUnqualifiedVersionlessIdValues(theUrl); + List selectQueries = myCaptureQueriesListener.getSelectQueriesForCurrentThread(); + assertEquals(1, selectQueries.size()); + + String sqlQuery = selectQueries.get(0).getSql(true, true).toLowerCase(); + assertEquals(theExpectedNumberOfUnions, countMatches(sqlQuery, "union"), sqlQuery); + } + private List searchAndReturnUnqualifiedVersionlessIdValues(String theUrl) throws IOException { List ids = new ArrayList<>(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImplTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImplTest.java index 0c442f127c6..d8d963181c7 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImplTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImplTest.java @@ -166,6 +166,7 @@ public class SearchCoordinatorSvcImplTest { @Test public void testAsyncSearchFailDuringSearchSameCoordinator() { initSearches(); + initAsyncSearches(); SearchParameterMap params = new SearchParameterMap(); params.add("name", new StringParam("ANAME")); @@ -186,6 +187,7 @@ public class SearchCoordinatorSvcImplTest { @Test public void testAsyncSearchLargeResultSetBigCountSameCoordinator() { initSearches(); + initAsyncSearches(); List allResults = new ArrayList<>(); doAnswer(t -> { @@ -281,6 +283,7 @@ public class SearchCoordinatorSvcImplTest { @Test public void testAsyncSearchLargeResultSetSameCoordinator() { initSearches(); + initAsyncSearches(); SearchParameterMap params = new SearchParameterMap(); params.add("name", new StringParam("ANAME")); @@ -308,7 +311,9 @@ public class SearchCoordinatorSvcImplTest { when(mySearchBuilderFactory.newSearchBuilder(any(), any(), any())).thenReturn(mySearchBuilder); when(myTxManager.getTransaction(any())).thenReturn(mock(TransactionStatus.class)); + } + private void initAsyncSearches() { when(myPersistedJpaBundleProviderFactory.newInstanceFirstPage(nullable(RequestDetails.class), nullable(Search.class), nullable(SearchCoordinatorSvcImpl.SearchTask.class), nullable(ISearchBuilder.class))).thenAnswer(t->{ RequestDetails requestDetails = t.getArgument(0, RequestDetails.class); Search search = t.getArgument(1, Search.class); @@ -374,6 +379,7 @@ public class SearchCoordinatorSvcImplTest { @Test public void testAsyncSearchLargeResultSetSecondRequestSameCoordinator() { initSearches(); + initAsyncSearches(); SearchParameterMap params = new SearchParameterMap(); params.add("name", new StringParam("ANAME")); @@ -412,6 +418,7 @@ public class SearchCoordinatorSvcImplTest { @Test public void testAsyncSearchSmallResultSetSameCoordinator() { initSearches(); + initAsyncSearches(); SearchParameterMap params = new SearchParameterMap(); params.add("name", new StringParam("ANAME"));