Merge pull request #2950 from hapifhir/ft-search-with-not-modifier

Fixed :not modifier issue for the resourceLink
This commit is contained in:
Tadgh 2021-09-03 01:17:11 -04:00 committed by GitHub
commit 4d1de25a36
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 399 additions and 16 deletions

View File

@ -0,0 +1,4 @@
---
type: fix
issue: 2837
title: "The :not modifier does not currently work for observations with multiple codes for the search. This is fixed."

View File

@ -989,8 +989,11 @@ public class QueryStack {
SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId) {
List<IQueryParameterType> tokens = new ArrayList<>();
for (IQueryParameterType nextOr : theList) {
boolean paramInverted = false;
TokenParamModifier modifier = null;
for (IQueryParameterType nextOr : theList) {
if (nextOr instanceof TokenParam) {
if (!((TokenParam) nextOr).isEmpty()) {
TokenParam id = (TokenParam) nextOr;
@ -1011,15 +1014,18 @@ public class QueryStack {
return createPredicateString(theSourceJoinColumn, theResourceName, theSpnamePrefix, theSearchParam, theList, null, theRequestPartitionId);
}
tokens.add(nextOr);
}
modifier = id.getModifier();
// for :not modifier, create a token and remove the :not modifier
if (modifier != null && modifier == TokenParamModifier.NOT) {
tokens.add(new TokenParam(((TokenParam) nextOr).getSystem(), ((TokenParam) nextOr).getValue()));
paramInverted = true;
} else {
tokens.add(nextOr);
}
}
} else {
tokens.add(nextOr);
}
}
if (tokens.isEmpty()) {
@ -1027,14 +1033,37 @@ public class QueryStack {
}
String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName());
Condition predicate;
BaseJoiningPredicateBuilder join;
TokenPredicateBuilder join = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.TOKEN, theSourceJoinColumn, paramName, () -> mySqlBuilder.addTokenPredicateBuilder(theSourceJoinColumn)).getResult();
if (paramInverted) {
SearchQueryBuilder sqlBuilder = mySqlBuilder.newChildSqlBuilder();
TokenPredicateBuilder tokenSelector = sqlBuilder.addTokenPredicateBuilder(null);
sqlBuilder.addPredicate(tokenSelector.createPredicateToken(tokens, theResourceName, theSpnamePrefix, theSearchParam, theRequestPartitionId));
SelectQuery sql = sqlBuilder.getSelect();
Expression subSelect = new Subquery(sql);
if (theList.get(0).getMissing() != null) {
return join.createPredicateParamMissingForNonReference(theResourceName, paramName, theList.get(0).getMissing(), theRequestPartitionId);
join = mySqlBuilder.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 {
TokenPredicateBuilder tokenJoin = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.TOKEN, theSourceJoinColumn, paramName, () -> mySqlBuilder.addTokenPredicateBuilder(theSourceJoinColumn)).getResult();
if (theList.get(0).getMissing() != null) {
return tokenJoin.createPredicateParamMissingForNonReference(theResourceName, paramName, theList.get(0).getMissing(), theRequestPartitionId);
}
predicate = tokenJoin.createPredicateToken(tokens, theResourceName, theSpnamePrefix, theSearchParam, theOperation, theRequestPartitionId);
join = tokenJoin;
}
Condition predicate = join.createPredicateToken(tokens, theResourceName, theSpnamePrefix, theSearchParam, theOperation, theRequestPartitionId);
return join.combineWithRequestPartitionIdPredicate(theRequestPartitionId, predicate);
}

View File

