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
This commit is contained in:
JasonRoberts-smile 2022-01-20 16:22:02 -05:00 committed by GitHub
parent 283ff19375
commit 48caff30d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 860 additions and 110 deletions

View File

@ -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."

View File

@ -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.exceptions.PreconditionFailedException;
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
import com.google.common.collect.Lists; 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.BinaryCondition;
import com.healthmarketscience.sqlbuilder.ComboCondition; import com.healthmarketscience.sqlbuilder.ComboCondition;
import com.healthmarketscience.sqlbuilder.Condition; import com.healthmarketscience.sqlbuilder.Condition;
@ -88,7 +90,9 @@ import com.healthmarketscience.sqlbuilder.Expression;
import com.healthmarketscience.sqlbuilder.InCondition; import com.healthmarketscience.sqlbuilder.InCondition;
import com.healthmarketscience.sqlbuilder.OrderObject; import com.healthmarketscience.sqlbuilder.OrderObject;
import com.healthmarketscience.sqlbuilder.SelectQuery; import com.healthmarketscience.sqlbuilder.SelectQuery;
import com.healthmarketscience.sqlbuilder.SetOperationQuery;
import com.healthmarketscience.sqlbuilder.Subquery; import com.healthmarketscience.sqlbuilder.Subquery;
import com.healthmarketscience.sqlbuilder.UnionQuery;
import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn; import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn;
import org.apache.commons.collections4.BidiMap; import org.apache.commons.collections4.BidiMap;
import org.apache.commons.collections4.bidimap.DualHashBidiMap; import org.apache.commons.collections4.bidimap.DualHashBidiMap;
@ -112,12 +116,15 @@ import java.util.EnumSet;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.apache.commons.lang3.StringUtils.split;
public class QueryStack { public class QueryStack {
@ -287,6 +294,10 @@ public class QueryStack {
} }
private Condition createPredicateComposite(@Nullable DbColumn theSourceJoinColumn, String theResourceName, String theSpnamePrefix, RuntimeSearchParam theParamDef, List<? extends IQueryParameterType> theNextAnd, RequestPartitionId theRequestPartitionId) { private Condition createPredicateComposite(@Nullable DbColumn theSourceJoinColumn, String theResourceName, String theSpnamePrefix, RuntimeSearchParam theParamDef, List<? extends IQueryParameterType> 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<? extends IQueryParameterType> theNextAnd, RequestPartitionId theRequestPartitionId, SearchQueryBuilder theSqlBuilder) {
Condition orCondidtion = null; Condition orCondidtion = null;
for (IQueryParameterType next : theNextAnd) { for (IQueryParameterType next : theNextAnd) {
@ -299,11 +310,11 @@ public class QueryStack {
List<RuntimeSearchParam> componentParams = JpaParamUtil.resolveComponentParameters(mySearchParamRegistry, theParamDef); List<RuntimeSearchParam> componentParams = JpaParamUtil.resolveComponentParameters(mySearchParamRegistry, theParamDef);
RuntimeSearchParam left = componentParams.get(0); RuntimeSearchParam left = componentParams.get(0);
IQueryParameterType leftValue = cp.getLeftValue(); 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); RuntimeSearchParam right = componentParams.get(1);
IQueryParameterType rightValue = cp.getRightValue(); 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); 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) { 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()) { switch (theParam.getParamType()) {
case STRING: { 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: { 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: { 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: { 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 NUMBER:
case REFERENCE: case REFERENCE:
@ -368,10 +383,15 @@ public class QueryStack {
public Condition createPredicateDate(@Nullable DbColumn theSourceJoinColumn, String theResourceName, public Condition createPredicateDate(@Nullable DbColumn theSourceJoinColumn, String theResourceName,
String theSpnamePrefix, RuntimeSearchParam theSearchParam, List<? extends IQueryParameterType> theList, String theSpnamePrefix, RuntimeSearchParam theSearchParam, List<? extends IQueryParameterType> theList,
SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId) { 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<? extends IQueryParameterType> theList,
SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId, SearchQueryBuilder theSqlBuilder) {
String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName()); String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName());
PredicateBuilderCacheLookupResult<DatePredicateBuilder> predicateBuilderLookupResult = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.DATE, theSourceJoinColumn, paramName, () -> mySqlBuilder.addDatePredicateBuilder(theSourceJoinColumn)); PredicateBuilderCacheLookupResult<DatePredicateBuilder> predicateBuilderLookupResult = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.DATE, theSourceJoinColumn, paramName, () -> theSqlBuilder.addDatePredicateBuilder(theSourceJoinColumn));
DatePredicateBuilder predicateBuilder = predicateBuilderLookupResult.getResult(); DatePredicateBuilder predicateBuilder = predicateBuilderLookupResult.getResult();
boolean cacheHit = predicateBuilderLookupResult.isCacheHit(); boolean cacheHit = predicateBuilderLookupResult.isCacheHit();
@ -577,10 +597,16 @@ public class QueryStack {
public Condition createPredicateNumber(@Nullable DbColumn theSourceJoinColumn, String theResourceName, public Condition createPredicateNumber(@Nullable DbColumn theSourceJoinColumn, String theResourceName,
String theSpnamePrefix, RuntimeSearchParam theSearchParam, List<? extends IQueryParameterType> theList, String theSpnamePrefix, RuntimeSearchParam theSearchParam, List<? extends IQueryParameterType> theList,
SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId) { 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<? extends IQueryParameterType> theList,
SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId, SearchQueryBuilder theSqlBuilder) {
String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName()); 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) { if (theList.get(0).getMissing() != null) {
return join.createPredicateParamMissingForNonReference(theResourceName, paramName, theList.get(0).getMissing(), theRequestPartitionId); 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, public Condition createPredicateQuantity(@Nullable DbColumn theSourceJoinColumn, String theResourceName,
String theSpnamePrefix, RuntimeSearchParam theSearchParam, List<? extends IQueryParameterType> theList, String theSpnamePrefix, RuntimeSearchParam theSearchParam, List<? extends IQueryParameterType> theList,
SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId) { 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<? extends IQueryParameterType> theList,
SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId, SearchQueryBuilder theSqlBuilder) {
String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName()); String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName());
if (theList.get(0).getMissing() != null) { 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); return join.createPredicateParamMissingForNonReference(theResourceName, paramName, theList.get(0).getMissing(), theRequestPartitionId);
} }
@ -641,13 +673,13 @@ public class QueryStack {
.collect(Collectors.toList()); .collect(Collectors.toList());
if (normalizedQuantityParams.size() == quantityParams.size()) { 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; quantityParams = normalizedQuantityParams;
} }
} }
if (join == null) { if (join == null) {
join = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.QUANTITY, theSourceJoinColumn, paramName, () -> mySqlBuilder.addQuantityPredicateBuilder(theSourceJoinColumn)).getResult(); join = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.QUANTITY, theSourceJoinColumn, paramName, () -> theSqlBuilder.addQuantityPredicateBuilder(theSourceJoinColumn)).getResult();
} }
List<Condition> codePredicates = new ArrayList<>(); List<Condition> codePredicates = new ArrayList<>();
@ -667,6 +699,17 @@ public class QueryStack {
SearchFilterParser.CompareOperation theOperation, SearchFilterParser.CompareOperation theOperation,
RequestDetails theRequest, RequestDetails theRequest,
RequestPartitionId theRequestPartitionId) { RequestPartitionId theRequestPartitionId) {
return createPredicateReference(theSourceJoinColumn, theResourceName, theParamName, theQualifiers, theList, theOperation, theRequest, theRequestPartitionId, mySqlBuilder);
}
public Condition createPredicateReference(@Nullable DbColumn theSourceJoinColumn,
String theResourceName,
String theParamName,
List<String> theQualifiers,
List<? extends IQueryParameterType> theList,
SearchFilterParser.CompareOperation theOperation,
RequestDetails theRequest,
RequestPartitionId theRequestPartitionId, SearchQueryBuilder theSqlBuilder) {
if ((theOperation != null) && if ((theOperation != null) &&
(theOperation != SearchFilterParser.CompareOperation.eq) && (theOperation != SearchFilterParser.CompareOperation.eq) &&
@ -675,140 +718,407 @@ public class QueryStack {
} }
if (theList.get(0).getMissing() != null) { 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); 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); 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<List<ChainElement>,Set<LeafNodeDefinition>> myChains = Maps.newHashMap();
public Map<List<ChainElement>,Set<LeafNodeDefinition>> getChains() { return myChains; }
private boolean isReferenceParamValid(ReferenceParam theReferenceParam) {
return split(theReferenceParam.getChain(), '.').length <= 3;
}
public void deriveChains(String theResourceType, RuntimeSearchParam theSearchParam, List<? extends IQueryParameterType> theList) {
List<ChainElement> 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<String> qualifiers = Lists.newArrayList(referenceParam.getResourceType());
processNextLinkInChain(searchParams, theSearchParam, targetChain, targetValue, qualifiers, referenceParam.getResourceType());
}
}
}
private void processNextLinkInChain(List<ChainElement> theSearchParams, RuntimeSearchParam thePreviousSearchParam, String theChain, String theTargetValue, List<String> 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<String> 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<ChainElement> searchParamBranch = Lists.newArrayList();
searchParamBranch.addAll(theSearchParams);
if (StringUtils.isEmpty(nextChain)) {
// We've reached the end of the chain
ArrayList<IQueryParameterType> 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<LeafNodeDefinition> 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<IQueryParameterType> myOrValues;
private final String myLeafTarget;
private final String myLeafParamName;
private final String myLeafPathPrefix;
private final List<String> myQualifiers;
public LeafNodeDefinition(RuntimeSearchParam theParamDefinition, ArrayList<IQueryParameterType> theOrValues, String theLeafTarget, String theLeafParamName, String theLeafPathPrefix, List<String> theQualifiers) {
myParamDefinition = theParamDefinition;
myOrValues = theOrValues;
myLeafTarget = theLeafTarget;
myLeafParamName = theLeafParamName;
myLeafPathPrefix = theLeafPathPrefix;
myQualifiers = theQualifiers;
}
public RuntimeSearchParam getParamDefinition() {
return myParamDefinition;
}
public ArrayList<IQueryParameterType> getOrValues() {
return myOrValues;
}
public String getLeafTarget() {
return myLeafTarget;
}
public String getLeafParamName() {
return myLeafParamName;
}
public String getLeafPathPrefix() {
return myLeafPathPrefix;
}
public List<String> 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, public Condition createPredicateReferenceForContainedResource(@Nullable DbColumn theSourceJoinColumn,
String theResourceName, String theParamName, List<String> theQualifiers, RuntimeSearchParam theSearchParam, String theResourceName, String theParamName, List<String> theQualifiers, RuntimeSearchParam theSearchParam,
List<? extends IQueryParameterType> theList, SearchFilterParser.CompareOperation theOperation, List<? extends IQueryParameterType> theList, SearchFilterParser.CompareOperation theOperation,
RequestDetails theRequest, RequestPartitionId theRequestPartitionId) { 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<PredicateBuilderTypeEnum> cachedReusePredicateBuilderTypes = EnumSet.copyOf(myReusePredicateBuilderTypes);
myReusePredicateBuilderTypes.clear();
String spnamePrefix = theParamName; UnionQuery union = new UnionQuery(SetOperationQuery.Type.UNION);
String targetChain = null; ReferenceChainExtractor chainExtractor = new ReferenceChainExtractor();
String targetParamName = null; chainExtractor.deriveChains(theResourceName, theSearchParam, theList);
String headQualifier = null; Map<List<ChainElement>,Set<LeafNodeDefinition>> chains = chainExtractor.getChains();
String targetQualifier = null;
String targetValue = null;
RuntimeSearchParam targetParamDefinition = null; Map<List<String>,Set<LeafNodeDefinition>> referenceLinks = Maps.newHashMap();
for (List<ChainElement> nextChain : chains.keySet()) {
Set<LeafNodeDefinition> leafNodes = chains.get(nextChain);
ArrayList<IQueryParameterType> orValues = Lists.newArrayList(); collateChainedSearchOptions(referenceLinks, nextChain, leafNodes);
List<IQueryParameterType> trimmedParameters = Lists.newArrayList(); }
IQueryParameterType qp = null;
for (int orIdx = 0; orIdx < theList.size(); orIdx++) { for (List<String> nextReferenceLink: referenceLinks.keySet()) {
for (LeafNodeDefinition leafNodeDefinition : referenceLinks.get(nextReferenceLink)) {
SearchQueryBuilder builder = mySqlBuilder.newChildSqlBuilder();
DbColumn previousJoinColumn = null;
IQueryParameterType nextOr = theList.get(orIdx); // Create a reference link predicate to the subselect for every link but the last one
for (String nextLink : nextReferenceLink) {
if (nextOr instanceof ReferenceParam) { // 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.
ReferenceParam referenceParam = (ReferenceParam) nextOr; ResourceLinkPredicateBuilder resourceLinkPredicateBuilder = builder.addReferencePredicateBuilder(this, previousJoinColumn);
builder.addPredicate(resourceLinkPredicateBuilder.createPredicateSourcePaths(Lists.newArrayList(nextLink)));
// 1. Find out the parameter, qualifier and the value previousJoinColumn = resourceLinkPredicateBuilder.getColumnTargetResourceId();
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);
} }
int qualifierIndex = targetParamName.indexOf(':'); Condition containedCondition = createIndexPredicate(
if (qualifierIndex != -1) { previousJoinColumn,
targetParamName = targetParamName.substring(0, qualifierIndex); leafNodeDefinition.getLeafTarget(),
targetQualifier = targetParamName.substring(qualifierIndex); leafNodeDefinition.getLeafPathPrefix(),
} leafNodeDefinition.getLeafParamName(),
trimmedParameters.add(new ReferenceParam(targetQualifier, targetNextChain, referenceParam.getValue())); leafNodeDefinition.getParamDefinition(),
leafNodeDefinition.getOrValues(),
theOperation,
leafNodeDefinition.getQualifiers(),
theRequest,
theRequestPartitionId,
builder);
// 2. find out the data type builder.addPredicate(containedCondition);
if (targetParamDefinition == null) {
for (String nextTarget : theSearchParam.getTargets()) {
if (!referenceParam.hasResourceType() || referenceParam.getResourceType().equals(nextTarget)) {
targetParamDefinition = mySearchParamRegistry.getActiveSearchParam(nextTarget, targetParamName);
}
if (targetParamDefinition != null)
break;
}
}
if (targetParamDefinition == null) { union.addQueries(builder.getSelect());
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);
} }
} }
if (targetParamDefinition == null) { InCondition inCondition;
throw new InvalidRequestException("Unknown search parameter name: " + theSearchParam.getName() + "."); 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<List<String>, Set<LeafNodeDefinition>> referenceLinks, List<ChainElement> nextChain, Set<LeafNodeDefinition> 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<List<String>, Set<LeafNodeDefinition>> theReferenceLinksMap, ArrayList<String> thePath, Set<LeafNodeDefinition> theLeafNodesToAdd) {
Set<LeafNodeDefinition> 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<IQueryParameterType> theOrValues, SearchFilterParser.CompareOperation theOperation, List<String> theQualifiers, RequestDetails theRequest, RequestPartitionId theRequestPartitionId, SearchQueryBuilder theSqlBuilder) {
Condition containedCondition = null; Condition containedCondition = null;
switch (targetParamDefinition.getParamType()) { switch (theParamDefinition.getParamType()) {
case DATE: case DATE:
containedCondition = createPredicateDate(theSourceJoinColumn, theResourceName, spnamePrefix, targetParamDefinition, containedCondition = createPredicateDate(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParamDefinition,
orValues, theOperation, theRequestPartitionId); theOrValues, theOperation, theRequestPartitionId, theSqlBuilder);
break; break;
case NUMBER: case NUMBER:
containedCondition = createPredicateNumber(theSourceJoinColumn, theResourceName, spnamePrefix, targetParamDefinition, containedCondition = createPredicateNumber(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParamDefinition,
orValues, theOperation, theRequestPartitionId); theOrValues, theOperation, theRequestPartitionId, theSqlBuilder);
break; break;
case QUANTITY: case QUANTITY:
containedCondition = createPredicateQuantity(theSourceJoinColumn, theResourceName, spnamePrefix, targetParamDefinition, containedCondition = createPredicateQuantity(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParamDefinition,
orValues, theOperation, theRequestPartitionId); theOrValues, theOperation, theRequestPartitionId, theSqlBuilder);
break; break;
case STRING: case STRING:
containedCondition = createPredicateString(theSourceJoinColumn, theResourceName, spnamePrefix, targetParamDefinition, containedCondition = createPredicateString(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParamDefinition,
orValues, theOperation, theRequestPartitionId); theOrValues, theOperation, theRequestPartitionId, theSqlBuilder);
break; break;
case TOKEN: case TOKEN:
containedCondition = createPredicateToken(theSourceJoinColumn, theResourceName, spnamePrefix, targetParamDefinition, containedCondition = createPredicateToken(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParamDefinition,
orValues, theOperation, theRequestPartitionId); theOrValues, theOperation, theRequestPartitionId, theSqlBuilder);
break; break;
case COMPOSITE: case COMPOSITE:
containedCondition = createPredicateComposite(theSourceJoinColumn, theResourceName, spnamePrefix, targetParamDefinition, containedCondition = createPredicateComposite(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParamDefinition,
orValues, theRequestPartitionId); theOrValues, theRequestPartitionId, theSqlBuilder);
break; break;
case URI: case URI:
containedCondition = createPredicateUri(theSourceJoinColumn, theResourceName, spnamePrefix, targetParamDefinition, containedCondition = createPredicateUri(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParamDefinition,
orValues, theOperation, theRequest, theRequestPartitionId); theOrValues, theOperation, theRequest, theRequestPartitionId, theSqlBuilder);
break; break;
case REFERENCE: case REFERENCE:
String chainedParamName = theParamName + "." + targetParamName; containedCondition = createPredicateReference(theSourceJoinColumn, theResourceName, StringUtils.isBlank(theSpnamePrefix) ? theParamName : theSpnamePrefix + "." + theParamName, theQualifiers,
containedCondition = createPredicateReference(theSourceJoinColumn, theResourceName, chainedParamName, theQualifiers, trimmedParameters, theOperation, theRequest, theRequestPartitionId); theOrValues, theOperation, theRequest, theRequestPartitionId, theSqlBuilder);
if (myModelConfig.isIndexOnContainedResourcesRecursively()) {
containedCondition = toOrPredicate(containedCondition,
createPredicateReferenceForContainedResource(theSourceJoinColumn, theResourceName, chainedParamName, theQualifiers, theSearchParam, trimmedParameters, theOperation, theRequest, theRequestPartitionId));
}
break; break;
case HAS: case HAS:
case SPECIAL: case SPECIAL:
default: default:
throw new InvalidRequestException( throw new InvalidRequestException(
"The search type:" + targetParamDefinition.getParamType() + " is not supported."); "The search type:" + theParamDefinition.getParamType() + " is not supported.");
} }
return containedCondition; return containedCondition;
} }
@ -857,10 +1167,17 @@ public class QueryStack {
public Condition createPredicateString(@Nullable DbColumn theSourceJoinColumn, String theResourceName, public Condition createPredicateString(@Nullable DbColumn theSourceJoinColumn, String theResourceName,
String theSpnamePrefix, RuntimeSearchParam theSearchParam, List<? extends IQueryParameterType> theList, String theSpnamePrefix, RuntimeSearchParam theSearchParam, List<? extends IQueryParameterType> theList,
SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId) { 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<? extends IQueryParameterType> theList,
SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId,
SearchQueryBuilder theSqlBuilder) {
String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName()); 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) { if (theList.get(0).getMissing() != null) {
return join.createPredicateParamMissingForNonReference(theResourceName, paramName, theList.get(0).getMissing(), theRequestPartitionId); 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, public Condition createPredicateToken(@Nullable DbColumn theSourceJoinColumn, String theResourceName,
String theSpnamePrefix, RuntimeSearchParam theSearchParam, List<? extends IQueryParameterType> theList, String theSpnamePrefix, RuntimeSearchParam theSearchParam, List<? extends IQueryParameterType> theList,
SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId) { 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<? extends IQueryParameterType> theList,
SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId, SearchQueryBuilder theSqlBuilder) {
List<IQueryParameterType> tokens = new ArrayList<>(); List<IQueryParameterType> tokens = new ArrayList<>();
@ -991,7 +1314,7 @@ public class QueryStack {
throw new MethodNotAllowedException(msg); 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(); modifier = id.getModifier();
@ -1017,13 +1340,13 @@ public class QueryStack {
BaseJoiningPredicateBuilder join; BaseJoiningPredicateBuilder join;
if (paramInverted) { if (paramInverted) {
SearchQueryBuilder sqlBuilder = mySqlBuilder.newChildSqlBuilder(); SearchQueryBuilder sqlBuilder = theSqlBuilder.newChildSqlBuilder();
TokenPredicateBuilder tokenSelector = sqlBuilder.addTokenPredicateBuilder(null); TokenPredicateBuilder tokenSelector = sqlBuilder.addTokenPredicateBuilder(null);
sqlBuilder.addPredicate(tokenSelector.createPredicateToken(tokens, theResourceName, theSpnamePrefix, theSearchParam, theRequestPartitionId)); sqlBuilder.addPredicate(tokenSelector.createPredicateToken(tokens, theResourceName, theSpnamePrefix, theSearchParam, theRequestPartitionId));
SelectQuery sql = sqlBuilder.getSelect(); SelectQuery sql = sqlBuilder.getSelect();
Expression subSelect = new Subquery(sql); Expression subSelect = new Subquery(sql);
join = mySqlBuilder.getOrCreateFirstPredicateBuilder(); join = theSqlBuilder.getOrCreateFirstPredicateBuilder();
if (theSourceJoinColumn == null) { if (theSourceJoinColumn == null) {
predicate = new InCondition(join.getResourceIdColumn(), subSelect).setNegate(true); predicate = new InCondition(join.getResourceIdColumn(), subSelect).setNegate(true);
@ -1034,7 +1357,7 @@ public class QueryStack {
} else { } 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) { if (theList.get(0).getMissing() != null) {
return tokenJoin.createPredicateParamMissingForNonReference(theResourceName, paramName, theList.get(0).getMissing(), theRequestPartitionId); return tokenJoin.createPredicateParamMissingForNonReference(theResourceName, paramName, theList.get(0).getMissing(), theRequestPartitionId);
@ -1051,10 +1374,17 @@ public class QueryStack {
String theSpnamePrefix, RuntimeSearchParam theSearchParam, List<? extends IQueryParameterType> theList, String theSpnamePrefix, RuntimeSearchParam theSearchParam, List<? extends IQueryParameterType> theList,
SearchFilterParser.CompareOperation theOperation, RequestDetails theRequestDetails, SearchFilterParser.CompareOperation theOperation, RequestDetails theRequestDetails,
RequestPartitionId theRequestPartitionId) { 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<? extends IQueryParameterType> theList,
SearchFilterParser.CompareOperation theOperation, RequestDetails theRequestDetails,
RequestPartitionId theRequestPartitionId, SearchQueryBuilder theSqlBuilder) {
String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName()); String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName());
UriPredicateBuilder join = mySqlBuilder.addUriPredicateBuilder(theSourceJoinColumn); UriPredicateBuilder join = theSqlBuilder.addUriPredicateBuilder(theSourceJoinColumn);
if (theList.get(0).getMissing() != null) { if (theList.get(0).getMissing() != null) {
return join.createPredicateParamMissingForNonReference(theResourceName, paramName, theList.get(0).getMissing(), theRequestPartitionId); return join.createPredicateParamMissingForNonReference(theResourceName, paramName, theList.get(0).getMissing(), theRequestPartitionId);
@ -1140,10 +1470,7 @@ public class QueryStack {
case REFERENCE: case REFERENCE:
for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) { for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
if (isEligibleForContainedResourceSearch(nextAnd)) { if (isEligibleForContainedResourceSearch(nextAnd)) {
andPredicates.add(toOrPredicate( andPredicates.add(createPredicateReferenceForContainedResource(theSourceJoinColumn, theResourceName, theParamName, new ArrayList<>(), nextParamDef, nextAnd, null, theRequest, theRequestPartitionId));
createPredicateReference(theSourceJoinColumn, theResourceName, theParamName, new ArrayList<>(), nextAnd, null, theRequest, theRequestPartitionId),
createPredicateReferenceForContainedResource(theSourceJoinColumn, theResourceName, theParamName, new ArrayList<>(), nextParamDef, nextAnd, null, theRequest, theRequestPartitionId)
));
} else { } else {
andPredicates.add(createPredicateReference(theSourceJoinColumn, theResourceName, theParamName, new ArrayList<>(), nextAnd, null, theRequest, theRequestPartitionId)); andPredicates.add(createPredicateReference(theSourceJoinColumn, theResourceName, theParamName, new ArrayList<>(), nextAnd, null, theRequest, theRequestPartitionId));
} }

View File

@ -265,7 +265,7 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder {
} }
@Nonnull @Nonnull
private Condition createPredicateSourcePaths(List<String> thePathsToMatch) { public Condition createPredicateSourcePaths(List<String> thePathsToMatch) {
return toEqualToOrInPredicate(myColumnSrcPath, generatePlaceholders(thePathsToMatch)); return toEqualToOrInPredicate(myColumnSrcPath, generatePlaceholders(thePathsToMatch));
} }

View File

@ -140,6 +140,7 @@ import org.hl7.fhir.r4.model.NamingSystem;
import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.OperationDefinition; import org.hl7.fhir.r4.model.OperationDefinition;
import org.hl7.fhir.r4.model.Organization; 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.Patient;
import org.hl7.fhir.r4.model.Practitioner; import org.hl7.fhir.r4.model.Practitioner;
import org.hl7.fhir.r4.model.PractitionerRole; import org.hl7.fhir.r4.model.PractitionerRole;
@ -370,6 +371,9 @@ public abstract class BaseJpaR4Test extends BaseJpaTest implements ITestDataBuil
@Qualifier("myOrganizationDaoR4") @Qualifier("myOrganizationDaoR4")
protected IFhirResourceDao<Organization> myOrganizationDao; protected IFhirResourceDao<Organization> myOrganizationDao;
@Autowired @Autowired
@Qualifier("myOrganizationAffiliationDaoR4")
protected IFhirResourceDao<OrganizationAffiliation> myOrganizationAffiliationDao;
@Autowired
protected DatabaseBackedPagingProvider myPagingProvider; protected DatabaseBackedPagingProvider myPagingProvider;
@Autowired @Autowired
@Qualifier("myBinaryDaoR4") @Qualifier("myBinaryDaoR4")

View File

@ -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.MatchUrlService;
import ca.uhn.fhir.jpa.searchparam.ResourceSearch; import ca.uhn.fhir.jpa.searchparam.ResourceSearch;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.util.SqlQuery;
import ca.uhn.fhir.parser.StrictErrorHandler; import ca.uhn.fhir.parser.StrictErrorHandler;
import ca.uhn.fhir.rest.api.server.IBundleProvider; 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.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Device; import org.hl7.fhir.r4.model.Device;
import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.IdType;
@ -25,9 +27,11 @@ import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import static org.apache.commons.lang3.StringUtils.countMatches;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.contains;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
public class ChainingR4SearchTest extends BaseJpaR4Test { public class ChainingR4SearchTest extends BaseJpaR4Test {
@ -46,7 +50,6 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
myDaoConfig.setAllowContainsSearches(new DaoConfig().isAllowContainsSearches()); myDaoConfig.setAllowContainsSearches(new DaoConfig().isAllowContainsSearches());
myDaoConfig.setIndexMissingFields(new DaoConfig().getIndexMissingFields()); myDaoConfig.setIndexMissingFields(new DaoConfig().getIndexMissingFields());
myModelConfig.setIndexOnContainedResources(false);
myModelConfig.setIndexOnContainedResources(new ModelConfig().isIndexOnContainedResources()); myModelConfig.setIndexOnContainedResources(new ModelConfig().isIndexOnContainedResources());
myModelConfig.setIndexOnContainedResourcesRecursively(new ModelConfig().isIndexOnContainedResourcesRecursively()); myModelConfig.setIndexOnContainedResourcesRecursively(new ModelConfig().isIndexOnContainedResourcesRecursively());
} }
@ -57,12 +60,11 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
myDaoConfig.setAllowMultipleDelete(true); myDaoConfig.setAllowMultipleDelete(true);
myDaoConfig.setSearchPreFetchThresholds(new DaoConfig().getSearchPreFetchThresholds()); myDaoConfig.setSearchPreFetchThresholds(new DaoConfig().getSearchPreFetchThresholds());
myModelConfig.setIndexOnContainedResources(true);
myDaoConfig.setReuseCachedSearchResultsForMillis(null); myDaoConfig.setReuseCachedSearchResultsForMillis(null);
} }
@Test @Test
public void testShouldResolveATwoLinkChainWithStandAloneResources() throws Exception { public void testShouldResolveATwoLinkChainWithStandAloneResourcesWithoutContainedResourceIndexing() throws Exception {
// setup // setup
IIdType oid1; IIdType oid1;
@ -78,6 +80,43 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
obs.getSubject().setReference(p.getId()); obs.getSubject().setReference(p.getId());
oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); 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<String> 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"; String url = "/Observation?subject.name=Smith";
@ -93,6 +132,8 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
@Test @Test
public void testShouldResolveATwoLinkChainWithAContainedResource() throws Exception { public void testShouldResolveATwoLinkChainWithAContainedResource() throws Exception {
// setup // setup
myModelConfig.setIndexOnContainedResources(true);
IIdType oid1; IIdType oid1;
{ {
@ -107,6 +148,11 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
obs.getSubject().setReference("#pat"); obs.getSubject().setReference("#pat");
oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); 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"; String url = "/Observation?subject.name=Smith";
@ -119,12 +165,45 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
assertThat(oids, contains(oid1.getIdPart())); 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<String> oids = searchAndReturnUnqualifiedVersionlessIdValues(url);
// validate
assertEquals(0L, oids.size());
}
@Test @Test
@Disabled @Disabled
public void testShouldResolveATwoLinkChainWithQualifiersWithAContainedResource() throws Exception { public void testShouldResolveATwoLinkChainWithQualifiersWithAContainedResource() throws Exception {
// TODO: This test fails because of a known limitation in qualified searches over contained resources. // 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. // Type information for intermediate resources in the chain is not being retained in the indexes.
// setup // setup
myModelConfig.setIndexOnContainedResources(true);
IIdType oid1; IIdType oid1;
{ {
@ -151,6 +230,9 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
obs2.getSubject().setReference("#loc"); obs2.getSubject().setReference("#loc");
myObservationDao.create(obs2, mySrd); 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"; String url = "/Observation?subject:Patient.name=Smith";
@ -168,6 +250,8 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
// Adding support for this case in SMILE-3151 // Adding support for this case in SMILE-3151
// setup // setup
myModelConfig.setIndexOnContainedResources(true);
IIdType oid1; IIdType oid1;
IIdType orgId; IIdType orgId;
@ -188,6 +272,9 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
obs.getSubject().setReference("#pat"); obs.getSubject().setReference("#pat");
oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); 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(); String url = "/Observation?subject.organization=" + orgId.getValueAsString();
@ -201,7 +288,49 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
} }
@Test @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<String> oids = searchAndReturnUnqualifiedVersionlessIdValues(url);
// validate
assertEquals(1L, oids.size());
assertThat(oids, contains(oid1.getIdPart()));
}
@Test
public void testShouldResolveAThreeLinkChainWhereAllResourcesStandAloneWithoutContainedResourceIndexing() throws Exception {
// setup // setup
IIdType oid1; IIdType oid1;
@ -223,6 +352,77 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
obs.getSubject().setReference(p.getId()); obs.getSubject().setReference(p.getId());
oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); 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<String> 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"; 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 // This is the case that is most relevant to SMILE-2899
// setup // setup
myModelConfig.setIndexOnContainedResources(true);
IIdType oid1; IIdType oid1;
{ {
@ -259,6 +461,9 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
obs.getSubject().setReference(p.getId()); obs.getSubject().setReference(p.getId());
oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); 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"; String url = "/Observation?subject.organization.name=HealthCo";
@ -276,6 +481,8 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
// Adding support for this case in SMILE-3151 // Adding support for this case in SMILE-3151
// setup // setup
myModelConfig.setIndexOnContainedResources(true);
IIdType oid1; IIdType oid1;
{ {
@ -295,6 +502,9 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
obs.getSubject().setReference("#pat"); obs.getSubject().setReference("#pat");
oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); 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"; String url = "/Observation?subject.organization.name=HealthCo";
@ -307,10 +517,50 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
assertThat(oids, contains(oid1.getIdPart())); 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<String> oids = searchAndReturnUnqualifiedVersionlessIdValues(url);
// validate
assertEquals(0L, oids.size());
}
@Test @Test
public void testShouldResolveAThreeLinkChainWithAllContainedResources() throws Exception { public void testShouldResolveAThreeLinkChainWithAllContainedResources() throws Exception {
// setup // setup
myModelConfig.setIndexOnContainedResources(true);
myModelConfig.setIndexOnContainedResourcesRecursively(true); myModelConfig.setIndexOnContainedResourcesRecursively(true);
IIdType oid1; IIdType oid1;
@ -332,6 +582,9 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
obs.getSubject().setReference("#pat"); obs.getSubject().setReference("#pat");
oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); 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"; String url = "/Observation?subject.organization.name=HealthCo";
@ -350,6 +603,8 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
public void testShouldResolveAThreeLinkChainWithQualifiersWhereAllResourcesStandAlone() throws Exception { public void testShouldResolveAThreeLinkChainWithQualifiersWhereAllResourcesStandAlone() throws Exception {
// setup // setup
myModelConfig.setIndexOnContainedResources(true);
IIdType oid1; IIdType oid1;
{ {
@ -379,6 +634,9 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
oid1 = myObservationDao.create(obs1, mySrd).getId().toUnqualifiedVersionless(); oid1 = myObservationDao.create(obs1, mySrd).getId().toUnqualifiedVersionless();
myObservationDao.create(obs2, mySrd); 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"; 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 // This is the case that is most relevant to SMILE-2899
// setup // setup
myModelConfig.setIndexOnContainedResources(true);
IIdType oid1; IIdType oid1;
{ {
@ -430,6 +690,9 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
obs2.getCode().setText("Observation 2"); obs2.getCode().setText("Observation 2");
obs2.getSubject().setReference(d.getId()); obs2.getSubject().setReference(d.getId());
myObservationDao.create(obs2, mySrd); 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"; 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 // Adding support for this case in SMILE-3151
// setup // setup
myModelConfig.setIndexOnContainedResources(true);
IIdType oid1; IIdType oid1;
{ {
@ -477,6 +742,9 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
obs2.getSubject().setReference("#dev"); obs2.getSubject().setReference("#dev");
myObservationDao.create(obs2, mySrd); 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"; 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 // Adding support for this case in SMILE-3151
// setup // setup
myModelConfig.setIndexOnContainedResources(true);
IIdType oid1; IIdType oid1;
{ {
@ -530,6 +800,9 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
obs2.getSubject().setReference("#loc"); obs2.getSubject().setReference("#loc");
myObservationDao.create(obs2, mySrd); 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"; 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. // Type information for intermediate resources in the chain is not being retained in the indexes.
// setup // setup
myModelConfig.setIndexOnContainedResources(true);
myModelConfig.setIndexOnContainedResourcesRecursively(true); myModelConfig.setIndexOnContainedResourcesRecursively(true);
IIdType oid1; IIdType oid1;
@ -588,6 +862,9 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
obs2.getSubject().setReference("#dev"); obs2.getSubject().setReference("#dev");
myObservationDao.create(obs2, mySrd); 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"; String url = "/Observation?subject:Patient.organization:Organization.name=HealthCo";
@ -606,6 +883,8 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
public void testShouldResolveAFourLinkChainWhereAllResourcesStandAlone() throws Exception { public void testShouldResolveAFourLinkChainWhereAllResourcesStandAlone() throws Exception {
// setup // setup
myModelConfig.setIndexOnContainedResources(true);
IIdType oid1; IIdType oid1;
{ {
@ -630,6 +909,9 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
obs.getSubject().setReference(p.getId()); obs.getSubject().setReference(p.getId());
oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); 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"; String url = "/Observation?subject.organization.partof.name=HealthCo";
@ -646,6 +928,8 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
public void testShouldResolveAFourLinkChainWhereTheLastReferenceIsContained() throws Exception { public void testShouldResolveAFourLinkChainWhereTheLastReferenceIsContained() throws Exception {
// setup // setup
myModelConfig.setIndexOnContainedResources(true);
IIdType oid1; IIdType oid1;
{ {
@ -670,6 +954,9 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
obs.getSubject().setReference(p.getId()); obs.getSubject().setReference(p.getId());
oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); 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"; String url = "/Observation?subject.organization.partof.name=HealthCo";
@ -686,6 +973,7 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
public void testShouldResolveAFourLinkChainWhereTheLastTwoReferencesAreContained() throws Exception { public void testShouldResolveAFourLinkChainWhereTheLastTwoReferencesAreContained() throws Exception {
// setup // setup
myModelConfig.setIndexOnContainedResources(true);
myModelConfig.setIndexOnContainedResourcesRecursively(true); myModelConfig.setIndexOnContainedResourcesRecursively(true);
IIdType oid1; IIdType oid1;
@ -711,6 +999,9 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
obs.getSubject().setReference(p.getId()); obs.getSubject().setReference(p.getId());
oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); 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"; String url = "/Observation?subject.organization.partof.name=HealthCo";
@ -727,6 +1018,8 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
public void testShouldResolveAFourLinkChainWithAContainedResourceInTheMiddle() throws Exception { public void testShouldResolveAFourLinkChainWithAContainedResourceInTheMiddle() throws Exception {
// setup // setup
myModelConfig.setIndexOnContainedResources(true);
IIdType oid1; IIdType oid1;
{ {
@ -755,6 +1048,9 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless();
myCaptureQueriesListener.logInsertQueries(); 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"; String url = "/Observation?subject.organization.partof.name=HealthCo";
@ -773,6 +1069,7 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
public void testShouldResolveAFourLinkChainWhereTheFirstTwoReferencesAreContained() throws Exception { public void testShouldResolveAFourLinkChainWhereTheFirstTwoReferencesAreContained() throws Exception {
// setup // setup
myModelConfig.setIndexOnContainedResources(true);
myModelConfig.setIndexOnContainedResourcesRecursively(true); myModelConfig.setIndexOnContainedResourcesRecursively(true);
IIdType oid1; IIdType oid1;
@ -799,6 +1096,54 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
obs.getSubject().setReference("#pat"); obs.getSubject().setReference("#pat");
oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); 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<String> 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"; String url = "/Observation?subject.organization.partof.name=HealthCo";
@ -815,6 +1160,7 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
public void testShouldResolveAFourLinkChainWhereAllReferencesAreContained() throws Exception { public void testShouldResolveAFourLinkChainWhereAllReferencesAreContained() throws Exception {
// setup // setup
myModelConfig.setIndexOnContainedResources(true);
myModelConfig.setIndexOnContainedResourcesRecursively(true); myModelConfig.setIndexOnContainedResourcesRecursively(true);
IIdType oid1; IIdType oid1;
@ -840,6 +1186,9 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
obs.getSubject().setReference("#pat"); obs.getSubject().setReference("#pat");
oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); 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"; String url = "/Observation?subject.organization.partof.name=HealthCo";
@ -852,6 +1201,64 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
assertThat(oids, contains(oid1.getIdPart())); 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<SqlQuery> selectQueries = myCaptureQueriesListener.getSelectQueriesForCurrentThread();
assertEquals(1, selectQueries.size());
String sqlQuery = selectQueries.get(0).getSql(true, true).toLowerCase();
assertEquals(theExpectedNumberOfUnions, countMatches(sqlQuery, "union"), sqlQuery);
}
private List<String> searchAndReturnUnqualifiedVersionlessIdValues(String theUrl) throws IOException { private List<String> searchAndReturnUnqualifiedVersionlessIdValues(String theUrl) throws IOException {
List<String> ids = new ArrayList<>(); List<String> ids = new ArrayList<>();

View File

@ -166,6 +166,7 @@ public class SearchCoordinatorSvcImplTest {
@Test @Test
public void testAsyncSearchFailDuringSearchSameCoordinator() { public void testAsyncSearchFailDuringSearchSameCoordinator() {
initSearches(); initSearches();
initAsyncSearches();
SearchParameterMap params = new SearchParameterMap(); SearchParameterMap params = new SearchParameterMap();
params.add("name", new StringParam("ANAME")); params.add("name", new StringParam("ANAME"));
@ -186,6 +187,7 @@ public class SearchCoordinatorSvcImplTest {
@Test @Test
public void testAsyncSearchLargeResultSetBigCountSameCoordinator() { public void testAsyncSearchLargeResultSetBigCountSameCoordinator() {
initSearches(); initSearches();
initAsyncSearches();
List<ResourcePersistentId> allResults = new ArrayList<>(); List<ResourcePersistentId> allResults = new ArrayList<>();
doAnswer(t -> { doAnswer(t -> {
@ -281,6 +283,7 @@ public class SearchCoordinatorSvcImplTest {
@Test @Test
public void testAsyncSearchLargeResultSetSameCoordinator() { public void testAsyncSearchLargeResultSetSameCoordinator() {
initSearches(); initSearches();
initAsyncSearches();
SearchParameterMap params = new SearchParameterMap(); SearchParameterMap params = new SearchParameterMap();
params.add("name", new StringParam("ANAME")); params.add("name", new StringParam("ANAME"));
@ -308,7 +311,9 @@ public class SearchCoordinatorSvcImplTest {
when(mySearchBuilderFactory.newSearchBuilder(any(), any(), any())).thenReturn(mySearchBuilder); when(mySearchBuilderFactory.newSearchBuilder(any(), any(), any())).thenReturn(mySearchBuilder);
when(myTxManager.getTransaction(any())).thenReturn(mock(TransactionStatus.class)); 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->{ 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); RequestDetails requestDetails = t.getArgument(0, RequestDetails.class);
Search search = t.getArgument(1, Search.class); Search search = t.getArgument(1, Search.class);
@ -374,6 +379,7 @@ public class SearchCoordinatorSvcImplTest {
@Test @Test
public void testAsyncSearchLargeResultSetSecondRequestSameCoordinator() { public void testAsyncSearchLargeResultSetSecondRequestSameCoordinator() {
initSearches(); initSearches();
initAsyncSearches();
SearchParameterMap params = new SearchParameterMap(); SearchParameterMap params = new SearchParameterMap();
params.add("name", new StringParam("ANAME")); params.add("name", new StringParam("ANAME"));
@ -412,6 +418,7 @@ public class SearchCoordinatorSvcImplTest {
@Test @Test
public void testAsyncSearchSmallResultSetSameCoordinator() { public void testAsyncSearchSmallResultSetSameCoordinator() {
initSearches(); initSearches();
initAsyncSearches();
SearchParameterMap params = new SearchParameterMap(); SearchParameterMap params = new SearchParameterMap();
params.add("name", new StringParam("ANAME")); params.add("name", new StringParam("ANAME"));