Jr 20211013 chained references (#3079)

* Create index entries for outbound references of contained resources

* build query for chained reference

* fix case where the contained reference is an explicit id rather than a continued chain

* fix contained index to use path names not search param names

* make qualified search work

* cleanup and changelog

* code review

* fix broken tests
This commit is contained in:
JasonRoberts-smile 2021-10-19 14:07:29 -04:00 committed by GitHub
parent 34010fa78d
commit d07764e8e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 397 additions and 67 deletions

View File

@ -0,0 +1,6 @@
---
type: add
issue: 3088
jira: SMILE-3151
title: "Previously, chained searches were not able to traverse reference fields within contained resources.
This enhancement adds the ability to traverse the reference fields of contained resources when those fields refer to discrete resources."

View File

@ -93,6 +93,7 @@ import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn;
import org.apache.commons.collections4.BidiMap; import org.apache.commons.collections4.BidiMap;
import org.apache.commons.collections4.bidimap.DualHashBidiMap; import org.apache.commons.collections4.bidimap.DualHashBidiMap;
import org.apache.commons.collections4.bidimap.UnmodifiableBidiMap; import org.apache.commons.collections4.bidimap.UnmodifiableBidiMap;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Pair;
@ -109,7 +110,6 @@ import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.Supplier; import java.util.function.Supplier;
@ -231,7 +231,7 @@ public class QueryStack {
BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder(); BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder();
ResourceLinkPredicateBuilder sortPredicateBuilder = mySqlBuilder.addReferencePredicateBuilder(this, firstPredicateBuilder.getResourceIdColumn()); ResourceLinkPredicateBuilder sortPredicateBuilder = mySqlBuilder.addReferencePredicateBuilder(this, firstPredicateBuilder.getResourceIdColumn());
Condition pathPredicate = sortPredicateBuilder.createPredicateSourcePaths(theResourceName, theParamName); Condition pathPredicate = sortPredicateBuilder.createPredicateSourcePaths(theResourceName, theParamName, new ArrayList<>());
mySqlBuilder.addPredicate(pathPredicate); mySqlBuilder.addPredicate(pathPredicate);
mySqlBuilder.addSortNumeric(sortPredicateBuilder.getColumnTargetResourceId(), theAscending); mySqlBuilder.addSortNumeric(sortPredicateBuilder.getColumnTargetResourceId(), theAscending);
} }
@ -459,7 +459,7 @@ public class QueryStack {
String chain = (theFilter.getParamPath().getNext() != null) ? theFilter.getParamPath().getNext().toString() : null; String chain = (theFilter.getParamPath().getNext() != null) ? theFilter.getParamPath().getNext().toString() : null;
String value = theFilter.getValue(); String value = theFilter.getValue();
ReferenceParam referenceParam = new ReferenceParam(resourceType, chain, value); ReferenceParam referenceParam = new ReferenceParam(resourceType, chain, value);
return theQueryStack3.createPredicateReference(null, theResourceName, paramName, Collections.singletonList(referenceParam), operation, theRequest, theRequestPartitionId); return theQueryStack3.createPredicateReference(null, theResourceName, paramName, new ArrayList<>(), Collections.singletonList(referenceParam), operation, theRequest, theRequestPartitionId);
} else if (typeEnum == RestSearchParameterTypeEnum.QUANTITY) { } else if (typeEnum == RestSearchParameterTypeEnum.QUANTITY) {
return theQueryStack3.createPredicateQuantity(null, theResourceName, null, searchParam, Collections.singletonList(new QuantityParam(theFilter.getValue())), theFilter.getOperation(), theRequestPartitionId); return theQueryStack3.createPredicateQuantity(null, theResourceName, null, searchParam, Collections.singletonList(new QuantityParam(theFilter.getValue())), theFilter.getOperation(), theRequestPartitionId);
} else if (typeEnum == RestSearchParameterTypeEnum.COMPOSITE) { } else if (typeEnum == RestSearchParameterTypeEnum.COMPOSITE) {
@ -564,7 +564,7 @@ public class QueryStack {
ResourceLinkPredicateBuilder join = mySqlBuilder.addReferencePredicateBuilderReversed(this, theSourceJoinColumn); ResourceLinkPredicateBuilder join = mySqlBuilder.addReferencePredicateBuilderReversed(this, theSourceJoinColumn);
Condition partitionPredicate = join.createPartitionIdPredicate(theRequestPartitionId); Condition partitionPredicate = join.createPartitionIdPredicate(theRequestPartitionId);
List<String> paths = join.createResourceLinkPaths(targetResourceType, paramReference); List<String> paths = join.createResourceLinkPaths(targetResourceType, paramReference, new ArrayList<>());
Condition typePredicate = BinaryCondition.equalTo(join.getColumnTargetResourceType(), mySqlBuilder.generatePlaceholder(theResourceType)); Condition typePredicate = BinaryCondition.equalTo(join.getColumnTargetResourceType(), mySqlBuilder.generatePlaceholder(theResourceType));
Condition pathPredicate = toEqualToOrInPredicate(join.getColumnSourcePath(), mySqlBuilder.generatePlaceholders(paths)); Condition pathPredicate = toEqualToOrInPredicate(join.getColumnSourcePath(), mySqlBuilder.generatePlaceholders(paths));
Condition linkedPredicate = searchForIdsWithAndOr(join.getColumnSrcResourceId(), targetResourceType, parameterName, Collections.singletonList(orValues), theRequest, theRequestPartitionId, SearchContainedModeEnum.FALSE); Condition linkedPredicate = searchForIdsWithAndOr(join.getColumnSrcResourceId(), targetResourceType, parameterName, Collections.singletonList(orValues), theRequest, theRequestPartitionId, SearchContainedModeEnum.FALSE);
@ -662,14 +662,12 @@ public class QueryStack {
public Condition createPredicateReference(@Nullable DbColumn theSourceJoinColumn, public Condition createPredicateReference(@Nullable DbColumn theSourceJoinColumn,
String theResourceName, String theResourceName,
String theParamName, String theParamName,
List<String> theQualifiers,
List<? extends IQueryParameterType> theList, List<? extends IQueryParameterType> theList,
SearchFilterParser.CompareOperation theOperation, SearchFilterParser.CompareOperation theOperation,
RequestDetails theRequest, RequestDetails theRequest,
RequestPartitionId theRequestPartitionId) { RequestPartitionId theRequestPartitionId) {
// This just to ensure the chain has been split correctly
assert theParamName.contains(".") == false;
if ((theOperation != null) && if ((theOperation != null) &&
(theOperation != SearchFilterParser.CompareOperation.eq) && (theOperation != SearchFilterParser.CompareOperation.eq) &&
(theOperation != SearchFilterParser.CompareOperation.ne)) { (theOperation != SearchFilterParser.CompareOperation.ne)) {
@ -683,7 +681,7 @@ public class QueryStack {
} }
ResourceLinkPredicateBuilder predicateBuilder = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.REFERENCE, theSourceJoinColumn, theParamName, () -> mySqlBuilder.addReferencePredicateBuilder(this, theSourceJoinColumn)).getResult(); ResourceLinkPredicateBuilder predicateBuilder = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.REFERENCE, theSourceJoinColumn, theParamName, () -> mySqlBuilder.addReferencePredicateBuilder(this, theSourceJoinColumn)).getResult();
return predicateBuilder.createPredicate(theRequest, theResourceName, theParamName, theList, theOperation, theRequestPartitionId); return predicateBuilder.createPredicate(theRequest, theResourceName, theParamName, theQualifiers, theList, theOperation, theRequestPartitionId);
} }
public Condition createPredicateReferenceForContainedResource(@Nullable DbColumn theSourceJoinColumn, public Condition createPredicateReferenceForContainedResource(@Nullable DbColumn theSourceJoinColumn,
@ -695,12 +693,14 @@ public class QueryStack {
String targetChain = null; String targetChain = null;
String targetParamName = null; String targetParamName = null;
String headQualifier = null;
String targetQualifier = null; String targetQualifier = null;
String targetValue = null; String targetValue = null;
RuntimeSearchParam targetParamDefinition = null; RuntimeSearchParam targetParamDefinition = null;
ArrayList<IQueryParameterType> orValues = Lists.newArrayList(); ArrayList<IQueryParameterType> orValues = Lists.newArrayList();
List<IQueryParameterType> trimmedParameters = Lists.newArrayList();
IQueryParameterType qp = null; IQueryParameterType qp = null;
for (int orIdx = 0; orIdx < theList.size(); orIdx++) { for (int orIdx = 0; orIdx < theList.size(); orIdx++) {
@ -715,18 +715,28 @@ public class QueryStack {
targetChain = referenceParam.getChain(); targetChain = referenceParam.getChain();
targetParamName = targetChain; targetParamName = targetChain;
targetValue = nextOr.getValueAsQueryToken(myFhirContext); targetValue = nextOr.getValueAsQueryToken(myFhirContext);
headQualifier = referenceParam.getResourceType();
int qualifierIndex = targetChain.indexOf(':'); String targetNextChain = null;
if (qualifierIndex != -1) { int linkIndex = targetChain.indexOf('.');
targetParamName = targetChain.substring(0, qualifierIndex); if (linkIndex != -1) {
targetQualifier = targetChain.substring(qualifierIndex); targetParamName = targetChain.substring(0, linkIndex);
targetNextChain = targetChain.substring(linkIndex+1);
} }
int qualifierIndex = targetParamName.indexOf(':');
if (qualifierIndex != -1) {
targetParamName = targetParamName.substring(0, qualifierIndex);
targetQualifier = targetParamName.substring(qualifierIndex);
}
trimmedParameters.add(new ReferenceParam(targetQualifier, targetNextChain, referenceParam.getValue()));
// 2. find out the data type // 2. find out the data type
if (targetParamDefinition == null) { if (targetParamDefinition == null) {
Iterator<String> it = theSearchParam.getTargets().iterator(); for (String nextTarget : theSearchParam.getTargets()) {
while (it.hasNext()) { if (!referenceParam.hasResourceType() || referenceParam.getResourceType().equals(nextTarget)) {
targetParamDefinition = mySearchParamRegistry.getActiveSearchParam(it.next(), targetParamName); targetParamDefinition = mySearchParamRegistry.getActiveSearchParam(nextTarget, targetParamName);
}
if (targetParamDefinition != null) if (targetParamDefinition != null)
break; break;
} }
@ -736,6 +746,10 @@ public class QueryStack {
throw new InvalidRequestException("Unknown search parameter name: " + theSearchParam.getName() + '.' + targetParamName + "."); throw new InvalidRequestException("Unknown search parameter name: " + theSearchParam.getName() + '.' + targetParamName + ".");
} }
if (RestSearchParameterTypeEnum.REFERENCE.equals(targetParamDefinition.getParamType())) {
continue;
}
qp = toParameterType(targetParamDefinition); qp = toParameterType(targetParamDefinition);
qp.setValueAsQueryToken(myFhirContext, targetParamName, targetQualifier, targetValue); qp.setValueAsQueryToken(myFhirContext, targetParamName, targetQualifier, targetValue);
orValues.add(qp); orValues.add(qp);
@ -746,6 +760,8 @@ public class QueryStack {
throw new InvalidRequestException("Unknown search parameter name: " + theSearchParam.getName() + "."); throw new InvalidRequestException("Unknown search parameter name: " + theSearchParam.getName() + ".");
} }
List<String> qualifiers= Collections.singletonList(headQualifier);
// 3. create the query // 3. create the query
Condition containedCondition = null; Condition containedCondition = null;
@ -778,8 +794,11 @@ public class QueryStack {
containedCondition = createPredicateUri(theSourceJoinColumn, theResourceName, spnamePrefix, targetParamDefinition, containedCondition = createPredicateUri(theSourceJoinColumn, theResourceName, spnamePrefix, targetParamDefinition,
orValues, theOperation, theRequest, theRequestPartitionId); orValues, theOperation, theRequest, theRequestPartitionId);
break; break;
case HAS:
case REFERENCE: case REFERENCE:
String chainedParamName = theParamName + "." + targetParamName;
containedCondition = createPredicateReference(theSourceJoinColumn, theResourceName, chainedParamName, qualifiers, trimmedParameters, theOperation, theRequest, theRequestPartitionId);
break;
case HAS:
case SPECIAL: case SPECIAL:
default: default:
throw new InvalidRequestException( throw new InvalidRequestException(
@ -1123,16 +1142,12 @@ public class QueryStack {
// until the complete fix is available. // until the complete fix is available.
andPredicates.add(createPredicateReferenceForContainedResource(null, theResourceName, theParamName, nextParamDef, nextAnd, null, theRequest, theRequestPartitionId)); andPredicates.add(createPredicateReferenceForContainedResource(null, theResourceName, theParamName, nextParamDef, nextAnd, null, theRequest, theRequestPartitionId));
} else if (isEligibleForContainedResourceSearch(nextAnd)) { } else if (isEligibleForContainedResourceSearch(nextAnd)) {
// TODO for now, restrict contained reference traversal to the last reference in the chain
// We don't seem to be indexing the outbound references of a contained resource, so we can't
// include them in search chains.
// It would be nice to eventually relax this constraint, but no client seems to be asking for it.
andPredicates.add(toOrPredicate( andPredicates.add(toOrPredicate(
createPredicateReference(theSourceJoinColumn, theResourceName, theParamName, nextAnd, null, theRequest, theRequestPartitionId), createPredicateReference(theSourceJoinColumn, theResourceName, theParamName, new ArrayList<>(), nextAnd, null, theRequest, theRequestPartitionId),
createPredicateReferenceForContainedResource(theSourceJoinColumn, theResourceName, theParamName, nextParamDef, nextAnd, null, theRequest, theRequestPartitionId) createPredicateReferenceForContainedResource(theSourceJoinColumn, theResourceName, theParamName, nextParamDef, nextAnd, null, theRequest, theRequestPartitionId)
)); ));
} else { } else {
andPredicates.add(createPredicateReference(theSourceJoinColumn, theResourceName, theParamName, nextAnd, null, theRequest, theRequestPartitionId)); andPredicates.add(createPredicateReference(theSourceJoinColumn, theResourceName, theParamName, new ArrayList<>(), nextAnd, null, theRequest, theRequestPartitionId));
} }
} }
break; break;
@ -1215,8 +1230,8 @@ public class QueryStack {
return myModelConfig.isIndexOnContainedResources() && return myModelConfig.isIndexOnContainedResources() &&
nextAnd.stream() nextAnd.stream()
.filter(t -> t instanceof ReferenceParam) .filter(t -> t instanceof ReferenceParam)
.map(t -> (ReferenceParam) t) .map(t -> ((ReferenceParam) t).getChain())
.noneMatch(t -> t.getChain().contains(".")); .anyMatch(StringUtils::isNotBlank);
} }
public void addPredicateCompositeUnique(String theIndexString, RequestPartitionId theRequestPartitionId) { public void addPredicateCompositeUnique(String theIndexString, RequestPartitionId theRequestPartitionId) {

View File

@ -81,7 +81,9 @@ import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.ListIterator; import java.util.ListIterator;
import java.util.Set; import java.util.Set;
@ -159,7 +161,7 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder {
} }
} }
public Condition createPredicate(RequestDetails theRequest, String theResourceType, String theParamName, List<? extends IQueryParameterType> theReferenceOrParamList, SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId) { public Condition createPredicate(RequestDetails theRequest, String theResourceType, String theParamName, List<String> theQualifiers, List<? extends IQueryParameterType> theReferenceOrParamList, SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId) {
List<IIdType> targetIds = new ArrayList<>(); List<IIdType> targetIds = new ArrayList<>();
List<String> targetQualifiedUrls = new ArrayList<>(); List<String> targetQualifiedUrls = new ArrayList<>();
@ -195,7 +197,7 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder {
* Handle chained search, e.g. Patient?organization.name=Kwik-e-mart * Handle chained search, e.g. Patient?organization.name=Kwik-e-mart
*/ */
return addPredicateReferenceWithChain(theResourceType, theParamName, theReferenceOrParamList, ref, theRequest, theRequestPartitionId); return addPredicateReferenceWithChain(theResourceType, theParamName, theQualifiers, theReferenceOrParamList, ref, theRequest, theRequestPartitionId);
} }
@ -211,7 +213,7 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder {
} }
} }
List<String> pathsToMatch = createResourceLinkPaths(theResourceType, theParamName); List<String> pathsToMatch = createResourceLinkPaths(theResourceType, theParamName, theQualifiers);
boolean inverse; boolean inverse;
if ((theOperation == null) || (theOperation == SearchFilterParser.CompareOperation.eq)) { if ((theOperation == null) || (theOperation == SearchFilterParser.CompareOperation.eq)) {
inverse = false; inverse = false;
@ -266,8 +268,8 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder {
return toEqualToOrInPredicate(myColumnSrcPath, generatePlaceholders(thePathsToMatch)); return toEqualToOrInPredicate(myColumnSrcPath, generatePlaceholders(thePathsToMatch));
} }
public Condition createPredicateSourcePaths(String theResourceName, String theParamName) { public Condition createPredicateSourcePaths(String theResourceName, String theParamName, List<String> theQualifiers) {
List<String> pathsToMatch = createResourceLinkPaths(theResourceName, theParamName); List<String> pathsToMatch = createResourceLinkPaths(theResourceName, theParamName, theQualifiers);
return createPredicateSourcePaths(pathsToMatch); return createPredicateSourcePaths(pathsToMatch);
} }
@ -301,7 +303,7 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder {
* This is for handling queries like the following: /Observation?device.identifier=urn:system|foo in which we use a chain * This is for handling queries like the following: /Observation?device.identifier=urn:system|foo in which we use a chain
* on the device. * on the device.
*/ */
private Condition addPredicateReferenceWithChain(String theResourceName, String theParamName, List<? extends IQueryParameterType> theList, ReferenceParam theReferenceParam, RequestDetails theRequest, RequestPartitionId theRequestPartitionId) { private Condition addPredicateReferenceWithChain(String theResourceName, String theParamName, List<String> theQualifiers, List<? extends IQueryParameterType> theList, ReferenceParam theReferenceParam, RequestDetails theRequest, RequestPartitionId theRequestPartitionId) {
/* /*
* Which resource types can the given chained parameter actually link to? This might be a list * Which resource types can the given chained parameter actually link to? This might be a list
@ -318,7 +320,7 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder {
*/ */
if (Constants.PARAM_TYPE.equals(theReferenceParam.getChain())) { if (Constants.PARAM_TYPE.equals(theReferenceParam.getChain())) {
List<String> pathsToMatch = createResourceLinkPaths(theResourceName, theParamName); List<String> pathsToMatch = createResourceLinkPaths(theResourceName, theParamName, theQualifiers);
Condition typeCondition = createPredicateSourcePaths(pathsToMatch); Condition typeCondition = createPredicateSourcePaths(pathsToMatch);
String typeValue = theReferenceParam.getValue(); String typeValue = theReferenceParam.getValue();
@ -430,7 +432,7 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder {
multiTypePredicate = toOrPredicate(orPredicates); multiTypePredicate = toOrPredicate(orPredicates);
} }
List<String> pathsToMatch = createResourceLinkPaths(theResourceName, theParamName); List<String> pathsToMatch = createResourceLinkPaths(theResourceName, theParamName, theQualifiers);
Condition pathPredicate = createPredicateSourcePaths(pathsToMatch); Condition pathPredicate = createPredicateSourcePaths(pathsToMatch);
return toAndPredicate(pathPredicate, multiTypePredicate); return toAndPredicate(pathPredicate, multiTypePredicate);
} }
@ -440,15 +442,7 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder {
final List<Class<? extends IBaseResource>> resourceTypes; final List<Class<? extends IBaseResource>> resourceTypes;
if (!theReferenceParam.hasResourceType()) { if (!theReferenceParam.hasResourceType()) {
RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam(theResourceName, theParamName); resourceTypes = determineResourceTypes(Collections.singleton(theResourceName), theParamName);
resourceTypes = new ArrayList<>();
if (param.hasTargets()) {
Set<String> targetTypes = param.getTargets();
for (String next : targetTypes) {
resourceTypes.add(getFhirContext().getResourceDefinition(next).getImplementingClass());
}
}
if (resourceTypes.isEmpty()) { if (resourceTypes.isEmpty()) {
RuntimeSearchParam searchParamByName = mySearchParamRegistry.getActiveSearchParam(theResourceName, theParamName); RuntimeSearchParam searchParamByName = mySearchParamRegistry.getActiveSearchParam(theResourceName, theParamName);
@ -513,25 +507,89 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder {
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
public List<String> createResourceLinkPaths(String theResourceName, String theParamName) { private List<Class<? extends IBaseResource>> determineResourceTypes(Set<String> theResourceNames, String theParamNameChain) {
RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam(theResourceName, theParamName); int linkIndex = theParamNameChain.indexOf('.');
List<String> path = param.getPathsSplit(); if (linkIndex == -1) {
Set<Class<? extends IBaseResource>> resourceTypes = new HashSet<>();
for (String resourceName : theResourceNames) {
RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam(resourceName, theParamNameChain);
/* if (param != null && param.hasTargets()) {
* SearchParameters can declare paths on multiple resource Set<String> targetTypes = param.getTargets();
* types. Here we only want the ones that actually apply. for (String next : targetTypes) {
*/ resourceTypes.add(getFhirContext().getResourceDefinition(next).getImplementingClass());
path = new ArrayList<>(path); }
}
ListIterator<String> iter = path.listIterator();
while (iter.hasNext()) {
String nextPath = trim(iter.next());
if (!nextPath.contains(theResourceName + ".")) {
iter.remove();
} }
} return new ArrayList<>(resourceTypes);
} else {
String paramNameHead = theParamNameChain.substring(0, linkIndex);
String paramNameTail = theParamNameChain.substring(linkIndex+1);
Set<String> targetResourceTypeNames = new HashSet<>();
for (String resourceName : theResourceNames) {
RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam(resourceName, paramNameHead);
return path; if (param != null && param.hasTargets()) {
targetResourceTypeNames.addAll(param.getTargets());
}
}
return determineResourceTypes(targetResourceTypeNames, paramNameTail);
}
}
public List<String> createResourceLinkPaths(String theResourceName, String theParamName, List<String> theParamQualifiers) {
int linkIndex = theParamName.indexOf('.');
if (linkIndex == -1) {
RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam(theResourceName, theParamName);
if (param == null) {
// This can happen during recursion, if not all the possible target types of one link in the chain support the next link
return new ArrayList<>();
}
List<String> path = param.getPathsSplit();
/*
* SearchParameters can declare paths on multiple resource
* types. Here we only want the ones that actually apply.
*/
path = new ArrayList<>(path);
ListIterator<String> iter = path.listIterator();
while (iter.hasNext()) {
String nextPath = trim(iter.next());
if (!nextPath.contains(theResourceName + ".")) {
iter.remove();
}
}
return path;
} else {
String paramNameHead = theParamName.substring(0, linkIndex);
String paramNameTail = theParamName.substring(linkIndex + 1);
String qualifier = theParamQualifiers.get(0);
RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam(theResourceName, paramNameHead);
Set<String> tailPaths = param.getTargets().stream()
.filter(t -> isBlank(qualifier) || qualifier.equals(t))
.map(t -> createResourceLinkPaths(t, paramNameTail, theParamQualifiers.subList(1, theParamQualifiers.size())))
.flatMap(Collection::stream)
.map(t -> t.substring(t.indexOf('.')+1))
.collect(Collectors.toSet());
List<String> path = param.getPathsSplit();
/*
* SearchParameters can declare paths on multiple resource
* types. Here we only want the ones that actually apply.
* Then append all the tail paths to each of the applicable head paths
*/
return path.stream()
.map(String::trim)
.filter(t -> t.startsWith(theResourceName + "."))
.map(head -> tailPaths.stream().map(tail -> head + "." + tail).collect(Collectors.toSet()))
.flatMap(Collection::stream)
.collect(Collectors.toList());
}
} }

View File

@ -8,6 +8,7 @@ import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.parser.StrictErrorHandler; import ca.uhn.fhir.parser.StrictErrorHandler;
import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.api.server.IBundleProvider;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Device;
import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.Organization; import org.hl7.fhir.r4.model.Organization;
@ -15,7 +16,6 @@ import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.StringType; import org.hl7.fhir.r4.model.StringType;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -116,6 +116,43 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
assertThat(oids, contains(oid1.getIdPart())); assertThat(oids, contains(oid1.getIdPart()));
} }
@Test
public void testShouldResolveATwoLinkChainToAContainedReference() throws Exception {
// Adding support for this case in SMILE-3151
// setup
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.setId("pat");
p.addName().setFamily("Smith").addGiven("John");
p.getManagingOrganization().setReference(org.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();
}
String url = "/Observation?subject.organization=" + orgId.getValueAsString();
// execute
List<String> oids = searchAndReturnUnqualifiedVersionlessIdValues(url);
// validate
assertEquals(1L, oids.size());
assertThat(oids, contains(oid1.getIdPart()));
}
@Test @Test
public void testShouldResolveAThreeLinkChainWhereAllResourcesStandAlone() throws Exception { public void testShouldResolveAThreeLinkChainWhereAllResourcesStandAlone() throws Exception {
@ -188,9 +225,8 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
} }
@Test @Test
@Disabled
public void testShouldResolveAThreeLinkChainWithAContainedResourceAtTheBeginningOfTheChain() throws Exception { public void testShouldResolveAThreeLinkChainWithAContainedResourceAtTheBeginningOfTheChain() throws Exception {
// We do not currently support this case - we may not be indexing the references of contained resources // Adding support for this case in SMILE-3151
// setup // setup
IIdType oid1; IIdType oid1;
@ -242,11 +278,21 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
p.getManagingOrganization().setReference(org.getId()); p.getManagingOrganization().setReference(org.getId());
myPatientDao.create(p, mySrd); myPatientDao.create(p, mySrd);
Observation obs = new Observation(); Device d = new Device();
obs.getCode().setText("Observation 1"); d.setId(IdType.newRandomUuid());
obs.getSubject().setReference(p.getId()); d.getOwner().setReference(org.getId());
myDeviceDao.create(d, mySrd);
oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); Observation obs1 = new Observation();
obs1.getCode().setText("Observation 1");
obs1.getSubject().setReference(p.getId());
Observation obs2 = new Observation();
obs2.getCode().setText("Observation 2");
obs2.getSubject().setReference(d.getId());
oid1 = myObservationDao.create(obs1, mySrd).getId().toUnqualifiedVersionless();
myObservationDao.create(obs2, mySrd);
} }
String url = "/Observation?subject:Patient.organization:Organization.name=HealthCo"; String url = "/Observation?subject:Patient.organization:Organization.name=HealthCo";
@ -278,11 +324,26 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
p.getManagingOrganization().setReference("#org"); p.getManagingOrganization().setReference("#org");
myPatientDao.create(p, mySrd); myPatientDao.create(p, mySrd);
Organization org2 = new Organization();
org2.setId("org");
org2.setName("HealthCo");
Device d = new Device();
d.setId(IdType.newRandomUuid());
d.getContained().add(org2);
d.getOwner().setReference("#org");
myDeviceDao.create(d, mySrd);
Observation obs = new Observation(); Observation obs = new Observation();
obs.getCode().setText("Observation 1"); obs.getCode().setText("Observation 1");
obs.getSubject().setReference(p.getId()); obs.getSubject().setReference(p.getId());
oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless();
Observation obs2 = new Observation();
obs2.getCode().setText("Observation 2");
obs2.getSubject().setReference(d.getId());
myObservationDao.create(obs2, mySrd);
} }
String url = "/Observation?subject:Patient.organization:Organization.name=HealthCo"; String url = "/Observation?subject:Patient.organization:Organization.name=HealthCo";
@ -295,6 +356,55 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
assertThat(oids, contains(oid1.getIdPart())); assertThat(oids, contains(oid1.getIdPart()));
} }
@Test
public void testShouldResolveAThreeLinkChainWithQualifiersWithAContainedResourceAtTheBeginning() throws Exception {
// Adding support for this case in SMILE-3151
// setup
IIdType oid1;
{
Organization org = new Organization();
org.setId(IdType.newRandomUuid());
org.setName("HealthCo");
myOrganizationDao.create(org, mySrd);
Patient p = new Patient();
p.setId("pat");
p.addName().setFamily("Smith").addGiven("John");
p.getManagingOrganization().setReference(org.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();
Device d = new Device();
d.setId("dev");
d.getOwner().setReference(org.getId());
Observation obs2 = new Observation();
obs2.getContained().add(d);
obs2.getCode().setText("Observation 2");
obs2.getSubject().setReference("#dev");
myObservationDao.create(obs2, mySrd);
}
String url = "/Observation?subject:Patient.organization:Organization.name=HealthCo";
// execute
myCaptureQueriesListener.clear();
List<String> oids = searchAndReturnUnqualifiedVersionlessIdValues(url);
myCaptureQueriesListener.logSelectQueries();
// validate
assertEquals(1L, oids.size());
assertThat(oids, contains(oid1.getIdPart()));
}
@Test @Test
public void testShouldResolveAFourLinkChainWhereAllResourcesStandAlone() throws Exception { public void testShouldResolveAFourLinkChainWhereAllResourcesStandAlone() throws Exception {
@ -375,6 +485,52 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
assertThat(oids, contains(oid1.getIdPart())); assertThat(oids, contains(oid1.getIdPart()));
} }
@Test
public void testShouldResolveAFourLinkChainWithAContainedResourceInTheMiddle() throws Exception {
// setup
IIdType oid1;
{
myCaptureQueriesListener.clear();
Organization org = new Organization();
org.setId(IdType.newRandomUuid());
org.setName("HealthCo");
myOrganizationDao.create(org, mySrd);
Organization partOfOrg = new Organization();
partOfOrg.setId("org");
partOfOrg.getPartOf().setReference(org.getId());
Patient p = new Patient();
p.setId(IdType.newRandomUuid());
p.addName().setFamily("Smith").addGiven("John");
p.getContained().add(partOfOrg);
p.getManagingOrganization().setReference("#org");
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();
myCaptureQueriesListener.logInsertQueries();
}
String url = "/Observation?subject.organization.partof.name=HealthCo";
// execute
myCaptureQueriesListener.clear();
List<String> oids = searchAndReturnUnqualifiedVersionlessIdValues(url);
myCaptureQueriesListener.logSelectQueries();
// validate
assertEquals(1L, oids.size());
assertThat(oids, contains(oid1.getIdPart()));
}
private List<String> searchAndReturnUnqualifiedVersionlessIdValues(String theUrl) throws IOException { private List<String> searchAndReturnUnqualifiedVersionlessIdValues(String theUrl) throws IOException {
List<String> ids = new ArrayList<>(); List<String> ids = new ArrayList<>();

View File

@ -2,6 +2,7 @@ package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.model.entity.NormalizedQuantitySearchLevel; import ca.uhn.fhir.jpa.model.entity.NormalizedQuantitySearchLevel;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantityNormalized; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantityNormalized;
@ -23,6 +24,7 @@ import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.DateType; import org.hl7.fhir.r4.model.DateType;
import org.hl7.fhir.r4.model.DecimalType; import org.hl7.fhir.r4.model.DecimalType;
import org.hl7.fhir.r4.model.Encounter;
import org.hl7.fhir.r4.model.Enumerations; import org.hl7.fhir.r4.model.Enumerations;
import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.Observation;
@ -42,6 +44,7 @@ import java.io.IOException;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
@ -63,6 +66,7 @@ public class FhirResourceDaoR4CreateTest extends BaseJpaR4Test {
myDaoConfig.setResourceClientIdStrategy(new DaoConfig().getResourceClientIdStrategy()); myDaoConfig.setResourceClientIdStrategy(new DaoConfig().getResourceClientIdStrategy());
myDaoConfig.setDefaultSearchParamsCanBeOverridden(new DaoConfig().isDefaultSearchParamsCanBeOverridden()); myDaoConfig.setDefaultSearchParamsCanBeOverridden(new DaoConfig().isDefaultSearchParamsCanBeOverridden());
myModelConfig.setNormalizedQuantitySearchLevel(NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_SEARCH_NOT_SUPPORTED); myModelConfig.setNormalizedQuantitySearchLevel(NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_SEARCH_NOT_SUPPORTED);
myModelConfig.setIndexOnContainedResources(new ModelConfig().isIndexOnContainedResources());
} }
@ -81,13 +85,43 @@ public class FhirResourceDaoR4CreateTest extends BaseJpaR4Test {
List<ResourceLink> allLinks = myResourceLinkDao.findAll(); List<ResourceLink> allLinks = myResourceLinkDao.findAll();
List<String> paths = allLinks List<String> paths = allLinks
.stream() .stream()
.map(t -> t.getSourcePath()) .map(ResourceLink::getSourcePath)
.sorted() .sorted()
.collect(Collectors.toList()); .collect(Collectors.toList());
assertThat(paths.toString(), paths, contains("Observation.subject", "Observation.subject.where(resolve() is Patient)")); assertThat(paths.toString(), paths, contains("Observation.subject", "Observation.subject.where(resolve() is Patient)"));
}); });
} }
@Test
public void testCreateLinkCreatesAppropriatePaths_ContainedResource() {
myModelConfig.setIndexOnContainedResources(true);
Patient p = new Patient();
p.setId("Patient/A");
p.setActive(true);
myPatientDao.update(p, mySrd);
Observation containedObs = new Observation();
containedObs.setId("#cont");
containedObs.setSubject(new Reference("Patient/A"));
Encounter enc = new Encounter();
enc.getContained().add(containedObs);
enc.addReasonReference(new Reference("#cont"));
myEncounterDao.create(enc, mySrd);
runInTransaction(() ->{
List<ResourceLink> allLinks = myResourceLinkDao.findAll();
Optional<ResourceLink> link = allLinks
.stream()
.filter(t -> "Encounter.reasonReference.subject".equals(t.getSourcePath()))
.findFirst();
assertTrue(link.isPresent());
assertEquals("Patient", link.get().getTargetResourceType());
assertEquals("A", link.get().getTargetResourceId());
});
}
@Test @Test
public void testConditionalCreateWithPlusInUrl() { public void testConditionalCreateWithPlusInUrl() {
@ -245,7 +279,7 @@ public class FhirResourceDaoR4CreateTest extends BaseJpaR4Test {
// Read it back // Read it back
p = myPatientDao.read(new IdType("Patient/" + firstClientAssignedId)); p = myPatientDao.read(new IdType("Patient/" + firstClientAssignedId));
assertEquals(true, p.getActive()); assertTrue(p.getActive());
// Now create a client assigned numeric ID // Now create a client assigned numeric ID
p = new Patient(); p = new Patient();
@ -298,7 +332,7 @@ public class FhirResourceDaoR4CreateTest extends BaseJpaR4Test {
// Read it back // Read it back
p = myPatientDao.read(id0.toUnqualifiedVersionless()); p = myPatientDao.read(id0.toUnqualifiedVersionless());
assertEquals(true, p.getActive()); assertTrue(p.getActive());
// Pick an ID that was already used as an internal PID // Pick an ID that was already used as an internal PID
Long newId = runInTransaction(() -> myResourceTableDao.findIdsOfResourcesWithinUpdatedRangeOrderedFromNewest( Long newId = runInTransaction(() -> myResourceTableDao.findIdsOfResourcesWithinUpdatedRangeOrderedFromNewest(

View File

@ -158,6 +158,21 @@ public final class ResourceIndexedSearchParams {
updateSpnamePrefixForIndexedOnContainedResource(myCoordsParams, theSpnamePrefix); updateSpnamePrefixForIndexedOnContainedResource(myCoordsParams, theSpnamePrefix);
} }
public void updateSpnamePrefixForLinksOnContainedResource(String theSpNamePrefix) {
for (ResourceLink param : myLinks) {
// The resource link already has the resource type of the contained resource at the head of the path.
// We need to replace this with the name of the containing type, and extend the search path.
int index = param.getSourcePath().indexOf('.');
if (index > -1) {
param.setSourcePath(theSpNamePrefix + param.getSourcePath().substring(index));
} else {
// Can this ever happen?
param.setSourcePath(theSpNamePrefix + "." + param.getSourcePath());
}
param.calculateHashes(); // re-calculateHashes
}
}
void setUpdatedTime(Date theUpdateTime) { void setUpdatedTime(Date theUpdateTime) {
setUpdatedTime(myStringParams, theUpdateTime); setUpdatedTime(myStringParams, theUpdateTime);
setUpdatedTime(myNumberParams, theUpdateTime); setUpdatedTime(myNumberParams, theUpdateTime);

View File

@ -114,6 +114,10 @@ public class SearchParamExtractorService {
// Reference search parameters // Reference search parameters
extractResourceLinks(theRequestPartitionId, theParams, theEntity, theResource, theTransactionDetails, theFailOnInvalidReference, theRequestDetails); extractResourceLinks(theRequestPartitionId, theParams, theEntity, theResource, theTransactionDetails, theFailOnInvalidReference, theRequestDetails);
if (myModelConfig.isIndexOnContainedResources()) {
extractResourceLinksForContainedResources(theRequestPartitionId, theParams, theEntity, theResource, theTransactionDetails, theFailOnInvalidReference, theRequestDetails);
}
theParams.setUpdatedTime(theTransactionDetails.getTransactionDate()); theParams.setUpdatedTime(theTransactionDetails.getTransactionDate());
} }
@ -402,6 +406,48 @@ public class SearchParamExtractorService {
theParams.myLinks.add(resourceLink); theParams.myLinks.add(resourceLink);
} }
private void extractResourceLinksForContainedResources(RequestPartitionId theRequestPartitionId, ResourceIndexedSearchParams theParams, ResourceTable theEntity, IBaseResource theResource, TransactionDetails theTransactionDetails, boolean theFailOnInvalidReference, RequestDetails theRequest) {
FhirTerser terser = myContext.newTerser();
// 1. get all contained resources
Collection<IBaseResource> containedResources = terser.getAllEmbeddedResources(theResource, false);
// 2. Find referenced search parameters
ISearchParamExtractor.SearchParamSet<PathAndRef> referencedSearchParamSet = mySearchParamExtractor.extractResourceLinks(theResource, true);
String spNamePrefix = null;
ResourceIndexedSearchParams currParams;
// 3. for each referenced search parameter, create an index
for (PathAndRef nextPathAndRef : referencedSearchParamSet) {
// 3.1 get the search parameter name as spname prefix
spNamePrefix = nextPathAndRef.getSearchParamName();
if (spNamePrefix == null || nextPathAndRef.getRef() == null)
continue;
// 3.2 find the contained resource
IBaseResource containedResource = findContainedResource(containedResources, nextPathAndRef.getRef());
if (containedResource == null)
continue;
currParams = new ResourceIndexedSearchParams();
// 3.3 create indexes for the current contained resource
extractResourceLinks(theRequestPartitionId, currParams, theEntity, containedResource, theTransactionDetails, theFailOnInvalidReference, theRequest);
// 3.4 added reference name as a prefix for the contained resource if any
// e.g. for Observation.subject contained reference
// the SP_NAME = subject.family
currParams.updateSpnamePrefixForLinksOnContainedResource(nextPathAndRef.getPath());
// 3.5 merge to the mainParams
// NOTE: the spname prefix is different
theParams.getResourceLinks().addAll(currParams.getResourceLinks());
}
}
private ResourceLink resolveTargetAndCreateResourceLinkOrReturnNull(@Nonnull RequestPartitionId theRequestPartitionId, ResourceTable theEntity, Date theUpdateTime, RuntimeSearchParam nextSpDef, String theNextPathsUnsplit, PathAndRef nextPathAndRef, IIdType theNextId, String theTypeString, Class<? extends IBaseResource> theType, IBaseReference theReference, RequestDetails theRequest, TransactionDetails theTransactionDetails) { private ResourceLink resolveTargetAndCreateResourceLinkOrReturnNull(@Nonnull RequestPartitionId theRequestPartitionId, ResourceTable theEntity, Date theUpdateTime, RuntimeSearchParam nextSpDef, String theNextPathsUnsplit, PathAndRef nextPathAndRef, IIdType theNextId, String theTypeString, Class<? extends IBaseResource> theType, IBaseReference theReference, RequestDetails theRequest, TransactionDetails theTransactionDetails) {
ResourcePersistentId resolvedResourceId = theTransactionDetails.getResolvedResourceId(theNextId); ResourcePersistentId resolvedResourceId = theTransactionDetails.getResolvedResourceId(theNextId);