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.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));
}

View File

@ -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));
}

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

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.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<>();

View File

@ -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"));