diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchSqlTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchSqlTest.java index e26273efff7..61d0aa047e2 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchSqlTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchSqlTest.java @@ -5,9 +5,7 @@ import ca.uhn.fhir.jpa.model.config.PartitionSettings; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.test.BaseJpaR4Test; import ca.uhn.fhir.rest.api.Constants; -import ca.uhn.fhir.rest.api.SortSpec; import ca.uhn.fhir.rest.api.server.IBundleProvider; -import ca.uhn.fhir.rest.param.StringParam; import ca.uhn.fhir.rest.param.TokenParam; import ca.uhn.fhir.rest.param.UriParam; import org.hl7.fhir.instance.model.api.IIdType; @@ -17,7 +15,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.MethodSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -26,7 +24,6 @@ import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; public class FhirResourceDaoR4SearchSqlTest extends BaseJpaR4Test { @@ -47,80 +44,73 @@ public class FhirResourceDaoR4SearchSqlTest extends BaseJpaR4Test { } - /** - * One regular search params - Doesn't need HFJ_RESOURCE as root - */ - @Test - public void testSingleRegularSearchParam() { + record SqlGenerationTestCase(String comment, String restQuery, String expectedSql, String expectedPartitionedSql) {} - myCaptureQueriesListener.clear(); - SearchParameterMap map = SearchParameterMap.newSynchronous(Patient.SP_NAME, new StringParam("FOO")); - myPatientDao.search(map); - assertEquals(1, myCaptureQueriesListener.countSelectQueries()); - String sql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(false, false); - assertEquals("SELECT t0.RES_ID FROM HFJ_SPIDX_STRING t0 WHERE ((t0.HASH_NORM_PREFIX = ?) AND (t0.SP_VALUE_NORMALIZED LIKE ?))", sql); + static List sqlGenerationTestCases() { + return List.of( + new SqlGenerationTestCase( + "single string - no hfj_resource root", + "Patient?name=FOO", + "SELECT t0.RES_ID FROM HFJ_SPIDX_STRING t0 WHERE ((t0.HASH_NORM_PREFIX = ?) AND (t0.SP_VALUE_NORMALIZED LIKE ?))", + "SELECT t0.PARTITION_ID,t0.RES_ID FROM HFJ_SPIDX_STRING t0 WHERE ((t0.HASH_NORM_PREFIX = ?) AND (t0.SP_VALUE_NORMALIZED LIKE ?))" + ) + , new SqlGenerationTestCase( + "two regular params - should use hfj_resource as root", + "Patient?name=smith&active=true", + "SELECT t1.RES_ID FROM HFJ_RESOURCE t1 INNER JOIN HFJ_SPIDX_STRING t0 ON (t1.RES_ID = t0.RES_ID) INNER JOIN HFJ_SPIDX_TOKEN t2 ON (t1.RES_ID = t2.RES_ID) WHERE (((t0.HASH_NORM_PREFIX = ?) AND (t0.SP_VALUE_NORMALIZED LIKE ?)) AND (t2.HASH_VALUE = ?))", + "SELECT t1.PARTITION_ID,t1.RES_ID FROM HFJ_RESOURCE t1 INNER JOIN HFJ_SPIDX_STRING t0 ON ((t1.PARTITION_ID = t0.PARTITION_ID) AND (t1.RES_ID = t0.RES_ID)) INNER JOIN HFJ_SPIDX_TOKEN t2 ON ((t1.PARTITION_ID = t2.PARTITION_ID) AND (t1.RES_ID = t2.RES_ID)) WHERE (((t0.HASH_NORM_PREFIX = ?) AND (t0.SP_VALUE_NORMALIZED LIKE ?)) AND (t2.HASH_VALUE = ?))" + ) + , new SqlGenerationTestCase( + "token not as a NOT IN subselect", + "Encounter?class:not=not-there", + "SELECT t0.RES_ID FROM HFJ_RESOURCE t0 WHERE (((t0.RES_TYPE = ?) AND (t0.RES_DELETED_AT IS NULL)) AND ((t0.RES_ID) NOT IN (SELECT t0.RES_ID FROM HFJ_SPIDX_TOKEN t0 WHERE (t0.HASH_VALUE = ?)) ))", + "SELECT t0.PARTITION_ID,t0.RES_ID FROM HFJ_RESOURCE t0 WHERE (((t0.RES_TYPE = ?) AND (t0.RES_DELETED_AT IS NULL)) AND ((t0.PARTITION_ID,t0.RES_ID) NOT IN (SELECT t0.PARTITION_ID,t0.RES_ID FROM HFJ_SPIDX_TOKEN t0 WHERE (t0.HASH_VALUE = ?)) ))" + ) + , new SqlGenerationTestCase( + "token not on chain join - NOT IN from hfj_res_link target columns", + "Observation?encounter.class:not=not-there", + "SELECT t0.SRC_RESOURCE_ID FROM HFJ_RES_LINK t0 WHERE ((t0.SRC_PATH = ?) AND ((t0.TARGET_RESOURCE_ID) NOT IN (SELECT t0.RES_ID FROM HFJ_SPIDX_TOKEN t0 WHERE (t0.HASH_VALUE = ?)) ))", + "SELECT t0.PARTITION_ID,t0.SRC_RESOURCE_ID FROM HFJ_RES_LINK t0 WHERE ((t0.SRC_PATH = ?) AND ((t0.TARGET_RES_PARTITION_ID,t0.TARGET_RESOURCE_ID) NOT IN (SELECT t0.PARTITION_ID,t0.RES_ID FROM HFJ_SPIDX_TOKEN t0 WHERE (t0.HASH_VALUE = ?)) ))" + ) + , new SqlGenerationTestCase( + "bare sort", + "Patient?_sort=name", + "SELECT t0.RES_ID FROM HFJ_RESOURCE t0 LEFT OUTER JOIN HFJ_SPIDX_STRING t1 ON ((t0.RES_ID = t1.RES_ID) AND (t1.HASH_IDENTITY = ?)) WHERE ((t0.RES_TYPE = ?) AND (t0.RES_DELETED_AT IS NULL)) ORDER BY t1.SP_VALUE_NORMALIZED ASC NULLS LAST", + "SELECT t0.PARTITION_ID,t0.RES_ID FROM HFJ_RESOURCE t0 LEFT OUTER JOIN HFJ_SPIDX_STRING t1 ON ((t0.PARTITION_ID = t1.PARTITION_ID) AND (t0.RES_ID = t1.RES_ID) AND (t1.HASH_IDENTITY = ?)) WHERE ((t0.RES_TYPE = ?) AND (t0.RES_DELETED_AT IS NULL)) ORDER BY t1.SP_VALUE_NORMALIZED ASC NULLS LAST" + ) + ); + } + + @ParameterizedTest + @MethodSource("sqlGenerationTestCases") + void testSqlGeneration_DefaultNoPartitionJoin(SqlGenerationTestCase theTestCase) { + // default config + + String sql = getSqlForRestQuery(theTestCase.restQuery); + + assertEquals(theTestCase.expectedSql, sql, theTestCase.comment); } @ParameterizedTest - @CsvSource(textBlock = """ - single param - no hfj_resource, Patient?name=smith , 'SELECT t0.RES_ID FROM HFJ_SPIDX_STRING t0 WHERE ((t0.HASH_NORM_PREFIX = ?) AND (t0.SP_VALUE_NORMALIZED LIKE ?))' - single join, Patient?name=smith&active=true ,'SELECT t1.RES_ID FROM HFJ_RESOURCE t1 INNER JOIN HFJ_SPIDX_STRING t0 ON (t1.RES_ID = t0.RES_ID) INNER JOIN HFJ_SPIDX_TOKEN t2 ON (t1.RES_ID = t2.RES_ID) WHERE (((t0.HASH_NORM_PREFIX = ?) AND (t0.SP_VALUE_NORMALIZED LIKE ?)) AND (t2.HASH_VALUE = ?))' - not, Encounter?class:not=not-there ,'SELECT t0.RES_ID FROM HFJ_RESOURCE t0 WHERE (((t0.RES_TYPE = ?) AND (t0.RES_DELETED_AT IS NULL)) AND ((t0.RES_ID) NOT IN (SELECT t0.RES_ID FROM HFJ_SPIDX_TOKEN t0 WHERE (t0.HASH_VALUE = ?)) ))' - not in chain, Observation?encounter.class:not=not-there ,'SELECT t0.SRC_RESOURCE_ID FROM HFJ_RES_LINK t0 WHERE ((t0.SRC_PATH = ?) AND ((t0.TARGET_RESOURCE_ID) NOT IN (SELECT t0.RES_ID FROM HFJ_SPIDX_TOKEN t0 WHERE (t0.HASH_VALUE = ?)) ))' - bare sort, Patient?_sort=name ,'SELECT t0.RES_ID FROM HFJ_RESOURCE t0 LEFT OUTER JOIN HFJ_SPIDX_STRING t1 ON ((t0.RES_ID = t1.RES_ID) AND (t1.HASH_IDENTITY = ?)) WHERE ((t0.RES_TYPE = ?) AND (t0.RES_DELETED_AT IS NULL)) ORDER BY t1.SP_VALUE_NORMALIZED ASC NULLS LAST' - """) - public void testSqlGeneration(String theComment, String theFhirRestQuery, String theExpectedSql) { - // setup - myCaptureQueriesListener.clear(); - - // execute - myTestDaoSearch.searchForIds(theFhirRestQuery); - - // verify - assertEquals(1, myCaptureQueriesListener.countSelectQueries()); - String sql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(false, false); - assertEquals(theExpectedSql, sql, theComment); - } - - @ParameterizedTest - @CsvSource(textBlock = """ - single- no hfj_resource,Patient?name=smith ,'SELECT t0.PARTITION_ID,t0.RES_ID FROM HFJ_SPIDX_STRING t0 WHERE ((t0.HASH_NORM_PREFIX = ?) AND (t0.SP_VALUE_NORMALIZED LIKE ?))' - single join, Patient?name=smith&active=true ,'SELECT t1.PARTITION_ID,t1.RES_ID FROM HFJ_RESOURCE t1 INNER JOIN HFJ_SPIDX_STRING t0 ON ((t1.PARTITION_ID = t0.PARTITION_ID) AND (t1.RES_ID = t0.RES_ID)) INNER JOIN HFJ_SPIDX_TOKEN t2 ON ((t1.PARTITION_ID = t2.PARTITION_ID) AND (t1.RES_ID = t2.RES_ID)) WHERE (((t0.HASH_NORM_PREFIX = ?) AND (t0.SP_VALUE_NORMALIZED LIKE ?)) AND (t2.HASH_VALUE = ?))' - not, Encounter?class:not=not-there ,'SELECT t0.PARTITION_ID,t0.RES_ID FROM HFJ_RESOURCE t0 WHERE (((t0.RES_TYPE = ?) AND (t0.RES_DELETED_AT IS NULL)) AND ((t0.PARTITION_ID,t0.RES_ID) NOT IN (SELECT t0.PARTITION_ID,t0.RES_ID FROM HFJ_SPIDX_TOKEN t0 WHERE (t0.HASH_VALUE = ?)) ))' - not in chain, Observation?encounter.class:not=not-there ,'SELECT t0.PARTITION_ID,t0.SRC_RESOURCE_ID FROM HFJ_RES_LINK t0 WHERE ((t0.SRC_PATH = ?) AND ((t0.TARGET_RES_PARTITION_ID,t0.TARGET_RESOURCE_ID) NOT IN (SELECT t0.PARTITION_ID,t0.RES_ID FROM HFJ_SPIDX_TOKEN t0 WHERE (t0.HASH_VALUE = ?)) ))' - bare sort, Patient?_sort=name ,'SELECT t0.PARTITION_ID,t0.RES_ID FROM HFJ_RESOURCE t0 LEFT OUTER JOIN HFJ_SPIDX_STRING t1 ON ((t0.PARTITION_ID = t1.PARTITION_ID) AND (t0.RES_ID = t1.RES_ID) AND (t1.HASH_IDENTITY = ?)) WHERE ((t0.RES_TYPE = ?) AND (t0.RES_DELETED_AT IS NULL)) ORDER BY t1.SP_VALUE_NORMALIZED ASC NULLS LAST' - """) - public void testSqlGenerationWithPartitionJoins(String theComment, String theFhirRestQuery, String theExpectedSql) { - // setup + @MethodSource("sqlGenerationTestCases") + void testSqlGeneration_WithPartitionJoins(SqlGenerationTestCase theTestCase) { + // include partition_id in joins myPartitionSettings.setDefaultPartitionId(0); myPartitionSettings.setPartitionIdsInPrimaryKeys(true); - myCaptureQueriesListener.clear(); - // execute + String sql = getSqlForRestQuery(theTestCase.restQuery); + + assertEquals(theTestCase.expectedPartitionedSql, sql, theTestCase.comment); + } + + private String getSqlForRestQuery(String theFhirRestQuery) { + myCaptureQueriesListener.clear(); myTestDaoSearch.searchForIds(theFhirRestQuery); - - // verify assertEquals(1, myCaptureQueriesListener.countSelectQueries()); - String sql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(false, false); - assertEquals(theExpectedSql, sql, theComment); + return myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(false, false); } - /** - * Two regular search params - Should use HFJ_RESOURCE as root - */ - @Test - public void testTwoRegularSearchParams() { - - myCaptureQueriesListener.clear(); - SearchParameterMap map = SearchParameterMap.newSynchronous() - .add(Patient.SP_NAME, new StringParam("FOO")) - .add(Patient.SP_GENDER, new TokenParam("a", "b")); - myPatientDao.search(map); - assertEquals(1, myCaptureQueriesListener.countSelectQueries()); - String sql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(false, false); - assertEquals("SELECT t1.RES_ID FROM HFJ_RESOURCE t1 INNER JOIN HFJ_SPIDX_STRING t0 ON (t1.RES_ID = t0.RES_ID) INNER JOIN HFJ_SPIDX_TOKEN t2 ON (t1.RES_ID = t2.RES_ID) WHERE (((t0.HASH_NORM_PREFIX = ?) AND (t0.SP_VALUE_NORMALIZED LIKE ?)) AND (t2.HASH_SYS_AND_VALUE = ?))", sql); - } @Test public void testSearchByProfile_VersionedMode() { @@ -129,14 +119,14 @@ public class FhirResourceDaoR4SearchSqlTest extends BaseJpaR4Test { String code = "http://" + UUID.randomUUID(); Patient p = new Patient(); p.getMeta().addProfile(code); - IIdType id = myPatientDao.create(p).getId().toUnqualifiedVersionless(); + IIdType id = myPatientDao.create(p, mySrd).getId().toUnqualifiedVersionless(); myMemoryCacheService.invalidateAllCaches(); // Search myCaptureQueriesListener.clear(); SearchParameterMap map = SearchParameterMap.newSynchronous() .add(Constants.PARAM_PROFILE, new TokenParam(code)); - IBundleProvider outcome = myPatientDao.search(map); + IBundleProvider outcome = myPatientDao.search(map, mySrd); assertEquals(3, myCaptureQueriesListener.countSelectQueries()); // Query 1 - Find resources: Make sure we search for tag type+system+code always String sql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(false, false); @@ -158,7 +148,6 @@ public class FhirResourceDaoR4SearchSqlTest extends BaseJpaR4Test { boolean reindexParamCache = myStorageSettings.isMarkResourcesForReindexingUponSearchParameterChange(); myStorageSettings.setMarkResourcesForReindexingUponSearchParameterChange(false); -// SearchParameter searchParameter = FhirResourceDaoR4TagsTest.createSearchParamForInlineResourceProfile(); SearchParameter searchParameter = FhirResourceDaoR4TagsInlineTest.createSearchParameterForInlineProfile(); ourLog.debug("SearchParam:\n{}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(searchParameter)); mySearchParameterDao.update(searchParameter, mySrd); @@ -168,14 +157,14 @@ public class FhirResourceDaoR4SearchSqlTest extends BaseJpaR4Test { String code = "http://" + UUID.randomUUID(); Patient p = new Patient(); p.getMeta().addProfile(code); - IIdType id = myPatientDao.create(p).getId().toUnqualifiedVersionless(); + IIdType id = myPatientDao.create(p, mySrd).getId().toUnqualifiedVersionless(); myMemoryCacheService.invalidateAllCaches(); // Search myCaptureQueriesListener.clear(); SearchParameterMap map = SearchParameterMap.newSynchronous() .add(Constants.PARAM_PROFILE, new UriParam(code)); - IBundleProvider outcome = myPatientDao.search(map); + IBundleProvider outcome = myPatientDao.search(map, mySrd); assertEquals(2, myCaptureQueriesListener.countSelectQueries()); // Query 1 - Find resources: Just a standard token search in this mode String sql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(false, false);