diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/sql/SearchQueryBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/sql/SearchQueryBuilder.java index ca3314c0c2e..7b3faedb77f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/sql/SearchQueryBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/sql/SearchQueryBuilder.java @@ -549,7 +549,7 @@ public class SearchQueryBuilder { } public ComboCondition addPredicateLastUpdated(DateRangeParam theDateRange) { - ResourceTablePredicateBuilder resourceTableRoot = getOrCreateResourceTablePredicateBuilder(); + ResourceTablePredicateBuilder resourceTableRoot = getOrCreateResourceTablePredicateBuilder(false); List conditions = new ArrayList<>(2); if (theDateRange.getLowerBoundAsInstant() != null) { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchOptimizedTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchOptimizedTest.java index d58e23b5675..e726c5254da 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchOptimizedTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchOptimizedTest.java @@ -16,6 +16,7 @@ import ca.uhn.fhir.rest.api.SearchTotalModeEnum; import ca.uhn.fhir.rest.api.SortSpec; import ca.uhn.fhir.rest.api.SummaryEnum; import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.param.DateRangeParam; import ca.uhn.fhir.rest.param.ReferenceOrListParam; import ca.uhn.fhir.rest.param.ReferenceParam; import ca.uhn.fhir.rest.param.StringParam; @@ -917,18 +918,60 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test { } + /** + * Make sure that if we're performing a query where the ONLY param is _lastUpdated, + * we include a selector for the resource type + */ + @Test + public void testSearchByLastUpdatedOnly() { + Patient p = new Patient(); + p.setId("B"); + myPatientDao.update(p); + + Observation obs = new Observation(); + obs.setId("A"); + obs.setSubject(new Reference("Patient/B")); + obs.setStatus(Observation.ObservationStatus.FINAL); + myObservationDao.update(obs); + + // Search using only a _lastUpdated param + { + SearchParameterMap map = SearchParameterMap.newSynchronous(); + map.setLastUpdated(new DateRangeParam("ge2021-01-01", null)); + myCaptureQueriesListener.clear(); + IBundleProvider outcome = myObservationDao.search(map, new SystemRequestDetails()); + assertEquals(1, outcome.getResources(0, 999).size()); + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + + String selectQuery = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false); + assertEquals(1, StringUtils.countMatches(selectQuery.toLowerCase(), "select t0.res_id from hfj_resource t0"), selectQuery); + assertEquals(1, StringUtils.countMatches(selectQuery.toLowerCase(), "t0.res_type = 'observation'"), selectQuery); + assertEquals(1, StringUtils.countMatches(selectQuery.toLowerCase(), "t0.res_deleted_at is null"), selectQuery); + } + } + @Test public void testSearchOnUnderscoreParams_AvoidHFJResourceJoins() { // This Issue: https://github.com/hapifhir/hapi-fhir/issues/2942 // See this PR for a similar type of Fix: https://github.com/hapifhir/hapi-fhir/pull/2909 - SearchParameter searchParameter = new SearchParameter(); - searchParameter.addBase("BodySite").addBase("Procedure"); - searchParameter.setCode("focalAccess"); - searchParameter.setType(Enumerations.SearchParamType.REFERENCE); - searchParameter.setExpression("Procedure.extension('Procedure#focalAccess')"); - searchParameter.setXpathUsage(SearchParameter.XPathUsageType.NORMAL); - searchParameter.setStatus(Enumerations.PublicationStatus.ACTIVE); - IIdType spId = mySearchParameterDao.create(searchParameter).getId().toUnqualifiedVersionless(); + // SearchParam - focalAccess + SearchParameter searchParameter1 = new SearchParameter(); + searchParameter1.addBase("BodySite").addBase("Procedure"); + searchParameter1.setCode("focalAccess"); + searchParameter1.setType(Enumerations.SearchParamType.REFERENCE); + searchParameter1.setExpression("Procedure.extension('Procedure#focalAccess')"); + searchParameter1.setXpathUsage(SearchParameter.XPathUsageType.NORMAL); + searchParameter1.setStatus(Enumerations.PublicationStatus.ACTIVE); + IIdType sp1Id = mySearchParameterDao.create(searchParameter1).getId().toUnqualifiedVersionless(); + // SearchParam - focalAccess + SearchParameter searchParameter2 = new SearchParameter(); + searchParameter2.addBase("Provenance"); + searchParameter2.setCode("activity"); + searchParameter2.setType(Enumerations.SearchParamType.TOKEN); + searchParameter2.setExpression("Provenance.extension('Provenance#activity')"); + searchParameter2.setXpathUsage(SearchParameter.XPathUsageType.NORMAL); + searchParameter2.setStatus(Enumerations.PublicationStatus.ACTIVE); + IIdType sp2Id = mySearchParameterDao.create(searchParameter2).getId().toUnqualifiedVersionless(); mySearchParamRegistry.forceRefresh(); BodyStructure bs = new BodyStructure(); @@ -968,6 +1011,15 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test { .addTag("acc_procext_fkc", "1STCANN2NDL", "First Successful Cannulation with 2 Needles"); IIdType procedureId = myProcedureDao.create(procedure).getId().toUnqualifiedVersionless(); + Device device = new Device(); + device.setManufacturer("Acme"); + IIdType deviceId = myDeviceDao.create(device).getId().toUnqualifiedVersionless(); + + Provenance provenance = new Provenance(); + provenance.setActivity(new CodeableConcept().addCoding(new Coding().setSystem("http://hl7.org/fhir/v3/DocumentCompletion").setCode("PA"))); + provenance.addAgent().setWho(new Reference(deviceId)); + IIdType provenanceId = myProvenanceDao.create(provenance).getId().toUnqualifiedVersionless(); + logAllResources(); logAllResourceTags(); logAllResourceVersions(); @@ -1027,6 +1079,30 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test { assertEquals(0, StringUtils.countMatches(selectQuery.toLowerCase(), ".res_type = 'procedure'"), selectQuery); assertEquals(0, StringUtils.countMatches(selectQuery.toLowerCase(), ".res_deleted_at is null"), selectQuery); } + + // Search example 3: + // http://FHIR_SERVER/fhir_request/Provenance + // ?agent=Acme&activity=PA&_lastUpdated=ge2021-01-01&_requestTrace=True + // NOTE: This gets sorted once so the order is different once it gets executed! + { + // IMPORTANT: Keep the query param order exactly as shown below! + // NOTE: The "outcome" SearchParameter is not being used below, but it doesn't affect the test. + SearchParameterMap map = SearchParameterMap.newSynchronous(); + map.add("agent", new ReferenceParam("Device/" + deviceId.getIdPart())); + map.add("activity", new TokenParam("PA")); + DateRangeParam dateRangeParam = new DateRangeParam("ge2021-01-01", null); + map.setLastUpdated(dateRangeParam); + myCaptureQueriesListener.clear(); + IBundleProvider outcome = myProvenanceDao.search(map, new SystemRequestDetails()); + ourLog.info("Search returned {} resources.", outcome.getResources(0, 999).size()); + //assertEquals(1, outcome.getResources(0, 999).size()); + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + + String selectQuery = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false); + // Ensure that we do NOT see a couple of particular WHERE clauses + assertEquals(0, StringUtils.countMatches(selectQuery.toLowerCase(), ".res_type = 'provenance'"), selectQuery); + assertEquals(0, StringUtils.countMatches(selectQuery.toLowerCase(), ".res_deleted_at is null"), selectQuery); + } } @AfterEach