@ -62,6 +62,7 @@ import ca.uhn.fhir.rest.param.ReferenceParam;
import ca.uhn.fhir.rest.param.SpecialParam;
import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.param.TokenParamModifier;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
@ -338,6 +339,7 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder {
boolean foundChainMatch = false;
List<String> candidateTargetTypes = new ArrayList<>();
List<Condition> orPredicates = new ArrayList<>();
boolean paramInverted = false;
QueryStack childQueryFactory = myQueryStack.newChildQueryFactoryWithFullBuilderReuse();
for (String nextType : resourceTypes) {
String chain = theReferenceParam.getChain();
@ -383,6 +385,13 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder {
if (chainValue == null) {
continue;
}
// For the token param, if it's a :not modifier, need switch OR to AND
if (!paramInverted && chainValue instanceof TokenParam) {
if (((TokenParam) chainValue).getModifier() == TokenParamModifier.NOT) {
paramInverted = true;
}
}
foundChainMatch = true;
orValues.add(chainValue);
}
@ -410,10 +419,17 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder {
warnAboutPerformanceOnUnqualifiedResources(theParamName, theRequest, candidateTargetTypes);
}
Condition multiTypeOrPredicate = toOrPredicate(orPredicates);
// If :not modifier for a token, switch OR with AND in the multi-type case
Condition multiTypePredicate;
if (paramInverted) {
multiTypePredicate = toAndPredicate(orPredicates);
} else {
multiTypePredicate = toOrPredicate(orPredicates);
}
List<String> pathsToMatch = createResourceLinkPaths(theResourceName, theParamName);
Condition pathPredicate = createPredicateSourcePaths(pathsToMatch);
return toAndPredicate(pathPredicate, multiTypeOrPredicate);
return toAndPredicate(pathPredicate, multiTypePredicate);
}
@Nonnull

View File

@ -63,6 +63,7 @@ import org.hl7.fhir.dstu3.model.AllergyIntolerance;
import org.hl7.fhir.dstu3.model.Appointment;
import org.hl7.fhir.dstu3.model.AuditEvent;
import org.hl7.fhir.dstu3.model.Binary;
import org.hl7.fhir.dstu3.model.BodySite;
import org.hl7.fhir.dstu3.model.Bundle;
import org.hl7.fhir.dstu3.model.CarePlan;
import org.hl7.fhir.dstu3.model.CodeSystem;
@ -95,6 +96,7 @@ import org.hl7.fhir.dstu3.model.Organization;
import org.hl7.fhir.dstu3.model.Patient;
import org.hl7.fhir.dstu3.model.Practitioner;
import org.hl7.fhir.dstu3.model.PractitionerRole;
import org.hl7.fhir.dstu3.model.Procedure;
import org.hl7.fhir.dstu3.model.ProcedureRequest;
import org.hl7.fhir.dstu3.model.Questionnaire;
import org.hl7.fhir.dstu3.model.QuestionnaireResponse;
@ -317,6 +319,12 @@ public abstract class BaseJpaDstu3Test extends BaseJpaTest {
@Qualifier("myTaskDaoDstu3")
protected IFhirResourceDao<Task> myTaskDao;
@Autowired
@Qualifier("myBodySiteDaoDstu3")
protected IFhirResourceDao<BodySite> myBodySiteDao;
@Autowired
@Qualifier("myProcedureDaoDstu3")
protected IFhirResourceDao<Procedure> myProcedureDao;
@Autowired
protected ITermConceptDao myTermConceptDao;
@Autowired
protected ITermCodeSystemDao myTermCodeSystemDao;

View File

@ -0,0 +1,77 @@
package ca.uhn.fhir.jpa.provider.dstu3;
import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
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.rest.api.server.IBundleProvider;
import org.hl7.fhir.dstu3.model.BodySite;
import org.hl7.fhir.dstu3.model.CodeableConcept;
import org.hl7.fhir.dstu3.model.Coding;
import org.hl7.fhir.dstu3.model.Encounter;
import org.hl7.fhir.dstu3.model.Observation;
import org.hl7.fhir.dstu3.model.Patient;
import org.hl7.fhir.dstu3.model.Procedure;
import org.hl7.fhir.dstu3.model.Reference;
import org.hl7.fhir.dstu3.model.SearchParameter;
import org.hl7.fhir.instance.model.api.IIdType;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Collections;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
public class ResourceProviderSearchModifierDstu3Test extends BaseResourceProviderDstu3Test{
@Autowired
MatchUrlService myMatchUrlService;
@Test
public void testReplicateBugWithNotDuringChain() {
Encounter enc = new Encounter();
enc.setType(Collections.singletonList(new CodeableConcept().addCoding(new Coding("system", "value", "display"))));
IIdType encId = myEncounterDao.create(enc).getId();
Observation obs = new Observation();
obs.setContext(new Reference(encId));
myObservationDao.create(obs).getId();
{
//Works when not chained:
String encounterSearchString = "Encounter?type:not=system|value";
ResourceSearch resourceSearch = myMatchUrlService.getResourceSearch(encounterSearchString);
SearchParameterMap searchParameterMap = resourceSearch.getSearchParameterMap();
IBundleProvider search = myEncounterDao.search(searchParameterMap);
assertThat(search.size(), is(equalTo(0)));
}
{
//Works without the NOT qualifier.
String resultSearchString = "Observation?context.type=system|value";
ResourceSearch resourceSearch = myMatchUrlService.getResourceSearch(resultSearchString);
SearchParameterMap searchParameterMap = resourceSearch.getSearchParameterMap();
IBundleProvider search = myObservationDao.search(searchParameterMap);
assertThat(search.size(), is(equalTo(1)));
}
{
//Works in a chain
String noResultSearchString = "Observation?context.type:not=system|value";
ResourceSearch resourceSearch = myMatchUrlService.getResourceSearch(noResultSearchString);
SearchParameterMap searchParameterMap = resourceSearch.getSearchParameterMap();
IBundleProvider search = myObservationDao.search(searchParameterMap);
assertThat(search.size(), is(equalTo(0)));
}
{
//Works in a chain with only value
String noResultSearchString = "Observation?context.type:not=value";
ResourceSearch resourceSearch = myMatchUrlService.getResourceSearch(noResultSearchString);
SearchParameterMap searchParameterMap = resourceSearch.getSearchParameterMap();
IBundleProvider search = myObservationDao.search(searchParameterMap);
assertThat(search.size(), is(equalTo(0)));
}
}
}

View File

@ -0,0 +1,249 @@
package ca.uhn.fhir.jpa.provider.r4;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.io.IOUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.CodeableConcept;
import org.hl7.fhir.r4.model.DateTimeType;
import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.Observation.ObservationStatus;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Quantity;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.parser.StrictErrorHandler;
import ca.uhn.fhir.rest.client.interceptor.CapturingInterceptor;
public class ResourceProviderSearchModifierR4Test extends BaseResourceProviderR4Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ResourceProviderSearchModifierR4Test.class);
private CapturingInterceptor myCapturingInterceptor = new CapturingInterceptor();
@Override
@AfterEach
public void after() throws Exception {
super.after();
myDaoConfig.setAllowMultipleDelete(new DaoConfig().isAllowMultipleDelete());
myDaoConfig.setAllowExternalReferences(new DaoConfig().isAllowExternalReferences());
myDaoConfig.setReuseCachedSearchResultsForMillis(new DaoConfig().getReuseCachedSearchResultsForMillis());
myDaoConfig.setCountSearchResultsUpTo(new DaoConfig().getCountSearchResultsUpTo());
myDaoConfig.setSearchPreFetchThresholds(new DaoConfig().getSearchPreFetchThresholds());
myDaoConfig.setAllowContainsSearches(new DaoConfig().isAllowContainsSearches());
myDaoConfig.setIndexMissingFields(new DaoConfig().getIndexMissingFields());
myClient.unregisterInterceptor(myCapturingInterceptor);
}
@BeforeEach
@Override
public void before() throws Exception {
super.before();
myFhirCtx.setParserErrorHandler(new StrictErrorHandler());
myDaoConfig.setAllowMultipleDelete(true);
myClient.registerInterceptor(myCapturingInterceptor);
myDaoConfig.setSearchPreFetchThresholds(new DaoConfig().getSearchPreFetchThresholds());
}
@BeforeEach
public void beforeDisableResultReuse() {
myDaoConfig.setReuseCachedSearchResultsForMillis(null);
}
@Test
public void testSearch_SingleCode_not_modifier() throws Exception {
List<IIdType> obsList = createObs(10, false);
String uri = ourServerBase + "/Observation?code:not=2345-3";
List<String> ids = searchAndReturnUnqualifiedVersionlessIdValues(uri);
assertEquals(9, ids.size());
assertEquals(obsList.get(0).toString(), ids.get(0));
assertEquals(obsList.get(1).toString(), ids.get(1));
assertEquals(obsList.get(2).toString(), ids.get(2));
assertEquals(obsList.get(4).toString(), ids.get(3));
assertEquals(obsList.get(5).toString(), ids.get(4));
assertEquals(obsList.get(6).toString(), ids.get(5));
assertEquals(obsList.get(7).toString(), ids.get(6));
assertEquals(obsList.get(8).toString(), ids.get(7));
assertEquals(obsList.get(9).toString(), ids.get(8));
}
@Test
public void testSearch_SingleCode_multiple_not_modifier() throws Exception {
List<IIdType> obsList = createObs(10, false);
String uri = ourServerBase + "/Observation?code:not=2345-3&code:not=2345-7&code:not=2345-9";
List<String> ids = searchAndReturnUnqualifiedVersionlessIdValues(uri);
assertEquals(7, ids.size());
assertEquals(obsList.get(0).toString(), ids.get(0));
assertEquals(obsList.get(1).toString(), ids.get(1));
assertEquals(obsList.get(2).toString(), ids.get(2));
assertEquals(obsList.get(4).toString(), ids.get(3));
assertEquals(obsList.get(5).toString(), ids.get(4));
assertEquals(obsList.get(6).toString(), ids.get(5));
assertEquals(obsList.get(8).toString(), ids.get(6));
}
@Test
public void testSearch_SingleCode_mix_modifier() throws Exception {
List<IIdType> obsList = createObs(10, false);
// Observation?code:not=2345-3&code:not=2345-7&code:not=2345-9
// slower than Observation?code:not=2345-3&code=2345-7&code:not=2345-9
String uri = ourServerBase + "/Observation?code:not=2345-3&code=2345-7&code:not=2345-9";
List<String> ids = searchAndReturnUnqualifiedVersionlessIdValues(uri);
assertEquals(1, ids.size());
assertEquals(obsList.get(7).toString(), ids.get(0));
}
@Test
public void testSearch_SingleCode_or_not_modifier() throws Exception {
List<IIdType> obsList = createObs(10, false);
String uri = ourServerBase + "/Observation?code:not=2345-3,2345-7,2345-9";
List<String> ids = searchAndReturnUnqualifiedVersionlessIdValues(uri);
assertEquals(7, ids.size());
assertEquals(obsList.get(0).toString(), ids.get(0));
assertEquals(obsList.get(1).toString(), ids.get(1));
assertEquals(obsList.get(2).toString(), ids.get(2));
assertEquals(obsList.get(4).toString(), ids.get(3));
assertEquals(obsList.get(5).toString(), ids.get(4));
assertEquals(obsList.get(6).toString(), ids.get(5));
assertEquals(obsList.get(8).toString(), ids.get(6));
}
@Test
public void testSearch_MultiCode_not_modifier() throws Exception {
List<IIdType> obsList = createObs(10, true);
String uri = ourServerBase + "/Observation?code:not=2345-3";
List<String> ids = searchAndReturnUnqualifiedVersionlessIdValues(uri);
assertEquals(8, ids.size());
assertEquals(obsList.get(0).toString(), ids.get(0));
assertEquals(obsList.get(1).toString(), ids.get(1));
assertEquals(obsList.get(4).toString(), ids.get(2));
assertEquals(obsList.get(5).toString(), ids.get(3));
assertEquals(obsList.get(6).toString(), ids.get(4));
assertEquals(obsList.get(7).toString(), ids.get(5));
assertEquals(obsList.get(8).toString(), ids.get(6));
assertEquals(obsList.get(9).toString(), ids.get(7));
}
@Test
public void testSearch_MultiCode_multiple_not_modifier() throws Exception {
List<IIdType> obsList = createObs(10, true);
String uri = ourServerBase + "/Observation?code:not=2345-3&code:not=2345-4";
List<String> ids = searchAndReturnUnqualifiedVersionlessIdValues(uri);
assertEquals(7, ids.size());
assertEquals(obsList.get(0).toString(), ids.get(0));
assertEquals(obsList.get(1).toString(), ids.get(1));
assertEquals(obsList.get(5).toString(), ids.get(2));
assertEquals(obsList.get(6).toString(), ids.get(3));
assertEquals(obsList.get(7).toString(), ids.get(4));
assertEquals(obsList.get(8).toString(), ids.get(5));
assertEquals(obsList.get(9).toString(), ids.get(6));
}
@Test
public void testSearch_MultiCode_mix_modifier() throws Exception {
List<IIdType> obsList = createObs(10, true);
String uri = ourServerBase + "/Observation?code:not=2345-3&code=2345-7&code:not=2345-9";
List<String> ids = searchAndReturnUnqualifiedVersionlessIdValues(uri);
assertEquals(2, ids.size());
assertEquals(obsList.get(6).toString(), ids.get(0));
assertEquals(obsList.get(7).toString(), ids.get(1));
}
@Test
public void testSearch_MultiCode_or_not_modifier() throws Exception {
List<IIdType> obsList = createObs(10, true);
String uri = ourServerBase + "/Observation?code:not=2345-3,2345-7,2345-9";
List<String> ids = searchAndReturnUnqualifiedVersionlessIdValues(uri);
assertEquals(4, ids.size());
assertEquals(obsList.get(0).toString(), ids.get(0));
assertEquals(obsList.get(1).toString(), ids.get(1));
assertEquals(obsList.get(4).toString(), ids.get(2));
assertEquals(obsList.get(5).toString(), ids.get(3));
}
private List<String> searchAndReturnUnqualifiedVersionlessIdValues(String uri) throws IOException {
List<String> ids;
HttpGet get = new HttpGet(uri);
try (CloseableHttpResponse response = ourHttpClient.execute(get)) {
String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info("Response was: {}", resp);
Bundle bundle = myFhirCtx.newXmlParser().parseResource(Bundle.class, resp);
ourLog.info("Bundle: \n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(bundle));
ids = toUnqualifiedVersionlessIdValues(bundle);
}
return ids;
}
private List<IIdType> createObs(int obsNum, boolean isMultiple) {
Patient patient = new Patient();
patient.addIdentifier().setSystem("urn:system").setValue("001");
patient.addName().setFamily("Tester").addGiven("Joe");
IIdType pid = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless();
List<IIdType> obsIds = new ArrayList<>();
IIdType obsId = null;
for (int i=0; i<obsNum; i++) {
Observation obs = new Observation();
obs.setStatus(ObservationStatus.FINAL);
obs.getSubject().setReferenceElement(pid);
obs.setEffective(new DateTimeType("2001-02-01"));
CodeableConcept cc = obs.getCode();
cc.addCoding().setCode("2345-"+i).setSystem("http://loinc.org");
obs.setValue(new Quantity().setValue(200));
if (isMultiple) {
cc = obs.getCode();
cc.addCoding().setCode("2345-"+(i+1)).setSystem("http://loinc.org");
obs.setValue(new Quantity().setValue(300));
}
obsId = myObservationDao.create(obs).getId().toUnqualifiedVersionless();
obsIds.add(obsId);
}
return obsIds;
}
}