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:
parent
283ff19375
commit
48caff30d9
|
@ -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."
|
|
@ -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<? 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;
|
||||
for (IQueryParameterType next : theNextAnd) {
|
||||
|
@ -299,11 +310,11 @@ public class QueryStack {
|
|||
List<RuntimeSearchParam> 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<? extends IQueryParameterType> 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<? extends IQueryParameterType> theList,
|
||||
SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId, SearchQueryBuilder theSqlBuilder) {
|
||||
|
||||
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();
|
||||
boolean cacheHit = predicateBuilderLookupResult.isCacheHit();
|
||||
|
||||
|
@ -577,10 +597,16 @@ public class QueryStack {
|
|||
public Condition createPredicateNumber(@Nullable DbColumn theSourceJoinColumn, String theResourceName,
|
||||
String theSpnamePrefix, RuntimeSearchParam theSearchParam, List<? extends IQueryParameterType> 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<? extends IQueryParameterType> 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<? extends IQueryParameterType> 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<? extends IQueryParameterType> 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<Condition> 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<String> theQualifiers,
|
||||
List<? extends IQueryParameterType> 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<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,
|
||||
String theResourceName, String theParamName, List<String> theQualifiers, RuntimeSearchParam theSearchParam,
|
||||
List<? extends IQueryParameterType> 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<PredicateBuilderTypeEnum> 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<List<ChainElement>,Set<LeafNodeDefinition>> chains = chainExtractor.getChains();
|
||||
|
||||
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();
|
||||
List<IQueryParameterType> trimmedParameters = Lists.newArrayList();
|
||||
IQueryParameterType qp = null;
|
||||
collateChainedSearchOptions(referenceLinks, nextChain, leafNodes);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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<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;
|
||||
|
||||
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<? extends IQueryParameterType> 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<? extends IQueryParameterType> 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<? extends IQueryParameterType> 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<? extends IQueryParameterType> theList,
|
||||
SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId, SearchQueryBuilder theSqlBuilder) {
|
||||
|
||||
List<IQueryParameterType> 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<? extends IQueryParameterType> 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<? extends IQueryParameterType> 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<? extends IQueryParameterType> 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));
|
||||
}
|
||||
|
|
|
@ -265,7 +265,7 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder {
|
|||
}
|
||||
|
||||
@Nonnull
|
||||
private Condition createPredicateSourcePaths(List<String> thePathsToMatch) {
|
||||
public Condition createPredicateSourcePaths(List<String> thePathsToMatch) {
|
||||
return toEqualToOrInPredicate(myColumnSrcPath, generatePlaceholders(thePathsToMatch));
|
||||
}
|
||||
|
||||
|
|
|
@ -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<Organization> myOrganizationDao;
|
||||
@Autowired
|
||||
@Qualifier("myOrganizationAffiliationDaoR4")
|
||||
protected IFhirResourceDao<OrganizationAffiliation> myOrganizationAffiliationDao;
|
||||
@Autowired
|
||||
protected DatabaseBackedPagingProvider myPagingProvider;
|
||||
@Autowired
|
||||
@Qualifier("myBinaryDaoR4")
|
||||
|
|
|
@ -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<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";
|
||||
|
@ -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<String> 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<String> 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<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";
|
||||
|
@ -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<String> 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<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";
|
||||
|
@ -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<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 {
|
||||
List<String> ids = new ArrayList<>();
|
||||
|
||||
|
|
|
@ -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<ResourcePersistentId> 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"));
|
||||
|
|
Loading…
Reference in New Issue