Fix 2 issues with has reverse chaining (#4139)
* Fix 2 issues with chains * Add changelog * Cleanup * Add comment * Cleanup
This commit is contained in:
parent
f00f65aae4
commit
26ef32c1dd
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
type: fix
|
||||
issue: 4139
|
||||
title: "Two issues with reverse chaining (i.e. the `_has` search parameter) have been addressed:
|
||||
* Searching for a reverse chain with a target search parameter of `_id` did not work correctly, e.g. `Patient?_has:Observation:subject:_id=Patient/123`
|
||||
* Searching with a combination of a forward chain and a reverse chain did not work correctly if indexing contained resources was enabled, e.g. `Observation?subject._has:Group:member:_id=Group/123`"
|
|
@ -128,6 +128,8 @@ import static ca.uhn.fhir.jpa.util.QueryParameterUtils.toAndPredicate;
|
|||
import static ca.uhn.fhir.jpa.util.QueryParameterUtils.toEqualToOrInPredicate;
|
||||
import static ca.uhn.fhir.jpa.util.QueryParameterUtils.toOperation;
|
||||
import static ca.uhn.fhir.jpa.util.QueryParameterUtils.toOrPredicate;
|
||||
import static ca.uhn.fhir.rest.api.Constants.PARAM_HAS;
|
||||
import static ca.uhn.fhir.rest.api.Constants.PARAM_ID;
|
||||
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||
import static org.apache.commons.lang3.StringUtils.split;
|
||||
|
||||
|
@ -256,7 +258,7 @@ public class QueryStack {
|
|||
|
||||
TokenPredicateBuilder tokenPredicateBuilder = mySqlBuilder.createTokenPredicateBuilder();
|
||||
Condition hashIdentityPredicate = tokenPredicateBuilder.createHashIdentityPredicate(theResourceName, theParamName);
|
||||
|
||||
|
||||
addSortCustomJoin(firstPredicateBuilder, tokenPredicateBuilder, hashIdentityPredicate);
|
||||
|
||||
mySqlBuilder.addSortString(tokenPredicateBuilder.getColumnSystem(), theAscending);
|
||||
|
@ -727,10 +729,16 @@ public class QueryStack {
|
|||
String qualifier = paramName.substring(4);
|
||||
for (IQueryParameterType next : nextOrList) {
|
||||
HasParam nextHasParam = new HasParam();
|
||||
nextHasParam.setValueAsQueryToken(myFhirContext, Constants.PARAM_HAS, qualifier, next.getValueAsQueryToken(myFhirContext));
|
||||
nextHasParam.setValueAsQueryToken(myFhirContext, PARAM_HAS, qualifier, next.getValueAsQueryToken(myFhirContext));
|
||||
orValues.add(nextHasParam);
|
||||
}
|
||||
|
||||
} else if (paramName.equals(PARAM_ID)) {
|
||||
|
||||
for (IQueryParameterType next : nextOrList) {
|
||||
orValues.add(new TokenParam(next.getValueAsQueryToken(myFhirContext)));
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
//Ensure that the name of the search param
|
||||
|
@ -959,7 +967,7 @@ public class QueryStack {
|
|||
}
|
||||
|
||||
public String getPath() { return myPath; }
|
||||
|
||||
|
||||
public String getSearchParameterName() { return mySearchParameterName; }
|
||||
|
||||
@Override
|
||||
|
@ -1006,7 +1014,6 @@ public class QueryStack {
|
|||
String targetValue = nextOr.getValueAsQueryToken(myFhirContext);
|
||||
if (nextOr instanceof ReferenceParam) {
|
||||
ReferenceParam referenceParam = (ReferenceParam) nextOr;
|
||||
|
||||
if (!isReferenceParamValid(referenceParam)) {
|
||||
throw new InvalidRequestException(Msg.code(2007) +
|
||||
"The search chain " + theSearchParam.getName() + "." + referenceParam.getChain() +
|
||||
|
@ -1538,11 +1545,11 @@ public class QueryStack {
|
|||
String theSpnamePrefix, RuntimeSearchParam theSearchParam, List<? extends IQueryParameterType> theList,
|
||||
SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId, SearchQueryBuilder theSqlBuilder) {
|
||||
|
||||
List<IQueryParameterType> tokens = new ArrayList<>();
|
||||
|
||||
List<IQueryParameterType> tokens = new ArrayList<>();
|
||||
|
||||
boolean paramInverted = false;
|
||||
TokenParamModifier modifier;
|
||||
|
||||
|
||||
for (IQueryParameterType nextOr : theList) {
|
||||
if (nextOr instanceof TokenParam) {
|
||||
if (!((TokenParam) nextOr).isEmpty()) {
|
||||
|
@ -1561,8 +1568,8 @@ public class QueryStack {
|
|||
throw new MethodNotAllowedException(Msg.code(1219) + msg);
|
||||
}
|
||||
return createPredicateString(theSourceJoinColumn, theResourceName, theSpnamePrefix, theSearchParam, theList, null, theRequestPartitionId, theSqlBuilder);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
modifier = id.getModifier();
|
||||
// for :not modifier, create a token and remove the :not modifier
|
||||
if (modifier == TokenParamModifier.NOT) {
|
||||
|
@ -1584,23 +1591,23 @@ public class QueryStack {
|
|||
String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName());
|
||||
Condition predicate;
|
||||
BaseJoiningPredicateBuilder join;
|
||||
|
||||
|
||||
if (paramInverted) {
|
||||
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 = theSqlBuilder.getOrCreateFirstPredicateBuilder();
|
||||
|
||||
|
||||
if (theSourceJoinColumn == null) {
|
||||
predicate = new InCondition(join.getResourceIdColumn(), subSelect).setNegate(true);
|
||||
} else {
|
||||
//-- for the resource link, need join with target_resource_id
|
||||
predicate = new InCondition(theSourceJoinColumn, subSelect).setNegate(true);
|
||||
}
|
||||
|
||||
|
||||
} else {
|
||||
Boolean isMissing = theList.get(0).getMissing();
|
||||
if (isMissing != null) {
|
||||
|
@ -1620,9 +1627,9 @@ public class QueryStack {
|
|||
TokenPredicateBuilder tokenJoin = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.TOKEN, theSourceJoinColumn, paramName, () -> theSqlBuilder.addTokenPredicateBuilder(theSourceJoinColumn)).getResult();
|
||||
|
||||
predicate = tokenJoin.createPredicateToken(tokens, theResourceName, theSpnamePrefix, theSearchParam, theOperation, theRequestPartitionId);
|
||||
join = tokenJoin;
|
||||
}
|
||||
|
||||
join = tokenJoin;
|
||||
}
|
||||
|
||||
return join.combineWithRequestPartitionIdPredicate(theRequestPartitionId, predicate);
|
||||
}
|
||||
|
||||
|
@ -1676,7 +1683,7 @@ public class QueryStack {
|
|||
case IAnyResource.SP_RES_ID:
|
||||
return createPredicateResourceId(theSourceJoinColumn, theAndOrParams, theResourceName, null, theRequestPartitionId);
|
||||
|
||||
case Constants.PARAM_HAS:
|
||||
case PARAM_HAS:
|
||||
return createPredicateHas(theSourceJoinColumn, theResourceName, theAndOrParams, theRequest, theRequestPartitionId);
|
||||
|
||||
case Constants.PARAM_TAG:
|
||||
|
@ -1820,7 +1827,9 @@ public class QueryStack {
|
|||
nextAnd.stream()
|
||||
.filter(t -> t instanceof ReferenceParam)
|
||||
.map(t -> ((ReferenceParam) t).getChain())
|
||||
.anyMatch(StringUtils::isNotBlank);
|
||||
.filter(StringUtils::isNotBlank)
|
||||
// Chains on _has can't be indexed for contained searches - At least not yet. It's not clear to me if we ever want to support this, it would be really hard to do.
|
||||
.anyMatch(t->!t.startsWith(PARAM_HAS + ":"));
|
||||
}
|
||||
|
||||
public void addPredicateCompositeUnique(String theIndexString, RequestPartitionId theRequestPartitionId) {
|
||||
|
|
|
@ -4,6 +4,7 @@ import ca.uhn.fhir.i18n.Msg;
|
|||
import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
||||
import ca.uhn.fhir.jpa.dao.data.ISearchDao;
|
||||
import ca.uhn.fhir.jpa.entity.Search;
|
||||
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
|
||||
import ca.uhn.fhir.jpa.model.entity.NormalizedQuantitySearchLevel;
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable;
|
||||
import ca.uhn.fhir.jpa.model.util.JpaConstants;
|
||||
|
@ -70,6 +71,7 @@ import org.apache.http.entity.ContentType;
|
|||
import org.apache.http.entity.StringEntity;
|
||||
import org.apache.http.message.BasicNameValuePair;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
import org.apache.jena.rdf.model.ModelCon;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator;
|
||||
import org.hl7.fhir.instance.model.api.IAnyResource;
|
||||
|
@ -246,6 +248,8 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
|
|||
myDaoConfig.setIndexMissingFields(new DaoConfig().getIndexMissingFields());
|
||||
myDaoConfig.setAdvancedHSearchIndexing(new DaoConfig().isAdvancedHSearchIndexing());
|
||||
|
||||
myModelConfig.setIndexOnContainedResources(new ModelConfig().isIndexOnContainedResources());
|
||||
|
||||
mySearchCoordinatorSvcRaw.setLoadingThrottleForUnitTests(null);
|
||||
mySearchCoordinatorSvcRaw.setSyncSizeForUnitTests(QueryParameterUtils.DEFAULT_SYNC_SIZE);
|
||||
mySearchCoordinatorSvcRaw.setNeverUseLocalSearchForUnitTests(false);
|
||||
|
@ -3136,6 +3140,84 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
|
|||
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {true, false})
|
||||
public void testHasParameterOnChain(boolean theWithIndexOnContainedResources) throws Exception {
|
||||
myModelConfig.setIndexOnContainedResources(theWithIndexOnContainedResources);
|
||||
|
||||
IIdType pid0;
|
||||
IIdType pid1;
|
||||
IIdType groupId;
|
||||
IIdType obsId;
|
||||
{
|
||||
Patient patient = new Patient();
|
||||
patient.addIdentifier().setSystem("urn:system").setValue("001");
|
||||
patient.addName().setFamily("Tester").addGiven("Joe");
|
||||
pid0 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless();
|
||||
}
|
||||
{
|
||||
Patient patient = new Patient();
|
||||
patient.addIdentifier().setSystem("urn:system").setValue("001");
|
||||
patient.addName().setFamily("Tester").addGiven("Joe");
|
||||
pid1 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless();
|
||||
}
|
||||
{
|
||||
Group group = new Group();
|
||||
group.addMember().setEntity(new Reference(pid0.getValue()));
|
||||
group.addMember().setEntity(new Reference(pid1.getValue()));
|
||||
groupId = myGroupDao.create(group, mySrd).getId().toUnqualifiedVersionless();
|
||||
}
|
||||
{
|
||||
Observation obs = new Observation();
|
||||
obs.getCode().addCoding().setSystem("urn:system").setCode("FOO");
|
||||
obs.getSubject().setReferenceElement(pid0);
|
||||
obsId = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless();
|
||||
}
|
||||
|
||||
String uri;
|
||||
List<String> ids;
|
||||
|
||||
logAllTokenIndexes();
|
||||
|
||||
uri = ourServerBase + "/Observation?code=urn:system%7CFOO&subject._has:Group:member:_id=" + groupId.getValue();
|
||||
myCaptureQueriesListener.clear();
|
||||
ids = searchAndReturnUnqualifiedVersionlessIdValues(uri);
|
||||
myCaptureQueriesListener.logAllQueries();
|
||||
assertThat(ids, contains(obsId.getValue()));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {true, false})
|
||||
public void testHasParameterWithIdTarget(boolean theWithIndexOnContainedResources) throws Exception {
|
||||
myModelConfig.setIndexOnContainedResources(theWithIndexOnContainedResources);
|
||||
|
||||
IIdType pid0;
|
||||
IIdType obsId;
|
||||
{
|
||||
Patient patient = new Patient();
|
||||
patient.addIdentifier().setSystem("urn:system").setValue("001");
|
||||
patient.addName().setFamily("Tester").addGiven("Joe");
|
||||
pid0 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless();
|
||||
}
|
||||
{
|
||||
Observation obs = new Observation();
|
||||
obs.addIdentifier().setSystem("urn:system").setValue("FOO");
|
||||
obs.getSubject().setReferenceElement(pid0);
|
||||
obsId = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless();
|
||||
}
|
||||
|
||||
String uri;
|
||||
List<String> ids;
|
||||
|
||||
logAllResourceLinks();
|
||||
|
||||
uri = ourServerBase + "/Patient?_has:Observation:subject:_id=" + obsId.getValue();
|
||||
myCaptureQueriesListener.clear();
|
||||
ids = searchAndReturnUnqualifiedVersionlessIdValues(uri);
|
||||
myCaptureQueriesListener.logAllQueries();
|
||||
assertThat(ids, contains(pid0.getValue()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHistoryWithAtParameter() throws Exception {
|
||||
String methodName = "testHistoryWithFromAndTo";
|
||||
|
|
|
@ -33,6 +33,7 @@ import ca.uhn.fhir.rest.server.RestfulServer;
|
|||
import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
|
||||
import ca.uhn.fhir.rest.server.provider.HashMapResourceProvider;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
import org.hl7.fhir.instance.model.api.IBaseBundle;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.hl7.fhir.r4.model.Bundle;
|
||||
|
@ -89,9 +90,12 @@ public class RestServerR4Helper extends BaseRestServerHelper implements BeforeEa
|
|||
}
|
||||
|
||||
public List<Bundle> getTransactions() {
|
||||
return myRestServer
|
||||
.getPlainProvider()
|
||||
.getTransactions()
|
||||
List<IBaseBundle> transactions = myRestServer.getPlainProvider().getTransactions();
|
||||
|
||||
// Make a copy to avoid synchronization issues
|
||||
transactions = new ArrayList<>(transactions);
|
||||
|
||||
return transactions
|
||||
.stream()
|
||||
.map(t -> (Bundle) t)
|
||||
.collect(Collectors.toList());
|
||||
|
|
Loading…
Reference in New Issue