Searching with more than one chained Bundle SearchParameter returns incorrect results (#5614)

* Failing test

* fix

* changelog
This commit is contained in:
Nathan Doef 2024-01-24 10:35:40 -05:00 committed by GitHub
parent 47867fc628
commit 4b1ac778cc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 85 additions and 2 deletions

View File

@ -0,0 +1,5 @@
---
type: fix
issue: 5623
title: "Previously, searches that used more than one chained `Bundle` `SearchParameter` (i.e. `Composition`) were only
adding one condition to the underlying SQL query which resulted in incorrect search results. This has been fixed."

View File

@ -2466,7 +2466,7 @@ public class QueryStack {
theRequestPartitionId, theRequestPartitionId,
andPredicates, andPredicates,
nextAnd)) { nextAnd)) {
break; continue;
} }
EmbeddedChainedSearchModeEnum embeddedChainedSearchModeEnum = EmbeddedChainedSearchModeEnum embeddedChainedSearchModeEnum =

View File

@ -11,14 +11,18 @@ import ca.uhn.fhir.jpa.test.BaseJpaR4Test;
import ca.uhn.fhir.jpa.util.SqlQuery; import ca.uhn.fhir.jpa.util.SqlQuery;
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 ca.uhn.fhir.rest.param.ReferenceParam;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.AuditEvent; import org.hl7.fhir.r4.model.AuditEvent;
import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.CodeableConcept;
import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.Composition;
import org.hl7.fhir.r4.model.Device; import org.hl7.fhir.r4.model.Device;
import org.hl7.fhir.r4.model.DomainResource; import org.hl7.fhir.r4.model.DomainResource;
import org.hl7.fhir.r4.model.Encounter; import org.hl7.fhir.r4.model.Encounter;
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.Location; import org.hl7.fhir.r4.model.Location;
import org.hl7.fhir.r4.model.MessageHeader; import org.hl7.fhir.r4.model.MessageHeader;
@ -27,22 +31,26 @@ import org.hl7.fhir.r4.model.Organization;
import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Quantity; import org.hl7.fhir.r4.model.Quantity;
import org.hl7.fhir.r4.model.Reference; import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.SearchParameter;
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.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import java.io.IOException; import java.io.IOException;
import java.sql.Date;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import static org.apache.commons.lang3.StringUtils.countMatches; import static org.apache.commons.lang3.StringUtils.countMatches;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.in;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assertions.fail;
@ -1573,6 +1581,76 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
countUnionStatementsInGeneratedQuery("/Observation?subject:Location.name=Smith", 1); countUnionStatementsInGeneratedQuery("/Observation?subject:Location.name=Smith", 1);
} }
@ParameterizedTest
@CsvSource({
// search url expected count
"/Bundle?composition.patient.identifier=system|value-1&composition.patient.birthdate=1980-01-01, 1", // correct identifier, correct birthdate
"/Bundle?composition.patient.birthdate=1980-01-01&composition.patient.identifier=system|value-1, 1", // correct birthdate, correct identifier
"/Bundle?composition.patient.identifier=system|value-1&composition.patient.birthdate=2000-01-01, 0", // correct identifier, incorrect birthdate
"/Bundle?composition.patient.birthdate=2000-01-01&composition.patient.identifier=system|value-1, 0", // incorrect birthdate, correct identifier
"/Bundle?composition.patient.identifier=system|value-2&composition.patient.birthdate=1980-01-01, 0", // incorrect identifier, correct birthdate
"/Bundle?composition.patient.birthdate=1980-01-01&composition.patient.identifier=system|value-2, 0", // correct birthdate, incorrect identifier
"/Bundle?composition.patient.identifier=system|value-2&composition.patient.birthdate=2000-01-01, 0", // incorrect identifier, incorrect birthdate
"/Bundle?composition.patient.birthdate=2000-01-01&composition.patient.identifier=system|value-2, 0", // incorrect birthdate, incorrect identifier
})
public void testMultipleChainedBundleCompositionSearchParameters(String theSearchUrl, int theExpectedCount) {
createSearchParameter("bundle-composition-patient-birthdate",
"composition.patient.birthdate",
"Bundle",
"Bundle.entry.resource.ofType(Patient).birthDate",
Enumerations.SearchParamType.DATE
);
createSearchParameter("bundle-composition-patient-identifier",
"composition.patient.identifier",
"Bundle",
"Bundle.entry.resource.ofType(Patient).identifier",
Enumerations.SearchParamType.TOKEN
);
createDocumentBundleWithPatientDetails("1980-01-01", "system", "value-1");
SearchParameterMap params = myMatchUrlService.getResourceSearch(theSearchUrl).getSearchParameterMap().setLoadSynchronous(true);
assertSearchReturns(myBundleDao, params, theExpectedCount);
}
private void createSearchParameter(String theId, String theCode, String theBase, String theExpression, Enumerations.SearchParamType theType) {
SearchParameter searchParameter = new SearchParameter();
searchParameter.setId(theId);
searchParameter.setCode(theCode);
searchParameter.setName(theCode);
searchParameter.setUrl("http://example.org/SearchParameter/" + theId);
searchParameter.setStatus(Enumerations.PublicationStatus.ACTIVE);
searchParameter.addBase(theBase);
searchParameter.setType(theType);
searchParameter.setExpression(theExpression);
searchParameter = (SearchParameter) mySearchParameterDao.update(searchParameter, mySrd).getResource();
mySearchParamRegistry.forceRefresh();
assertNotNull(mySearchParamRegistry.getActiveSearchParam(theBase, searchParameter.getName()));
}
private void createDocumentBundleWithPatientDetails(String theBirthDate, String theIdentifierSystem, String theIdentifierValue) {
Patient patient = new Patient();
patient.setBirthDate(Date.valueOf(theBirthDate));
patient.addIdentifier().setSystem(theIdentifierSystem).setValue(theIdentifierValue);
patient = (Patient) myPatientDao.create(patient, mySrd).getResource();
assertSearchReturns(myPatientDao, SearchParameterMap.newSynchronous(), 1);
Bundle bundle = new Bundle();
bundle.setType(Bundle.BundleType.DOCUMENT);
Composition composition = new Composition();
composition.setType(new CodeableConcept().addCoding(new Coding().setCode("code").setSystem("http://example.org")));
bundle.addEntry().setResource(composition);
composition.getSubject().setReference(patient.getIdElement().getValue());
bundle.addEntry().setResource(patient);
myBundleDao.create(bundle, mySrd);
assertSearchReturns(myBundleDao, SearchParameterMap.newSynchronous(), 1);
}
private void assertSearchReturns(IFhirResourceDao<?> theDao, SearchParameterMap theSearchParams, int theExpectedCount){
assertEquals(theExpectedCount, theDao.search(theSearchParams, mySrd).size());
}
private void countUnionStatementsInGeneratedQuery(String theUrl, int theExpectedNumberOfUnions) throws IOException { private void countUnionStatementsInGeneratedQuery(String theUrl, int theExpectedNumberOfUnions) throws IOException {
myCaptureQueriesListener.clear(); myCaptureQueriesListener.clear();
searchAndReturnUnqualifiedVersionlessIdValues(theUrl); searchAndReturnUnqualifiedVersionlessIdValues(theUrl);