diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5623-searching-with-multiple-bundle-composition-searchparameters-fix.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5623-searching-with-multiple-bundle-composition-searchparameters-fix.yaml new file mode 100644 index 00000000000..9c99c5218d6 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5623-searching-with-multiple-bundle-composition-searchparameters-fix.yaml @@ -0,0 +1,6 @@ +--- +type: fix +issue: 5623 +backport: 6.10.5 +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." diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/QueryStack.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/QueryStack.java index 94fd99ff529..25c9acb89ee 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/QueryStack.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/QueryStack.java @@ -2466,7 +2466,7 @@ public class QueryStack { theRequestPartitionId, andPredicates, nextAnd)) { - break; + continue; } EmbeddedChainedSearchModeEnum embeddedChainedSearchModeEnum = diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/ChainingR4SearchTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/ChainingR4SearchTest.java index 31fecd59bdd..7c75a50ae87 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/ChainingR4SearchTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/ChainingR4SearchTest.java @@ -11,14 +11,18 @@ import ca.uhn.fhir.jpa.test.BaseJpaR4Test; 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.param.ReferenceParam; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.AuditEvent; 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.Composition; import org.hl7.fhir.r4.model.Device; import org.hl7.fhir.r4.model.DomainResource; 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.Location; 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.Quantity; import org.hl7.fhir.r4.model.Reference; +import org.hl7.fhir.r4.model.SearchParameter; import org.hl7.fhir.r4.model.StringType; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; 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 java.io.IOException; +import java.sql.Date; 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.hamcrest.Matchers.in; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.fail; @@ -1573,6 +1581,76 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { 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 { myCaptureQueriesListener.clear(); searchAndReturnUnqualifiedVersionlessIdValues(theUrl);