diff --git a/engine/src/androidTest/java/com/google/android/fhir/db/impl/DatabaseImplTest.kt b/engine/src/androidTest/java/com/google/android/fhir/db/impl/DatabaseImplTest.kt index 8dc19cc8ae..12f728b5c4 100644 --- a/engine/src/androidTest/java/com/google/android/fhir/db/impl/DatabaseImplTest.kt +++ b/engine/src/androidTest/java/com/google/android/fhir/db/impl/DatabaseImplTest.kt @@ -24,6 +24,7 @@ import com.google.android.fhir.db.ResourceNotFoundException import com.google.android.fhir.db.impl.entities.LocalChangeEntity import com.google.android.fhir.logicalId import com.google.android.fhir.resource.TestingUtils +import com.google.android.fhir.search.Operation import com.google.android.fhir.search.Order import com.google.android.fhir.search.Search import com.google.android.fhir.search.StringFilterModifier @@ -395,7 +396,7 @@ class DatabaseImplTest { database.insert(patient) val result = database.search( - Search(ResourceType.Patient).apply { filter(Patient.GIVEN) { value = "eve" } }.getQuery() + Search(ResourceType.Patient).apply { filter(Patient.GIVEN, { value = "eve" }) }.getQuery() ) assertThat(result.single().id).isEqualTo("Patient/${patient.id}") @@ -411,7 +412,7 @@ class DatabaseImplTest { database.insert(patient) val result = database.search( - Search(ResourceType.Patient).apply { filter(Patient.GIVEN) { value = "eve" } }.getQuery() + Search(ResourceType.Patient).apply { filter(Patient.GIVEN, { value = "eve" }) }.getQuery() ) assertThat(result).isEmpty() @@ -429,10 +430,13 @@ class DatabaseImplTest { database.search( Search(ResourceType.Patient) .apply { - filter(Patient.GIVEN) { - value = "Eve" - modifier = StringFilterModifier.MATCHES_EXACTLY - } + filter( + Patient.GIVEN, + { + value = "Eve" + modifier = StringFilterModifier.MATCHES_EXACTLY + } + ) } .getQuery() ) @@ -452,10 +456,13 @@ class DatabaseImplTest { database.search( Search(ResourceType.Patient) .apply { - filter(Patient.GIVEN) { - value = "Eve" - modifier = StringFilterModifier.MATCHES_EXACTLY - } + filter( + Patient.GIVEN, + { + value = "Eve" + modifier = StringFilterModifier.MATCHES_EXACTLY + } + ) } .getQuery() ) @@ -476,10 +483,13 @@ class DatabaseImplTest { database.search( Search(ResourceType.Patient) .apply { - filter(Patient.GIVEN) { - value = "Eve" - modifier = StringFilterModifier.CONTAINS - } + filter( + Patient.GIVEN, + { + value = "Eve" + modifier = StringFilterModifier.CONTAINS + } + ) } .getQuery() ) @@ -499,10 +509,13 @@ class DatabaseImplTest { database.search( Search(ResourceType.Patient) .apply { - filter(Patient.GIVEN) { - value = "eve" - modifier = StringFilterModifier.CONTAINS - } + filter( + Patient.GIVEN, + { + value = "eve" + modifier = StringFilterModifier.CONTAINS + } + ) } .getQuery() ) @@ -525,10 +538,13 @@ class DatabaseImplTest { database.search( Search(ResourceType.RiskAssessment) .apply { - filter(RiskAssessment.PROBABILITY) { - prefix = ParamPrefixEnum.EQUAL - value = BigDecimal("100") - } + filter( + RiskAssessment.PROBABILITY, + { + prefix = ParamPrefixEnum.EQUAL + value = BigDecimal("100") + } + ) } .getQuery() ) @@ -551,10 +567,13 @@ class DatabaseImplTest { database.search( Search(ResourceType.RiskAssessment) .apply { - filter(RiskAssessment.PROBABILITY) { - prefix = ParamPrefixEnum.EQUAL - value = BigDecimal("100") - } + filter( + RiskAssessment.PROBABILITY, + { + prefix = ParamPrefixEnum.EQUAL + value = BigDecimal("100") + } + ) } .getQuery() ) @@ -577,10 +596,13 @@ class DatabaseImplTest { database.search( Search(ResourceType.RiskAssessment) .apply { - filter(RiskAssessment.PROBABILITY) { - prefix = ParamPrefixEnum.NOT_EQUAL - value = BigDecimal("100") - } + filter( + RiskAssessment.PROBABILITY, + { + prefix = ParamPrefixEnum.NOT_EQUAL + value = BigDecimal("100") + } + ) } .getQuery() ) @@ -602,10 +624,13 @@ class DatabaseImplTest { database.search( Search(ResourceType.RiskAssessment) .apply { - filter(RiskAssessment.PROBABILITY) { - prefix = ParamPrefixEnum.NOT_EQUAL - value = BigDecimal("100") - } + filter( + RiskAssessment.PROBABILITY, + { + prefix = ParamPrefixEnum.NOT_EQUAL + value = BigDecimal("100") + } + ) } .getQuery() ) @@ -628,10 +653,13 @@ class DatabaseImplTest { database.search( Search(ResourceType.RiskAssessment) .apply { - filter(RiskAssessment.PROBABILITY) { - prefix = ParamPrefixEnum.GREATERTHAN - value = BigDecimal("99.5") - } + filter( + RiskAssessment.PROBABILITY, + { + prefix = ParamPrefixEnum.GREATERTHAN + value = BigDecimal("99.5") + } + ) } .getQuery() ) @@ -654,10 +682,13 @@ class DatabaseImplTest { database.search( Search(ResourceType.RiskAssessment) .apply { - filter(RiskAssessment.PROBABILITY) { - prefix = ParamPrefixEnum.GREATERTHAN - value = BigDecimal("99.5") - } + filter( + RiskAssessment.PROBABILITY, + { + prefix = ParamPrefixEnum.GREATERTHAN + value = BigDecimal("99.5") + } + ) } .getQuery() ) @@ -680,10 +711,13 @@ class DatabaseImplTest { database.search( Search(ResourceType.RiskAssessment) .apply { - filter(RiskAssessment.PROBABILITY) { - prefix = ParamPrefixEnum.GREATERTHAN_OR_EQUALS - value = BigDecimal("99.5") - } + filter( + RiskAssessment.PROBABILITY, + { + prefix = ParamPrefixEnum.GREATERTHAN_OR_EQUALS + value = BigDecimal("99.5") + } + ) } .getQuery() ) @@ -706,10 +740,13 @@ class DatabaseImplTest { database.search( Search(ResourceType.RiskAssessment) .apply { - filter(RiskAssessment.PROBABILITY) { - prefix = ParamPrefixEnum.GREATERTHAN_OR_EQUALS - value = BigDecimal("99.5") - } + filter( + RiskAssessment.PROBABILITY, + { + prefix = ParamPrefixEnum.GREATERTHAN_OR_EQUALS + value = BigDecimal("99.5") + } + ) } .getQuery() ) @@ -732,10 +769,13 @@ class DatabaseImplTest { database.search( Search(ResourceType.RiskAssessment) .apply { - filter(RiskAssessment.PROBABILITY) { - prefix = ParamPrefixEnum.LESSTHAN - value = BigDecimal("99.5") - } + filter( + RiskAssessment.PROBABILITY, + { + prefix = ParamPrefixEnum.LESSTHAN + value = BigDecimal("99.5") + } + ) } .getQuery() ) @@ -758,10 +798,13 @@ class DatabaseImplTest { database.search( Search(ResourceType.RiskAssessment) .apply { - filter(RiskAssessment.PROBABILITY) { - prefix = ParamPrefixEnum.LESSTHAN - value = BigDecimal("99.5") - } + filter( + RiskAssessment.PROBABILITY, + { + prefix = ParamPrefixEnum.LESSTHAN + value = BigDecimal("99.5") + } + ) } .getQuery() ) @@ -784,10 +827,13 @@ class DatabaseImplTest { database.search( Search(ResourceType.RiskAssessment) .apply { - filter(RiskAssessment.PROBABILITY) { - prefix = ParamPrefixEnum.LESSTHAN_OR_EQUALS - value = BigDecimal("99.5") - } + filter( + RiskAssessment.PROBABILITY, + { + prefix = ParamPrefixEnum.LESSTHAN_OR_EQUALS + value = BigDecimal("99.5") + } + ) } .getQuery() ) @@ -809,10 +855,13 @@ class DatabaseImplTest { database.search( Search(ResourceType.RiskAssessment) .apply { - filter(RiskAssessment.PROBABILITY) { - prefix = ParamPrefixEnum.LESSTHAN_OR_EQUALS - value = BigDecimal("99.5") - } + filter( + RiskAssessment.PROBABILITY, + { + prefix = ParamPrefixEnum.LESSTHAN_OR_EQUALS + value = BigDecimal("99.5") + } + ) } .getQuery() ) @@ -835,10 +884,13 @@ class DatabaseImplTest { database.search( Search(ResourceType.RiskAssessment) .apply { - filter(RiskAssessment.PROBABILITY) { - prefix = ParamPrefixEnum.ENDS_BEFORE - value = BigDecimal("99.5") - } + filter( + RiskAssessment.PROBABILITY, + { + prefix = ParamPrefixEnum.ENDS_BEFORE + value = BigDecimal("99.5") + } + ) } .getQuery() ) @@ -861,10 +913,13 @@ class DatabaseImplTest { database.search( Search(ResourceType.RiskAssessment) .apply { - filter(RiskAssessment.PROBABILITY) { - prefix = ParamPrefixEnum.ENDS_BEFORE - value = BigDecimal("99.5") - } + filter( + RiskAssessment.PROBABILITY, + { + prefix = ParamPrefixEnum.ENDS_BEFORE + value = BigDecimal("99.5") + } + ) } .getQuery() ) @@ -887,10 +942,13 @@ class DatabaseImplTest { database.search( Search(ResourceType.RiskAssessment) .apply { - filter(RiskAssessment.PROBABILITY) { - prefix = ParamPrefixEnum.STARTS_AFTER - value = BigDecimal("99.5") - } + filter( + RiskAssessment.PROBABILITY, + { + prefix = ParamPrefixEnum.STARTS_AFTER + value = BigDecimal("99.5") + } + ) } .getQuery() ) @@ -913,10 +971,13 @@ class DatabaseImplTest { database.search( Search(ResourceType.RiskAssessment) .apply { - filter(RiskAssessment.PROBABILITY) { - prefix = ParamPrefixEnum.STARTS_AFTER - value = BigDecimal("99.5") - } + filter( + RiskAssessment.PROBABILITY, + { + prefix = ParamPrefixEnum.STARTS_AFTER + value = BigDecimal("99.5") + } + ) } .getQuery() ) @@ -939,10 +1000,13 @@ class DatabaseImplTest { database.search( Search(ResourceType.RiskAssessment) .apply { - filter(RiskAssessment.PROBABILITY) { - prefix = ParamPrefixEnum.APPROXIMATE - value = BigDecimal("100") - } + filter( + RiskAssessment.PROBABILITY, + { + prefix = ParamPrefixEnum.APPROXIMATE + value = BigDecimal("100") + } + ) } .getQuery() ) @@ -965,10 +1029,13 @@ class DatabaseImplTest { database.search( Search(ResourceType.RiskAssessment) .apply { - filter(RiskAssessment.PROBABILITY) { - prefix = ParamPrefixEnum.APPROXIMATE - value = BigDecimal("100") - } + filter( + RiskAssessment.PROBABILITY, + { + prefix = ParamPrefixEnum.APPROXIMATE + value = BigDecimal("100") + } + ) } .getQuery() ) @@ -988,7 +1055,13 @@ class DatabaseImplTest { database.search( Search(ResourceType.Patient) .apply { - filter(Patient.DEATH_DATE, DateTimeType("2013-03-14"), ParamPrefixEnum.STARTS_AFTER) + filter( + Patient.DEATH_DATE, + { + value = of(DateTimeType("2013-03-14")) + prefix = ParamPrefixEnum.STARTS_AFTER + } + ) } .getQuery() ) @@ -1007,7 +1080,13 @@ class DatabaseImplTest { database.search( Search(ResourceType.Patient) .apply { - filter(Patient.DEATH_DATE, DateTimeType("2013-03-14"), ParamPrefixEnum.STARTS_AFTER) + filter( + Patient.DEATH_DATE, + { + value = of(DateTimeType("2013-03-14")) + prefix = ParamPrefixEnum.STARTS_AFTER + } + ) } .getQuery() ) @@ -1026,7 +1105,13 @@ class DatabaseImplTest { database.search( Search(ResourceType.Patient) .apply { - filter(Patient.DEATH_DATE, DateTimeType("2013-03-14"), ParamPrefixEnum.ENDS_BEFORE) + filter( + Patient.DEATH_DATE, + { + value = of(DateTimeType("2013-03-14")) + prefix = ParamPrefixEnum.ENDS_BEFORE + } + ) } .getQuery() ) @@ -1045,7 +1130,13 @@ class DatabaseImplTest { database.search( Search(ResourceType.Patient) .apply { - filter(Patient.DEATH_DATE, DateTimeType("2013-03-14"), ParamPrefixEnum.ENDS_BEFORE) + filter( + Patient.DEATH_DATE, + { + value = of(DateTimeType("2013-03-14")) + prefix = ParamPrefixEnum.ENDS_BEFORE + } + ) } .getQuery() ) @@ -1064,7 +1155,13 @@ class DatabaseImplTest { database.search( Search(ResourceType.Patient) .apply { - filter(Patient.DEATH_DATE, DateTimeType("2013-03-14"), ParamPrefixEnum.NOT_EQUAL) + filter( + Patient.DEATH_DATE, + { + value = of(DateTimeType("2013-03-14")) + prefix = ParamPrefixEnum.NOT_EQUAL + } + ) } .getQuery() ) @@ -1083,7 +1180,13 @@ class DatabaseImplTest { database.search( Search(ResourceType.Patient) .apply { - filter(Patient.DEATH_DATE, DateTimeType("2013-03-14"), ParamPrefixEnum.NOT_EQUAL) + filter( + Patient.DEATH_DATE, + { + value = of(DateTimeType("2013-03-14")) + prefix = ParamPrefixEnum.NOT_EQUAL + } + ) } .getQuery() ) @@ -1101,7 +1204,15 @@ class DatabaseImplTest { val result = database.search( Search(ResourceType.Patient) - .apply { filter(Patient.DEATH_DATE, DateTimeType("2013-03-14"), ParamPrefixEnum.EQUAL) } + .apply { + filter( + Patient.DEATH_DATE, + { + value = of(DateTimeType("2013-03-14")) + prefix = ParamPrefixEnum.EQUAL + } + ) + } .getQuery() ) assertThat(result.single().id).isEqualTo("Patient/1") @@ -1118,7 +1229,15 @@ class DatabaseImplTest { val result = database.search( Search(ResourceType.Patient) - .apply { filter(Patient.DEATH_DATE, DateTimeType("2013-03-14"), ParamPrefixEnum.EQUAL) } + .apply { + filter( + Patient.DEATH_DATE, + { + value = of(DateTimeType("2013-03-14")) + prefix = ParamPrefixEnum.EQUAL + } + ) + } .getQuery() ) assertThat(result).isEmpty() @@ -1136,7 +1255,13 @@ class DatabaseImplTest { database.search( Search(ResourceType.Patient) .apply { - filter(Patient.DEATH_DATE, DateTimeType("2013-03-14"), ParamPrefixEnum.GREATERTHAN) + filter( + Patient.DEATH_DATE, + { + value = of(DateTimeType("2013-03-14")) + prefix = ParamPrefixEnum.GREATERTHAN + } + ) } .getQuery() ) @@ -1155,7 +1280,13 @@ class DatabaseImplTest { database.search( Search(ResourceType.Patient) .apply { - filter(Patient.DEATH_DATE, DateTimeType("2013-03-14"), ParamPrefixEnum.GREATERTHAN) + filter( + Patient.DEATH_DATE, + { + value = of(DateTimeType("2013-03-14")) + prefix = ParamPrefixEnum.GREATERTHAN + } + ) } .getQuery() ) @@ -1176,8 +1307,10 @@ class DatabaseImplTest { .apply { filter( Patient.DEATH_DATE, - DateTimeType("2013-03-14"), - ParamPrefixEnum.GREATERTHAN_OR_EQUALS + { + value = of(DateTimeType("2013-03-14")) + prefix = ParamPrefixEnum.GREATERTHAN_OR_EQUALS + } ) } .getQuery() @@ -1198,8 +1331,10 @@ class DatabaseImplTest { .apply { filter( Patient.DEATH_DATE, - DateTimeType("2013-03-14"), - ParamPrefixEnum.GREATERTHAN_OR_EQUALS + { + value = of(DateTimeType("2013-03-14")) + prefix = ParamPrefixEnum.GREATERTHAN_OR_EQUALS + } ) } .getQuery() @@ -1219,7 +1354,13 @@ class DatabaseImplTest { database.search( Search(ResourceType.Patient) .apply { - filter(Patient.DEATH_DATE, DateTimeType("2013-03-14"), ParamPrefixEnum.LESSTHAN) + filter( + Patient.DEATH_DATE, + { + value = of(DateTimeType("2013-03-14")) + prefix = ParamPrefixEnum.LESSTHAN + } + ) } .getQuery() ) @@ -1238,7 +1379,13 @@ class DatabaseImplTest { database.search( Search(ResourceType.Patient) .apply { - filter(Patient.DEATH_DATE, DateTimeType("2013-03-14"), ParamPrefixEnum.LESSTHAN) + filter( + Patient.DEATH_DATE, + { + value = of(DateTimeType("2013-03-14")) + prefix = ParamPrefixEnum.LESSTHAN + } + ) } .getQuery() ) @@ -1259,8 +1406,10 @@ class DatabaseImplTest { .apply { filter( Patient.DEATH_DATE, - DateTimeType("2013-03-14"), - ParamPrefixEnum.LESSTHAN_OR_EQUALS + { + value = of(DateTimeType("2013-03-14")) + prefix = ParamPrefixEnum.LESSTHAN_OR_EQUALS + } ) } .getQuery() @@ -1282,8 +1431,10 @@ class DatabaseImplTest { .apply { filter( Patient.DEATH_DATE, - DateTimeType("2013-03-14"), - ParamPrefixEnum.LESSTHAN_OR_EQUALS + { + value = of(DateTimeType("2013-03-14")) + prefix = ParamPrefixEnum.LESSTHAN_OR_EQUALS + } ) } .getQuery() @@ -1308,12 +1459,15 @@ class DatabaseImplTest { database.search( Search(ResourceType.Observation) .apply { - filter(Observation.VALUE_QUANTITY) { - prefix = ParamPrefixEnum.EQUAL - value = BigDecimal("5.403") - system = "http://unitsofmeasure.org" - unit = "g" - } + filter( + Observation.VALUE_QUANTITY, + { + prefix = ParamPrefixEnum.EQUAL + value = BigDecimal("5.403") + system = "http://unitsofmeasure.org" + unit = "g" + } + ) } .getQuery() ) @@ -1337,12 +1491,15 @@ class DatabaseImplTest { database.search( Search(ResourceType.Observation) .apply { - filter(Observation.VALUE_QUANTITY) { - prefix = ParamPrefixEnum.NOT_EQUAL - value = BigDecimal("5.403") - system = "http://unitsofmeasure.org" - unit = "g" - } + filter( + Observation.VALUE_QUANTITY, + { + prefix = ParamPrefixEnum.NOT_EQUAL + value = BigDecimal("5.403") + system = "http://unitsofmeasure.org" + unit = "g" + } + ) } .getQuery() ) @@ -1366,12 +1523,15 @@ class DatabaseImplTest { database.search( Search(ResourceType.Observation) .apply { - filter(Observation.VALUE_QUANTITY) { - prefix = ParamPrefixEnum.LESSTHAN - value = BigDecimal("5.403") - system = "http://unitsofmeasure.org" - unit = "g" - } + filter( + Observation.VALUE_QUANTITY, + { + prefix = ParamPrefixEnum.LESSTHAN + value = BigDecimal("5.403") + system = "http://unitsofmeasure.org" + unit = "g" + } + ) } .getQuery() ) @@ -1395,12 +1555,15 @@ class DatabaseImplTest { database.search( Search(ResourceType.Observation) .apply { - filter(Observation.VALUE_QUANTITY) { - prefix = ParamPrefixEnum.LESSTHAN - value = BigDecimal("5.403") - system = "http://unitsofmeasure.org" - unit = "g" - } + filter( + Observation.VALUE_QUANTITY, + { + prefix = ParamPrefixEnum.LESSTHAN + value = BigDecimal("5.403") + system = "http://unitsofmeasure.org" + unit = "g" + } + ) } .getQuery() ) @@ -1424,12 +1587,15 @@ class DatabaseImplTest { database.search( Search(ResourceType.Observation) .apply { - filter(Observation.VALUE_QUANTITY) { - prefix = ParamPrefixEnum.GREATERTHAN - value = BigDecimal("5.403") - system = "http://unitsofmeasure.org" - unit = "g" - } + filter( + Observation.VALUE_QUANTITY, + { + prefix = ParamPrefixEnum.GREATERTHAN + value = BigDecimal("5.403") + system = "http://unitsofmeasure.org" + unit = "g" + } + ) } .getQuery() ) @@ -1453,12 +1619,15 @@ class DatabaseImplTest { database.search( Search(ResourceType.Observation) .apply { - filter(Observation.VALUE_QUANTITY) { - prefix = ParamPrefixEnum.GREATERTHAN - value = BigDecimal("5.403") - system = "http://unitsofmeasure.org" - unit = "g" - } + filter( + Observation.VALUE_QUANTITY, + { + prefix = ParamPrefixEnum.GREATERTHAN + value = BigDecimal("5.403") + system = "http://unitsofmeasure.org" + unit = "g" + } + ) } .getQuery() ) @@ -1482,12 +1651,15 @@ class DatabaseImplTest { database.search( Search(ResourceType.Observation) .apply { - filter(Observation.VALUE_QUANTITY) { - prefix = ParamPrefixEnum.LESSTHAN_OR_EQUALS - value = BigDecimal("5.403") - system = "http://unitsofmeasure.org" - unit = "g" - } + filter( + Observation.VALUE_QUANTITY, + { + prefix = ParamPrefixEnum.LESSTHAN_OR_EQUALS + value = BigDecimal("5.403") + system = "http://unitsofmeasure.org" + unit = "g" + } + ) } .getQuery() ) @@ -1511,12 +1683,15 @@ class DatabaseImplTest { database.search( Search(ResourceType.Observation) .apply { - filter(Observation.VALUE_QUANTITY) { - prefix = ParamPrefixEnum.LESSTHAN_OR_EQUALS - value = BigDecimal("5.403") - system = "http://unitsofmeasure.org" - unit = "g" - } + filter( + Observation.VALUE_QUANTITY, + { + prefix = ParamPrefixEnum.LESSTHAN_OR_EQUALS + value = BigDecimal("5.403") + system = "http://unitsofmeasure.org" + unit = "g" + } + ) } .getQuery() ) @@ -1540,12 +1715,15 @@ class DatabaseImplTest { database.search( Search(ResourceType.Observation) .apply { - filter(Observation.VALUE_QUANTITY) { - prefix = ParamPrefixEnum.GREATERTHAN_OR_EQUALS - value = BigDecimal("5.403") - system = "http://unitsofmeasure.org" - unit = "g" - } + filter( + Observation.VALUE_QUANTITY, + { + prefix = ParamPrefixEnum.GREATERTHAN_OR_EQUALS + value = BigDecimal("5.403") + system = "http://unitsofmeasure.org" + unit = "g" + } + ) } .getQuery() ) @@ -1569,12 +1747,15 @@ class DatabaseImplTest { database.search( Search(ResourceType.Observation) .apply { - filter(Observation.VALUE_QUANTITY) { - prefix = ParamPrefixEnum.GREATERTHAN_OR_EQUALS - value = BigDecimal("5.403") - system = "http://unitsofmeasure.org" - unit = "g" - } + filter( + Observation.VALUE_QUANTITY, + { + prefix = ParamPrefixEnum.GREATERTHAN_OR_EQUALS + value = BigDecimal("5.403") + system = "http://unitsofmeasure.org" + unit = "g" + } + ) } .getQuery() ) @@ -1598,12 +1779,15 @@ class DatabaseImplTest { database.search( Search(ResourceType.Observation) .apply { - filter(Observation.VALUE_QUANTITY) { - prefix = ParamPrefixEnum.STARTS_AFTER - value = BigDecimal("5.403") - system = "http://unitsofmeasure.org" - unit = "g" - } + filter( + Observation.VALUE_QUANTITY, + { + prefix = ParamPrefixEnum.STARTS_AFTER + value = BigDecimal("5.403") + system = "http://unitsofmeasure.org" + unit = "g" + } + ) } .getQuery() ) @@ -1627,12 +1811,15 @@ class DatabaseImplTest { database.search( Search(ResourceType.Observation) .apply { - filter(Observation.VALUE_QUANTITY) { - prefix = ParamPrefixEnum.STARTS_AFTER - value = BigDecimal("5.403") - system = "http://unitsofmeasure.org" - unit = "g" - } + filter( + Observation.VALUE_QUANTITY, + { + prefix = ParamPrefixEnum.STARTS_AFTER + value = BigDecimal("5.403") + system = "http://unitsofmeasure.org" + unit = "g" + } + ) } .getQuery() ) @@ -1656,12 +1843,15 @@ class DatabaseImplTest { database.search( Search(ResourceType.Observation) .apply { - filter(Observation.VALUE_QUANTITY) { - prefix = ParamPrefixEnum.ENDS_BEFORE - value = BigDecimal("5.403") - system = "http://unitsofmeasure.org" - unit = "g" - } + filter( + Observation.VALUE_QUANTITY, + { + prefix = ParamPrefixEnum.ENDS_BEFORE + value = BigDecimal("5.403") + system = "http://unitsofmeasure.org" + unit = "g" + } + ) } .getQuery() ) @@ -1685,12 +1875,15 @@ class DatabaseImplTest { database.search( Search(ResourceType.Observation) .apply { - filter(Observation.VALUE_QUANTITY) { - prefix = ParamPrefixEnum.ENDS_BEFORE - value = BigDecimal("5.403") - system = "http://unitsofmeasure.org" - unit = "g" - } + filter( + Observation.VALUE_QUANTITY, + { + prefix = ParamPrefixEnum.ENDS_BEFORE + value = BigDecimal("5.403") + system = "http://unitsofmeasure.org" + unit = "g" + } + ) } .getQuery() ) @@ -1714,12 +1907,15 @@ class DatabaseImplTest { database.search( Search(ResourceType.Observation) .apply { - filter(Observation.VALUE_QUANTITY) { - prefix = ParamPrefixEnum.EQUAL - value = BigDecimal("5403") - system = "http://unitsofmeasure.org" - unit = "mg" - } + filter( + Observation.VALUE_QUANTITY, + { + prefix = ParamPrefixEnum.EQUAL + value = BigDecimal("5403") + system = "http://unitsofmeasure.org" + unit = "mg" + } + ) } .getQuery() ) @@ -1773,24 +1969,34 @@ class DatabaseImplTest { has(Immunization.PATIENT) { filter( Immunization.VACCINE_CODE, - Coding( - "http://hl7.org/fhir/sid/cvx", - "140", - "Influenza, seasonal, injectable, preservative free" - ) + { + value = + of( + Coding( + "http://hl7.org/fhir/sid/cvx", + "140", + "Influenza, seasonal, injectable, preservative free" + ) + ) + } ) // Follow Immunization.ImmunizationStatus filter( Immunization.STATUS, - Coding("http://hl7.org/fhir/event-status", "completed", "Body Weight") + { + value = of(Coding("http://hl7.org/fhir/event-status", "completed", "Body Weight")) + } ) } - filter(Patient.ADDRESS_COUNTRY) { - modifier = StringFilterModifier.MATCHES_EXACTLY - value = "IN" - } + filter( + Patient.ADDRESS_COUNTRY, + { + modifier = StringFilterModifier.MATCHES_EXACTLY + value = "IN" + } + ) } .getQuery() ) @@ -1825,7 +2031,12 @@ class DatabaseImplTest { has(CarePlan.SUBJECT) { filter( CarePlan.CATEGORY, - Coding("http://snomed.info/sct", "698360004", "Diabetes self management plan") + { + value = + of( + Coding("http://snomed.info/sct", "698360004", "Diabetes self management plan") + ) + } ) } } @@ -1844,68 +2055,52 @@ class DatabaseImplTest { CodeableConcept(Coding("http://snomed.info/sct", "44054006", "Diabetes")) val hyperTensionCodeableConcept = CodeableConcept(Coding("http://snomed.info/sct", "827069000", "Hypertension stage 1")) - val resources = mutableListOf() - Practitioner().apply { id = "practitioner-001" }.also { resources.add(it) } - Practitioner().apply { id = "practitioner-002" }.also { resources.add(it) } - Patient() - .apply { - gender = Enumerations.AdministrativeGender.MALE - id = "patient-001" - this.addGeneralPractitioner(Reference("Practitioner/practitioner-001")) - this.addGeneralPractitioner(Reference("Practitioner/practitioner-002")) - } - .also { resources.add(it) } - Condition() - .apply { - subject = Reference("Patient/patient-001") - id = "condition-001" - code = diabetesCodeableConcept - } - .also { resources.add(it) } - Condition() - .apply { - subject = Reference("Patient/patient-001") - id = "condition-002" - code = hyperTensionCodeableConcept - } - .also { resources.add(it) } - - Patient() - .apply { - gender = Enumerations.AdministrativeGender.MALE - id = "patient-002" - } - .also { resources.add(it) } - Condition() - .apply { - subject = Reference("Patient/patient-002") - id = "condition-003" - code = hyperTensionCodeableConcept - } - .also { resources.add(it) } - Condition() - .apply { - subject = Reference("Patient/patient-002") - id = "condition-004" - code = diabetesCodeableConcept - } - .also { resources.add(it) } - - Practitioner().apply { id = "practitioner-003" }.also { resources.add(it) } - Patient() - .apply { - gender = Enumerations.AdministrativeGender.MALE - id = "patient-003" - this.addGeneralPractitioner(Reference("Practitioner/practitioner-00")) - } - .also { resources.add(it) } - Condition() - .apply { - subject = Reference("Patient/patient-003") - id = "condition-005" - code = diabetesCodeableConcept - } - .also { resources.add(it) } + val resources = + listOf( + Practitioner().apply { id = "practitioner-001" }, + Practitioner().apply { id = "practitioner-002" }, + Patient().apply { + gender = Enumerations.AdministrativeGender.MALE + id = "patient-001" + this.addGeneralPractitioner(Reference("Practitioner/practitioner-001")) + this.addGeneralPractitioner(Reference("Practitioner/practitioner-002")) + }, + Condition().apply { + subject = Reference("Patient/patient-001") + id = "condition-001" + code = diabetesCodeableConcept + }, + Condition().apply { + subject = Reference("Patient/patient-001") + id = "condition-002" + code = hyperTensionCodeableConcept + }, + Patient().apply { + gender = Enumerations.AdministrativeGender.MALE + id = "patient-002" + }, + Condition().apply { + subject = Reference("Patient/patient-002") + id = "condition-003" + code = hyperTensionCodeableConcept + }, + Condition().apply { + subject = Reference("Patient/patient-002") + id = "condition-004" + code = diabetesCodeableConcept + }, + Practitioner().apply { id = "practitioner-003" }, + Patient().apply { + gender = Enumerations.AdministrativeGender.MALE + id = "patient-003" + this.addGeneralPractitioner(Reference("Practitioner/practitioner-00")) + }, + Condition().apply { + subject = Reference("Patient/patient-003") + id = "condition-005" + code = diabetesCodeableConcept + } + ) database.insert(*resources.toTypedArray()) val result = @@ -1914,14 +2109,20 @@ class DatabaseImplTest { .apply { has(Patient.GENERAL_PRACTITIONER) { has(Condition.SUBJECT) { - filter(Condition.CODE, Coding("http://snomed.info/sct", "44054006", "Diabetes")) + filter( + Condition.CODE, + { value = of(Coding("http://snomed.info/sct", "44054006", "Diabetes")) } + ) } } has(Patient.GENERAL_PRACTITIONER) { has(Condition.SUBJECT) { filter( Condition.CODE, - Coding("http://snomed.info/sct", "827069000", "Hypertension stage 1") + { + value = + of(Coding("http://snomed.info/sct", "827069000", "Hypertension stage 1")) + } ) } } @@ -1934,6 +2135,254 @@ class DatabaseImplTest { .inOrder() } + @Test + fun search_filter_param_values_disjunction_covid_immunization_records() = runBlocking { + val resources = + listOf( + Immunization().apply { + id = "immunization-1" + vaccineCode = + CodeableConcept( + Coding("http://id.who.int/icd11/mms", "XM1NL1", "COVID-19 vaccine, inactivated virus") + ) + status = Immunization.ImmunizationStatus.COMPLETED + }, + Immunization().apply { + id = "immunization-2" + vaccineCode = + CodeableConcept( + Coding( + "http://id.who.int/icd11/mms", + "XM5DF6", + "COVID-19 vaccine, live attenuated virus" + ) + ) + status = Immunization.ImmunizationStatus.COMPLETED + }, + Immunization().apply { + id = "immunization-3" + vaccineCode = + CodeableConcept( + Coding("http://id.who.int/icd11/mms", "XM6AT1", "COVID-19 vaccine, DNA based") + ) + status = Immunization.ImmunizationStatus.COMPLETED + }, + Immunization().apply { + id = "immunization-4" + vaccineCode = + CodeableConcept( + Coding( + "http://hl7.org/fhir/sid/cvx", + "140", + "Influenza, seasonal, injectable, preservative free" + ) + ) + status = Immunization.ImmunizationStatus.COMPLETED + } + ) + + database.insert(*resources.toTypedArray()) + + val result = + database.search( + Search(ResourceType.Immunization) + .apply { + filter( + Immunization.VACCINE_CODE, + { + value = + of( + Coding( + "http://id.who.int/icd11/mms", + "XM1NL1", + "COVID-19 vaccine, inactivated virus" + ) + ) + }, + { + value = + of( + Coding( + "http://id.who.int/icd11/mms", + "XM5DF6", + "COVID-19 vaccine, inactivated virus" + ) + ) + }, + operation = Operation.OR + ) + } + .getQuery() + ) + + assertThat(result.map { it.vaccineCode.codingFirstRep.code }) + .containsExactly("XM1NL1", "XM5DF6") + .inOrder() + } + + @Test + fun test_search_multiple_param_disjunction_covid_immunization_records() = runBlocking { + val resources = + listOf( + Immunization().apply { + id = "immunization-1" + vaccineCode = + CodeableConcept( + Coding("http://id.who.int/icd11/mms", "XM1NL1", "COVID-19 vaccine, inactivated virus") + ) + status = Immunization.ImmunizationStatus.COMPLETED + }, + Immunization().apply { + id = "immunization-2" + vaccineCode = + CodeableConcept( + Coding( + "http://id.who.int/icd11/mms", + "XM5DF6", + "COVID-19 vaccine, live attenuated virus" + ) + ) + status = Immunization.ImmunizationStatus.COMPLETED + }, + Immunization().apply { + id = "immunization-3" + vaccineCode = + CodeableConcept( + Coding("http://id.who.int/icd11/mms", "XM6AT1", "COVID-19 vaccine, DNA based") + ) + status = Immunization.ImmunizationStatus.COMPLETED + }, + Immunization().apply { + id = "immunization-4" + vaccineCode = + CodeableConcept( + Coding( + "http://hl7.org/fhir/sid/cvx", + "140", + "Influenza, seasonal, injectable, preservative free" + ) + ) + status = Immunization.ImmunizationStatus.COMPLETED + } + ) + + database.insert(*resources.toTypedArray()) + + val result = + database.search( + Search(ResourceType.Immunization) + .apply { + filter( + Immunization.VACCINE_CODE, + { value = of(Coding("http://id.who.int/icd11/mms", "XM1NL1", "")) } + ) + + filter( + Immunization.VACCINE_CODE, + { value = of(Coding("http://id.who.int/icd11/mms", "XM5DF6", "")) } + ) + operation = Operation.OR + } + .getQuery() + ) + + assertThat(result.map { it.vaccineCode.codingFirstRep.code }) + .containsExactly("XM1NL1", "XM5DF6") + .inOrder() + } + + @Test + fun test_search_multiple_param_conjunction_with_multiple_values_disjunction() = runBlocking { + val resources = + listOf( + Patient().apply { + id = "patient-01" + addName( + HumanName().apply { + addGiven("John") + family = "Doe" + } + ) + }, + Patient().apply { + id = "patient-02" + addName( + HumanName().apply { + addGiven("Jane") + family = "Doe" + } + ) + }, + Patient().apply { + id = "patient-03" + addName( + HumanName().apply { + addGiven("John") + family = "Roe" + } + ) + }, + Patient().apply { + id = "patient-04" + addName( + HumanName().apply { + addGiven("Jane") + family = "Roe" + } + ) + }, + Patient().apply { + id = "patient-05" + addName( + HumanName().apply { + addGiven("Rocky") + family = "Balboa" + } + ) + } + ) + database.insert(*resources.toTypedArray()) + + val result = + database.search( + Search(ResourceType.Patient) + .apply { + filter( + Patient.GIVEN, + { + value = "John" + modifier = StringFilterModifier.MATCHES_EXACTLY + }, + { + value = "Jane" + modifier = StringFilterModifier.MATCHES_EXACTLY + }, + operation = Operation.OR + ) + + filter( + Patient.FAMILY, + { + value = "Doe" + modifier = StringFilterModifier.MATCHES_EXACTLY + }, + { + value = "Roe" + modifier = StringFilterModifier.MATCHES_EXACTLY + }, + operation = Operation.OR + ) + + operation = Operation.AND + } + .getQuery() + ) + + assertThat(result.map { it.nameFirstRep.nameAsSingleString }) + .containsExactly("John Doe", "Jane Doe", "John Roe", "Jane Roe") + .inOrder() + } + private companion object { const val TEST_PATIENT_1_ID = "test_patient_1" val TEST_PATIENT_1 = Patient() diff --git a/engine/src/main/java/com/google/android/fhir/search/MoreSearch.kt b/engine/src/main/java/com/google/android/fhir/search/MoreSearch.kt index 7a0cee293f..1514f76f46 100644 --- a/engine/src/main/java/com/google/android/fhir/search/MoreSearch.kt +++ b/engine/src/main/java/com/google/android/fhir/search/MoreSearch.kt @@ -24,6 +24,7 @@ import com.google.android.fhir.UcumValue import com.google.android.fhir.UnitConverter import com.google.android.fhir.db.Database import com.google.android.fhir.epochDay +import com.google.android.fhir.search.filter.FilterCriterion import com.google.android.fhir.ucumUrl import java.math.BigDecimal import org.hl7.fhir.r4.model.DateTimeType @@ -70,23 +71,26 @@ internal fun Search.getQuery( var filterStatement = "" val filterArgs = mutableListOf() + val allFilters = + stringFilterCriteria + + referenceFilterCriteria + + dateTimeFilterCriteria + + tokenFilterCriteria + + numberFilterCriteria + + quantityFilterCriteria + val filterQuery = - (stringFilters.map { it.query(type) } + - referenceFilters.map { it.query(type) } + - dateFilter.map { it.query(type) } + - dateTimeFilter.map { it.query(type) } + - tokenFilters.map { it.query(type) } + - numberFilter.map { it.query(type) } + - quantityFilters.map { it.query(type) }) - .intersect() - if (filterQuery != null) { - filterStatement = + (allFilters.mapNonSingleParamValues(type) + allFilters.joinSingleParamValues(type, operation)) + .filterNotNull() + filterQuery.forEachIndexed { i, it -> + filterStatement += """ - AND a.resourceId IN ( - ${filterQuery.query} + ${if (i == 0) "AND a.resourceId IN (" else "a.resourceId IN ("} + ${it.query} ) + ${if (i != filterQuery.lastIndex) operation.name else ""} """.trimIndent() - filterArgs.addAll(filterQuery.args) + filterArgs.addAll(it.args) } var limitStatement = "" @@ -100,7 +104,7 @@ internal fun Search.getQuery( } } - filterStatement += nestedSearches.nestedQuery(filterStatement, filterArgs, type) + filterStatement += nestedSearches.nestedQuery(filterStatement, filterArgs, type, operation) val whereArgs = mutableListOf() val query = when { @@ -145,102 +149,50 @@ internal fun Search.getQuery( return SearchQuery(query, sortArgs + type.name + whereArgs + filterArgs + limitArgs) } -fun StringFilter.query(type: ResourceType): SearchQuery { - val condition = - when (modifier) { - StringFilterModifier.STARTS_WITH -> "LIKE ? || '%' COLLATE NOCASE" - StringFilterModifier.MATCHES_EXACTLY -> "= ?" - StringFilterModifier.CONTAINS -> "LIKE '%' || ? || '%' COLLATE NOCASE" - } - return SearchQuery( - """ - SELECT resourceId FROM StringIndexEntity - WHERE resourceType = ? AND index_name = ? AND index_value $condition - """, - listOf(type.name, parameter.paramName, value!!) - ) -} - -/** - * Extension function that returns a SearchQuery based on the value and prefix of the NumberFilter - */ -fun NumberFilter.query(type: ResourceType): SearchQuery { - - val conditionParamPair = getConditionParamPair(prefix, value!!) - - return SearchQuery( - """ - SELECT resourceId FROM NumberIndexEntity - WHERE resourceType = ? AND index_name = ? AND ${conditionParamPair.condition} - """, - listOf(type.name, parameter.paramName) + conditionParamPair.params - ) -} - -fun ReferenceFilter.query(type: ResourceType): SearchQuery { - return SearchQuery( - """ - SELECT resourceId FROM ReferenceIndexEntity - WHERE resourceType = ? AND index_name = ? AND index_value = ? - """, - listOf(type.name, parameter!!.paramName, value!!) - ) -} - -fun DateFilter.query(type: ResourceType): SearchQuery { - val conditionParamPair = getConditionParamPair(prefix, value!!) - return SearchQuery( - """ - SELECT resourceId FROM DateIndexEntity - WHERE resourceType = ? AND index_name = ? AND ${conditionParamPair.condition} - """, - listOf(type.name, parameter.paramName) + conditionParamPair.params - ) -} - -fun DateTimeFilter.query(type: ResourceType): SearchQuery { - val conditionParamPair = getConditionParamPair(prefix, value!!) - return SearchQuery( - """ - SELECT resourceId FROM DateTimeIndexEntity - WHERE resourceType = ? AND index_name = ? AND ${conditionParamPair.condition} - """, - listOf(type.name, parameter.paramName) + conditionParamPair.params - ) -} - -fun TokenFilter.query(type: ResourceType): SearchQuery { - return SearchQuery( - """ - SELECT resourceId FROM TokenIndexEntity - WHERE resourceType = ? AND index_name = ? AND index_value = ? - AND IFNULL(index_system,'') = ? - """, - listOfNotNull(type.name, parameter!!.paramName, code, uri ?: "") - ) -} - -fun QuantityFilter.query(type: ResourceType): SearchQuery { - val conditionParamPair = getConditionParamPair(prefix, value!!, system, unit) - return SearchQuery( - """ - SELECT resourceId FROM QuantityIndexEntity - WHERE resourceType= ? AND index_name = ? - AND ${conditionParamPair.condition} - """.trimIndent(), - listOfNotNull(type.name, parameter.paramName) + conditionParamPair.params - ) +private fun List.query( + type: ResourceType, + op: Operation = Operation.OR +): SearchQuery { + return map { it.query(type) }.let { + SearchQuery( + it.joinToString("\n${op.resultSetCombiningOperator}\n") { it.query }, + it.flatMap { it.args } + ) + } } -fun List.intersect(): SearchQuery? { +internal fun List.joinSet(operation: Operation): SearchQuery? { return if (isEmpty()) { null } else { - SearchQuery(joinToString("\nINTERSECT\n") { it.query }, flatMap { it.args }) + SearchQuery( + joinToString("\n${operation.resultSetCombiningOperator}\n") { it.query }, + flatMap { it.args } + ) } } -val Order?.sqlString: String +/** + * Maps all the [FilterCriterion]s with multiple values into respective [SearchQuery] joined by + * [Operation.resultSetCombiningOperator] set in [Pair.second]. e.g. filter(Patient.GIVEN, {"John"}, + * {"Jane"},OR) AND filter(Patient.FAMILY, {"Doe"}, {"Roe"},OR) will result in SearchQuery( id in + * (given="John" UNION given="Jane")) and SearchQuery( id in (family="Doe" UNION name="Roe")) and + */ +private fun List.mapNonSingleParamValues(type: ResourceType) = + filterNot { it.filters.size == 1 }.map { it.filters.query(type, it.operation) } + +/** + * Takes all the [FilterCriterion]s with single values and converts them into a single [SearchQuery] + * joined by [Operation.resultSetCombiningOperator] set in [Search.operation]. e.g. + * filter(Patient.GIVEN, {"John"}) OR filter(Patient.FAMILY, {"Doe"}) will result in SearchQuery( id + * in (given="John" UNION family="Doe")) + */ +private fun List.joinSingleParamValues( + type: ResourceType, + op: Operation = Operation.AND +) = filter { it.filters.size == 1 }.map { it.filters.query(type, op) }.joinSet(op) + +private val Order?.sqlString: String get() = when (this) { Order.ASCENDING -> "ASC" @@ -248,7 +200,7 @@ val Order?.sqlString: String null -> "" } -private fun getConditionParamPair(prefix: ParamPrefixEnum, value: DateType): ConditionParam { +internal fun getConditionParamPair(prefix: ParamPrefixEnum, value: DateType): ConditionParam { val start = value.rangeEpochDays.first val end = value.rangeEpochDays.last return when (prefix) { @@ -280,7 +232,7 @@ private fun getConditionParamPair(prefix: ParamPrefixEnum, value: DateType): Con } } -private fun getConditionParamPair( +internal fun getConditionParamPair( prefix: ParamPrefixEnum, value: DateTimeType ): ConditionParam { @@ -319,7 +271,7 @@ private fun getConditionParamPair( * Returns the condition and list of params required in NumberFilter.query see * https://www.hl7.org/fhir/search.html#number. */ -private fun getConditionParamPair( +internal fun getConditionParamPair( prefix: ParamPrefixEnum?, value: BigDecimal ): ConditionParam { @@ -372,7 +324,7 @@ private fun getConditionParamPair( * Returns the condition and list of params required in Quantity.query see * https://www.hl7.org/fhir/search.html#quantity. */ -private fun getConditionParamPair( +internal fun getConditionParamPair( prefix: ParamPrefixEnum?, value: BigDecimal, system: String?, @@ -463,6 +415,6 @@ private val DateType.rangeEpochDays: LongRange private val DateTimeType.rangeEpochMillis get() = LongRange(value.time, precision.add(value, 1).time - 1) -private data class ConditionParam(val condition: String, val params: List) { +internal data class ConditionParam(val condition: String, val params: List) { constructor(condition: String, vararg params: T) : this(condition, params.asList()) } diff --git a/engine/src/main/java/com/google/android/fhir/search/NestedSearch.kt b/engine/src/main/java/com/google/android/fhir/search/NestedSearch.kt index 309a07638e..8bacb95c72 100644 --- a/engine/src/main/java/com/google/android/fhir/search/NestedSearch.kt +++ b/engine/src/main/java/com/google/android/fhir/search/NestedSearch.kt @@ -57,9 +57,10 @@ inline fun Search.has( internal fun List.nestedQuery( filterStatement: String, filterArgs: MutableList, - type: ResourceType + type: ResourceType, + operation: Operation ): String { - return this.map { it.nestedQuery(type) }.intersect()?.let { + return this.map { it.nestedQuery(type) }.joinSet(operation)?.let { filterArgs.addAll(it.args) """ ${(if (filterStatement.isEmpty()) "" else "\n")} diff --git a/engine/src/main/java/com/google/android/fhir/search/SearchDsl.kt b/engine/src/main/java/com/google/android/fhir/search/SearchDsl.kt index a8b49453de..c838dfbbf5 100644 --- a/engine/src/main/java/com/google/android/fhir/search/SearchDsl.kt +++ b/engine/src/main/java/com/google/android/fhir/search/SearchDsl.kt @@ -23,99 +23,88 @@ import ca.uhn.fhir.rest.gclient.QuantityClientParam import ca.uhn.fhir.rest.gclient.ReferenceClientParam import ca.uhn.fhir.rest.gclient.StringClientParam import ca.uhn.fhir.rest.gclient.TokenClientParam -import ca.uhn.fhir.rest.param.ParamPrefixEnum -import java.math.BigDecimal -import org.hl7.fhir.r4.model.CodeType -import org.hl7.fhir.r4.model.CodeableConcept -import org.hl7.fhir.r4.model.Coding -import org.hl7.fhir.r4.model.ContactPoint -import org.hl7.fhir.r4.model.DateTimeType -import org.hl7.fhir.r4.model.DateType -import org.hl7.fhir.r4.model.Identifier +import com.google.android.fhir.search.filter.DateParamFilterCriterion +import com.google.android.fhir.search.filter.FilterCriterion +import com.google.android.fhir.search.filter.NumberParamFilterCriterion +import com.google.android.fhir.search.filter.QuantityParamFilterCriterion +import com.google.android.fhir.search.filter.ReferenceParamFilterCriterion +import com.google.android.fhir.search.filter.StringParamFilterCriterion +import com.google.android.fhir.search.filter.TokenParamFilterCriterion +import org.hl7.fhir.r4.model.Patient import org.hl7.fhir.r4.model.ResourceType -import org.hl7.fhir.r4.model.UriType @SearchDslMarker data class Search(val type: ResourceType, var count: Int? = null, var from: Int? = null) { - internal val stringFilters = mutableListOf() - internal val dateFilter = mutableListOf() - internal val dateTimeFilter = mutableListOf() - internal val numberFilter = mutableListOf() - internal val referenceFilters = mutableListOf() - internal val tokenFilters = mutableListOf() - internal val quantityFilters = mutableListOf() + internal val p = Patient() + internal val stringFilterCriteria = mutableListOf() + internal val dateTimeFilterCriteria = mutableListOf() + internal val numberFilterCriteria = mutableListOf() + internal val referenceFilterCriteria = mutableListOf() + internal val tokenFilterCriteria = mutableListOf() + internal val quantityFilterCriteria = mutableListOf() internal var sort: IParam? = null internal var order: Order? = null @PublishedApi internal var nestedSearches = mutableListOf() + var operation = Operation.AND - fun filter(stringParameter: StringClientParam, init: StringFilter.() -> Unit) { - val filter = StringFilter(stringParameter) - filter.init() - stringFilters.add(filter) + fun filter( + stringParameter: StringClientParam, + vararg init: StringParamFilterCriterion.() -> Unit, + operation: Operation = Operation.OR + ) { + val filters = mutableListOf() + init.forEach { StringParamFilterCriterion(stringParameter).apply(it).also(filters::add) } + stringFilterCriteria.add(StringParamFilterCriteria(filters, operation)) } - fun filter(referenceParameter: ReferenceClientParam, init: ReferenceFilter.() -> Unit) { - val filter = ReferenceFilter(referenceParameter) - filter.init() - referenceFilters.add(filter) + fun filter( + referenceParameter: ReferenceClientParam, + vararg init: ReferenceParamFilterCriterion.() -> Unit, + operation: Operation = Operation.OR + ) { + val filters = mutableListOf() + init.forEach { ReferenceParamFilterCriterion(referenceParameter).apply(it).also(filters::add) } + referenceFilterCriteria.add(ReferenceParamFilterCriteria(filters, operation)) } fun filter( dateParameter: DateClientParam, - date: DateType, - prefix: ParamPrefixEnum = ParamPrefixEnum.EQUAL + vararg init: DateParamFilterCriterion.() -> Unit, + operation: Operation = Operation.OR ) { - dateFilter.add(DateFilter(dateParameter, prefix, date)) + val filters = mutableListOf() + init.forEach { DateParamFilterCriterion(dateParameter).apply(it).also(filters::add) } + dateTimeFilterCriteria.add(DateClientParamFilterCriteria(filters, operation)) } fun filter( - dateParameter: DateClientParam, - dateTime: DateTimeType, - prefix: ParamPrefixEnum = ParamPrefixEnum.EQUAL + parameter: QuantityClientParam, + vararg init: QuantityParamFilterCriterion.() -> Unit, + operation: Operation = Operation.OR ) { - dateTimeFilter.add(DateTimeFilter(dateParameter, prefix, dateTime)) + val filters = mutableListOf() + init.forEach { QuantityParamFilterCriterion(parameter).apply(it).also(filters::add) } + quantityFilterCriteria.add(QuantityParamFilterCriteria(filters, operation)) } - fun filter(parameter: QuantityClientParam, init: QuantityFilter.() -> Unit) { - val filter = QuantityFilter(parameter) - filter.init() - quantityFilters.add(filter) + fun filter( + filter: TokenClientParam, + vararg init: TokenParamFilterCriterion.() -> Unit, + operation: Operation = Operation.OR + ) { + val filters = mutableListOf() + init.forEach { TokenParamFilterCriterion(filter).apply(it).also(filters::add) } + tokenFilterCriteria.add(TokenParamFilterCriteria(filters, operation)) } - fun filter(filter: TokenClientParam, coding: Coding) = - tokenFilters.add(TokenFilter(parameter = filter, uri = coding.system, code = coding.code)) - - fun filter(filter: TokenClientParam, codeableConcept: CodeableConcept) = - codeableConcept.coding.forEach { - tokenFilters.add(TokenFilter(parameter = filter, uri = it.system, code = it.code)) - } - - fun filter(filter: TokenClientParam, identifier: Identifier) = - tokenFilters.add( - TokenFilter(parameter = filter, uri = identifier.system, code = identifier.value) - ) - - fun filter(filter: TokenClientParam, contactPoint: ContactPoint) = - tokenFilters.add( - TokenFilter(parameter = filter, uri = contactPoint.use?.toCode(), code = contactPoint.value) - ) - - fun filter(filter: TokenClientParam, codeType: CodeType) = - tokenFilters.add(TokenFilter(parameter = filter, code = codeType.value)) - - fun filter(filter: TokenClientParam, boolean: Boolean) = - tokenFilters.add(TokenFilter(parameter = filter, code = boolean.toString())) - - fun filter(filter: TokenClientParam, uriType: UriType) = - tokenFilters.add(TokenFilter(parameter = filter, code = uriType.value)) - - fun filter(filter: TokenClientParam, string: String) = - tokenFilters.add(TokenFilter(parameter = filter, code = string)) - - fun filter(numberParameter: NumberClientParam, init: NumberFilter.() -> Unit) { - val filter = NumberFilter(numberParameter) - filter.init() - numberFilter.add(filter) + fun filter( + numberParameter: NumberClientParam, + vararg init: NumberParamFilterCriterion.() -> Unit, + operation: Operation = Operation.OR + ) { + val filters = mutableListOf() + init.forEach { NumberParamFilterCriterion(numberParameter).apply(it).also(filters::add) } + numberFilterCriteria.add(NumberParamFilterCriteria(filters, operation)) } fun sort(parameter: StringClientParam, order: Order) { @@ -129,49 +118,6 @@ data class Search(val type: ResourceType, var count: Int? = null, var from: Int? } } -@SearchDslMarker -data class StringFilter( - val parameter: StringClientParam, - var modifier: StringFilterModifier = StringFilterModifier.STARTS_WITH, - var value: String? = null -) - -@SearchDslMarker -data class DateFilter( - val parameter: DateClientParam, - var prefix: ParamPrefixEnum = ParamPrefixEnum.EQUAL, - var value: DateType? = null -) - -@SearchDslMarker -data class DateTimeFilter( - val parameter: DateClientParam, - var prefix: ParamPrefixEnum = ParamPrefixEnum.EQUAL, - var value: DateTimeType? = null -) - -@SearchDslMarker -data class ReferenceFilter(val parameter: ReferenceClientParam?, var value: String? = null) - -@SearchDslMarker -data class NumberFilter( - val parameter: NumberClientParam, - var prefix: ParamPrefixEnum? = null, - var value: BigDecimal? = null -) - -@SearchDslMarker -data class TokenFilter(val parameter: TokenClientParam?, var uri: String? = null, var code: String) - -@SearchDslMarker -data class QuantityFilter( - val parameter: QuantityClientParam, - var prefix: ParamPrefixEnum? = null, - var value: BigDecimal? = null, - var system: String? = null, - var unit: String? = null -) - enum class Order { ASCENDING, DESCENDING @@ -183,4 +129,53 @@ enum class StringFilterModifier { CONTAINS } -@PublishedApi internal data class NestedQuery(val param: ReferenceClientParam, val search: Search) +/** Logical operator between the filter values or the filters themselves. */ +enum class Operation(val resultSetCombiningOperator: String) { + OR("UNION"), + AND("INTERSECT"), +} + +/** + * Contains a set of filter criteria sharing the same search parameter. e.g A + * [StringParamFilterCriteria] may contain a list of [StringParamFilterCriterion] each with + * different [StringParamFilterCriterion.value] and [StringParamFilterCriterion.modifier] to filter + * results for a particular [StringClientParam] like [Patient.GIVEN]. + * + * An api call like filter(Patient.GIVEN,{value = "John"},{value = "Jane"}) will create a + * [StringParamFilterCriteria] with two [StringParamFilterCriterion] one with + * [StringParamFilterCriterion.value] as "John" and other as "Jane." + */ +internal sealed class FilterCriteria( + open val filters: List, + open val operation: Operation +) + +internal data class StringParamFilterCriteria( + override val filters: List, + override val operation: Operation +) : FilterCriteria(filters, operation) + +internal data class DateClientParamFilterCriteria( + override val filters: List, + override val operation: Operation +) : FilterCriteria(filters, operation) + +internal data class NumberParamFilterCriteria( + override val filters: List, + override val operation: Operation +) : FilterCriteria(filters, operation) + +internal data class ReferenceParamFilterCriteria( + override val filters: List, + override val operation: Operation +) : FilterCriteria(filters, operation) + +internal data class TokenParamFilterCriteria( + override val filters: List, + override val operation: Operation +) : FilterCriteria(filters, operation) + +internal data class QuantityParamFilterCriteria( + override val filters: List, + override val operation: Operation +) : FilterCriteria(filters, operation) diff --git a/engine/src/main/java/com/google/android/fhir/search/filter/DateParamFilterCriterion.kt b/engine/src/main/java/com/google/android/fhir/search/filter/DateParamFilterCriterion.kt new file mode 100644 index 0000000000..2917e5e294 --- /dev/null +++ b/engine/src/main/java/com/google/android/fhir/search/filter/DateParamFilterCriterion.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.fhir.search.filter + +import ca.uhn.fhir.rest.gclient.DateClientParam +import ca.uhn.fhir.rest.param.ParamPrefixEnum +import com.google.android.fhir.search.SearchDslMarker +import com.google.android.fhir.search.SearchQuery +import com.google.android.fhir.search.getConditionParamPair +import org.hl7.fhir.r4.model.DateTimeType +import org.hl7.fhir.r4.model.DateType +import org.hl7.fhir.r4.model.ResourceType + +/** + * Represents a criterion for filtering [DateClientParam]. e.g. filter(Patient.BIRTHDATE, { value + * =of(DateType("2013-03-14")) }) + */ +@SearchDslMarker +data class DateParamFilterCriterion( + val parameter: DateClientParam, + var prefix: ParamPrefixEnum = ParamPrefixEnum.EQUAL, + var value: DateFilterValues? = null +) : FilterCriterion { + /** Returns [DateFilterValues] from [DateType]. */ + fun of(date: DateType) = DateFilterValues().apply { this.date = date } + + /** Returns [DateFilterValues] from [DateTimeType]. */ + fun of(dateTime: DateTimeType) = DateFilterValues().apply { this.dateTime = dateTime } + + override fun query(type: ResourceType): SearchQuery { + return when { + value?.date != null -> { + val conditionParamPair = getConditionParamPair(prefix, value?.date!!) + SearchQuery( + """ + SELECT resourceId FROM DateIndexEntity + WHERE resourceType = ? AND index_name = ? AND ${conditionParamPair.condition} + """, + listOf(type.name, parameter.paramName) + conditionParamPair.params + ) + } + value?.dateTime != null -> { + val conditionParamPair = getConditionParamPair(prefix, value?.dateTime!!) + SearchQuery( + """ + SELECT resourceId FROM DateTimeIndexEntity + WHERE resourceType = ? AND index_name = ? AND ${conditionParamPair.condition} + """, + listOf(type.name, parameter.paramName) + conditionParamPair.params + ) + } + else -> throw IllegalArgumentException("DateClientParamFilter.value can't be null.") + } + } +} + +@SearchDslMarker +class DateFilterValues internal constructor() { + var date: DateType? = null + var dateTime: DateTimeType? = null +} diff --git a/engine/src/main/java/com/google/android/fhir/search/filter/FilterCriterion.kt b/engine/src/main/java/com/google/android/fhir/search/filter/FilterCriterion.kt new file mode 100644 index 0000000000..c72460c320 --- /dev/null +++ b/engine/src/main/java/com/google/android/fhir/search/filter/FilterCriterion.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.fhir.search.filter + +import com.google.android.fhir.search.SearchQuery +import org.hl7.fhir.r4.model.ResourceType + +/** Represents filter for a [IParam] */ +internal interface FilterCriterion { + fun query(type: ResourceType): SearchQuery +} diff --git a/engine/src/main/java/com/google/android/fhir/search/filter/NumberParamFilterCriterion.kt b/engine/src/main/java/com/google/android/fhir/search/filter/NumberParamFilterCriterion.kt new file mode 100644 index 0000000000..e653928b9e --- /dev/null +++ b/engine/src/main/java/com/google/android/fhir/search/filter/NumberParamFilterCriterion.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.fhir.search.filter + +import ca.uhn.fhir.rest.gclient.NumberClientParam +import ca.uhn.fhir.rest.param.ParamPrefixEnum +import com.google.android.fhir.search.SearchDslMarker +import com.google.android.fhir.search.SearchQuery +import com.google.android.fhir.search.getConditionParamPair +import java.math.BigDecimal +import org.hl7.fhir.r4.model.ResourceType + +/** + * Represents a criterion for filtering [NumberClientParam]. e.g. + * filter(RiskAssessment.PROBABILITY,{value = BigDecimal("100")}). + */ +@SearchDslMarker +data class NumberParamFilterCriterion( + val parameter: NumberClientParam, + var prefix: ParamPrefixEnum? = null, + var value: BigDecimal? = null +) : FilterCriterion { + override fun query(type: ResourceType): SearchQuery { + val conditionParamPair = getConditionParamPair(prefix, value!!) + return SearchQuery( + """ + SELECT resourceId FROM NumberIndexEntity + WHERE resourceType = ? AND index_name = ? AND ${conditionParamPair.condition} + """, + listOf(type.name, parameter.paramName) + conditionParamPair.params + ) + } +} diff --git a/engine/src/main/java/com/google/android/fhir/search/filter/QuantityParamFilterCriterion.kt b/engine/src/main/java/com/google/android/fhir/search/filter/QuantityParamFilterCriterion.kt new file mode 100644 index 0000000000..e1cefbca3c --- /dev/null +++ b/engine/src/main/java/com/google/android/fhir/search/filter/QuantityParamFilterCriterion.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.fhir.search.filter + +import ca.uhn.fhir.rest.gclient.QuantityClientParam +import ca.uhn.fhir.rest.param.ParamPrefixEnum +import com.google.android.fhir.search.SearchDslMarker +import com.google.android.fhir.search.SearchQuery +import com.google.android.fhir.search.getConditionParamPair +import java.math.BigDecimal +import org.hl7.fhir.r4.model.ResourceType + +/** + * Represents a criterion for filtering [QuantityClientParam]. e.g. + * filter(Observation.VALUE_QUANTITY,{value = BigDecimal("5.403")} ) + */ +@SearchDslMarker +data class QuantityParamFilterCriterion( + val parameter: QuantityClientParam, + var prefix: ParamPrefixEnum? = null, + var value: BigDecimal? = null, + var system: String? = null, + var unit: String? = null +) : FilterCriterion { + override fun query(type: ResourceType): SearchQuery { + val conditionParamPair = getConditionParamPair(prefix, value!!, system, unit) + return SearchQuery( + """ + SELECT resourceId FROM QuantityIndexEntity + WHERE resourceType= ? AND index_name = ? + AND ${conditionParamPair.condition} + """.trimIndent(), + listOfNotNull(type.name, parameter.paramName) + conditionParamPair.params + ) + } +} diff --git a/engine/src/main/java/com/google/android/fhir/search/filter/ReferenceParamFilterCriterion.kt b/engine/src/main/java/com/google/android/fhir/search/filter/ReferenceParamFilterCriterion.kt new file mode 100644 index 0000000000..4e37d856c9 --- /dev/null +++ b/engine/src/main/java/com/google/android/fhir/search/filter/ReferenceParamFilterCriterion.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.fhir.search.filter + +import ca.uhn.fhir.rest.gclient.ReferenceClientParam +import com.google.android.fhir.search.SearchDslMarker +import com.google.android.fhir.search.SearchQuery +import org.hl7.fhir.r4.model.ResourceType + +/** + * Represents a criterion for filtering [ReferenceClientParam]. e.g. filter(Observation.SUBJECT, { + * value = "Patient/001" }) + */ +@SearchDslMarker +data class ReferenceParamFilterCriterion( + val parameter: ReferenceClientParam?, + var value: String? = null +) : FilterCriterion { + override fun query(type: ResourceType): SearchQuery { + return SearchQuery( + """ + SELECT resourceId FROM ReferenceIndexEntity + WHERE resourceType = ? AND index_name = ? AND index_value = ? + """, + listOf(type.name, parameter!!.paramName, value!!) + ) + } +} diff --git a/engine/src/main/java/com/google/android/fhir/search/filter/StringParamFilterCriterion.kt b/engine/src/main/java/com/google/android/fhir/search/filter/StringParamFilterCriterion.kt new file mode 100644 index 0000000000..f9635d361e --- /dev/null +++ b/engine/src/main/java/com/google/android/fhir/search/filter/StringParamFilterCriterion.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.fhir.search.filter + +import ca.uhn.fhir.rest.gclient.StringClientParam +import com.google.android.fhir.search.SearchDslMarker +import com.google.android.fhir.search.SearchQuery +import com.google.android.fhir.search.StringFilterModifier +import org.hl7.fhir.r4.model.ResourceType + +/** + * Represents a criterion for filtering [StringClientParam]. e.g. filter(Patient.FAMILY, { value = + * "Jones" }) + */ +@SearchDslMarker +data class StringParamFilterCriterion( + val parameter: StringClientParam, + var modifier: StringFilterModifier = StringFilterModifier.STARTS_WITH, + var value: String? = null +) : FilterCriterion { + override fun query(type: ResourceType): SearchQuery { + val condition = + when (modifier) { + StringFilterModifier.STARTS_WITH -> "LIKE ? || '%' COLLATE NOCASE" + StringFilterModifier.MATCHES_EXACTLY -> "= ?" + StringFilterModifier.CONTAINS -> "LIKE '%' || ? || '%' COLLATE NOCASE" + } + return SearchQuery( + """ + SELECT resourceId FROM StringIndexEntity + WHERE resourceType = ? AND index_name = ? AND index_value $condition + """, + listOf(type.name, parameter.paramName, value!!) + ) + } +} diff --git a/engine/src/main/java/com/google/android/fhir/search/filter/TokenParamFilterCriterion.kt b/engine/src/main/java/com/google/android/fhir/search/filter/TokenParamFilterCriterion.kt new file mode 100644 index 0000000000..c8489687eb --- /dev/null +++ b/engine/src/main/java/com/google/android/fhir/search/filter/TokenParamFilterCriterion.kt @@ -0,0 +1,123 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.fhir.search.filter + +import ca.uhn.fhir.rest.gclient.TokenClientParam +import com.google.android.fhir.search.Operation +import com.google.android.fhir.search.SearchDslMarker +import com.google.android.fhir.search.SearchQuery +import org.hl7.fhir.r4.model.CodeType +import org.hl7.fhir.r4.model.CodeableConcept +import org.hl7.fhir.r4.model.Coding +import org.hl7.fhir.r4.model.ContactPoint +import org.hl7.fhir.r4.model.Identifier +import org.hl7.fhir.r4.model.ResourceType +import org.hl7.fhir.r4.model.UriType + +/** + * Represents a criterion for filtering [TokenClientParam]. e.g. filter(Patient.GENDER, { value = + * of(CodeType("male")) }) + */ +@SearchDslMarker +data class TokenParamFilterCriterion internal constructor(var parameter: TokenClientParam) : + FilterCriterion { + var value: TokenFilterValue? = null + + /** Returns [TokenFilterValue] from [Boolean]. */ + fun of(boolean: Boolean) = + TokenFilterValue().apply { + tokenFilters.add(TokenParamFilterValueInstance(code = boolean.toString())) + } + + /** Returns [TokenFilterValue] from [String]. */ + fun of(string: String) = + TokenFilterValue().apply { tokenFilters.add(TokenParamFilterValueInstance(code = string)) } + + /** Returns [TokenFilterValue] from [UriType]. */ + fun of(uriType: UriType) = + TokenFilterValue().apply { + tokenFilters.add(TokenParamFilterValueInstance(code = uriType.value)) + } + + /** Returns [TokenFilterValue] from [CodeType]. */ + fun of(codeType: CodeType) = + TokenFilterValue().apply { + tokenFilters.add(TokenParamFilterValueInstance(code = codeType.value)) + } + + /** Returns [TokenFilterValue] from [Coding]. */ + fun of(coding: Coding) = + TokenFilterValue().apply { + tokenFilters.add(TokenParamFilterValueInstance(uri = coding.system, code = coding.code)) + } + + /** Returns [TokenFilterValue] from [CodeableConcept]. */ + fun of(codeableConcept: CodeableConcept) = + TokenFilterValue().apply { + codeableConcept.coding.forEach { + tokenFilters.add(TokenParamFilterValueInstance(uri = it.system, code = it.code)) + } + } + + /** Returns [TokenFilterValue] from [Identifier]. */ + fun of(identifier: Identifier) = + TokenFilterValue().apply { + tokenFilters.add( + TokenParamFilterValueInstance(uri = identifier.system, code = identifier.value) + ) + } + + /** Returns [TokenFilterValue] from [ContactPoint]. */ + fun of(contactPoint: ContactPoint) = + TokenFilterValue().apply { + tokenFilters.add( + TokenParamFilterValueInstance(uri = contactPoint.use?.toCode(), code = contactPoint.value) + ) + } + + override fun query(type: ResourceType): SearchQuery { + return value!!.tokenFilters.map { it.query(type, parameter) }.let { + SearchQuery( + it.joinToString("\n${Operation.OR.resultSetCombiningOperator}\n") { it.query }, + it.flatMap { it.args } + ) + } + } +} + +@SearchDslMarker +class TokenFilterValue internal constructor() { + internal val tokenFilters = mutableListOf() +} + +/** + * A structure like [CodeableConcept] may contain multiple [Coding] values each of which will be a + * filter value. We use [TokenParamFilterValueInstance] to represent individual filter value. + */ +@SearchDslMarker +internal data class TokenParamFilterValueInstance(var uri: String? = null, var code: String) { + fun query(type: ResourceType, parameter: TokenClientParam): SearchQuery { + return SearchQuery( + """ + SELECT resourceId FROM TokenIndexEntity + WHERE resourceType = ? AND index_name = ? AND index_value = ? + AND IFNULL(index_system,'') = ? + """, + listOfNotNull(type.name, parameter.paramName, code, uri ?: "") + ) + } +} diff --git a/engine/src/test/java/com/google/android/fhir/search/SearchTest.kt b/engine/src/test/java/com/google/android/fhir/search/SearchTest.kt index 919629145e..24b97dfde1 100644 --- a/engine/src/test/java/com/google/android/fhir/search/SearchTest.kt +++ b/engine/src/test/java/com/google/android/fhir/search/SearchTest.kt @@ -80,7 +80,7 @@ class SearchTest { fun search_string_default() { val query = Search(ResourceType.Patient) - .apply { filter(Patient.ADDRESS) { value = "someValue" } } + .apply { filter(Patient.ADDRESS, { value = "someValue" }) } .getQuery() assertThat(query.query) @@ -109,10 +109,13 @@ class SearchTest { val query = Search(ResourceType.Patient) .apply { - filter(Patient.ADDRESS) { - modifier = StringFilterModifier.MATCHES_EXACTLY - value = "someValue" - } + filter( + Patient.ADDRESS, + { + modifier = StringFilterModifier.MATCHES_EXACTLY + value = "someValue" + } + ) } .getQuery() @@ -142,10 +145,13 @@ class SearchTest { val query = Search(ResourceType.Patient) .apply { - filter(Patient.ADDRESS) { - modifier = StringFilterModifier.CONTAINS - value = "someValue" - } + filter( + Patient.ADDRESS, + { + modifier = StringFilterModifier.CONTAINS + value = "someValue" + } + ) } .getQuery() @@ -211,7 +217,7 @@ class SearchTest { @Test fun search_filter() { val query = - Search(ResourceType.Patient).apply { filter(Patient.FAMILY) { value = "Jones" } }.getQuery() + Search(ResourceType.Patient).apply { filter(Patient.FAMILY, { value = "Jones" }) }.getQuery() assertThat(query.query) .isEqualTo( @@ -243,7 +249,10 @@ class SearchTest { .apply { filter( Patient.GENDER, - Coding("http://hl7.org/fhir/ValueSet/administrative-gender", "male", "Male") + { + value = + of(Coding("http://hl7.org/fhir/ValueSet/administrative-gender", "male", "Male")) + } ) } .getQuery() @@ -280,7 +289,10 @@ class SearchTest { .apply { filter( Immunization.VACCINE_CODE, - CodeableConcept(Coding("http://snomed.info/sct", "260385009", "Allergy X")) + { + value = + of(CodeableConcept(Coding("http://snomed.info/sct", "260385009", "Allergy X"))) + } ) } .getQuery() @@ -317,7 +329,9 @@ class SearchTest { identifier.system = "http://acme.org/patient" val query = - Search(ResourceType.Patient).apply { filter(Patient.IDENTIFIER, identifier) }.getQuery() + Search(ResourceType.Patient) + .apply { filter(Patient.IDENTIFIER, { value = of(identifier) }) } + .getQuery() assertThat(query.query) .isEqualTo( """ @@ -350,10 +364,15 @@ class SearchTest { .apply { filter( Patient.TELECOM, - ContactPoint().apply { - system = ContactPoint.ContactPointSystem.EMAIL - use = ContactPoint.ContactPointUse.HOME - value = "test@gmail.com" + { + value = + of( + ContactPoint().apply { + system = ContactPoint.ContactPointSystem.EMAIL + use = ContactPoint.ContactPointUse.HOME + value = "test@gmail.com" + } + ) } ) } @@ -390,9 +409,14 @@ class SearchTest { .apply { filter( Patient.TELECOM, - ContactPoint().apply { - system = ContactPoint.ContactPointSystem.EMAIL - value = "test@gmail.com" + { + value = + of( + ContactPoint().apply { + system = ContactPoint.ContactPointSystem.EMAIL + value = "test@gmail.com" + } + ) } ) } @@ -425,7 +449,9 @@ class SearchTest { @Test fun search_filter_token_codeType() { val query = - Search(ResourceType.Patient).apply { filter(Patient.GENDER, CodeType("male")) }.getQuery() + Search(ResourceType.Patient) + .apply { filter(Patient.GENDER, { value = of(CodeType("male")) }) } + .getQuery() assertThat(query.query) .isEqualTo( """ @@ -453,7 +479,8 @@ class SearchTest { @Test fun search_filter_token_boolean() { - val query = Search(ResourceType.Patient).apply { filter(Patient.ACTIVE, true) }.getQuery() + val query = + Search(ResourceType.Patient).apply { filter(Patient.ACTIVE, { value = of(true) }) }.getQuery() assertThat(query.query) .isEqualTo( @@ -484,7 +511,12 @@ class SearchTest { fun search_filter_token_uriType() { val query = Search(ResourceType.Patient) - .apply { filter(Patient.IDENTIFIER, UriType("16009886-bd57-11eb-8529-0242ac130003")) } + .apply { + filter( + Patient.IDENTIFIER, + { value = of(UriType("16009886-bd57-11eb-8529-0242ac130003")) } + ) + } .getQuery() assertThat(query.query) @@ -515,7 +547,9 @@ class SearchTest { @Test fun search_filter_token_string() { val query = - Search(ResourceType.Patient).apply { filter(Patient.PHONE, "+14845219791") }.getQuery() + Search(ResourceType.Patient) + .apply { filter(Patient.PHONE, { value = of("+14845219791") }) } + .getQuery() assertThat(query.query) .isEqualTo( @@ -546,7 +580,15 @@ class SearchTest { fun search_date_starts_after() { val query = Search(ResourceType.Patient) - .apply { filter(Patient.BIRTHDATE, DateType("2013-03-14"), ParamPrefixEnum.STARTS_AFTER) } + .apply { + filter( + Patient.BIRTHDATE, + { + prefix = ParamPrefixEnum.STARTS_AFTER + value = of(DateType("2013-03-14")) + } + ) + } .getQuery() assertThat(query.query) @@ -577,7 +619,15 @@ class SearchTest { fun search_date_ends_before() { val query = Search(ResourceType.Patient) - .apply { filter(Patient.BIRTHDATE, DateType("2013-03-14"), ParamPrefixEnum.ENDS_BEFORE) } + .apply { + filter( + Patient.BIRTHDATE, + { + value = of(DateType("2013-03-14")) + prefix = ParamPrefixEnum.ENDS_BEFORE + } + ) + } .getQuery() assertThat(query.query) @@ -608,7 +658,15 @@ class SearchTest { fun search_date_not_equal() { val query = Search(ResourceType.Patient) - .apply { filter(Patient.BIRTHDATE, DateType("2013-03-14"), ParamPrefixEnum.NOT_EQUAL) } + .apply { + filter( + Patient.BIRTHDATE, + { + value = of(DateType("2013-03-14")) + prefix = ParamPrefixEnum.NOT_EQUAL + } + ) + } .getQuery() assertThat(query.query) @@ -642,7 +700,7 @@ class SearchTest { fun search_date_equal() { val query = Search(ResourceType.Patient) - .apply { filter(Patient.BIRTHDATE, DateType("2013-03-14")) } + .apply { filter(Patient.BIRTHDATE, { value = of(DateType("2013-03-14")) }) } .getQuery() assertThat(query.query) @@ -676,7 +734,15 @@ class SearchTest { fun search_date_greater() { val query = Search(ResourceType.Patient) - .apply { filter(Patient.BIRTHDATE, DateType("2013-03-14"), ParamPrefixEnum.GREATERTHAN) } + .apply { + filter( + Patient.BIRTHDATE, + { + value = of(DateType("2013-03-14")) + prefix = ParamPrefixEnum.GREATERTHAN + } + ) + } .getQuery() assertThat(query.query) @@ -708,7 +774,13 @@ class SearchTest { val query = Search(ResourceType.Patient) .apply { - filter(Patient.BIRTHDATE, DateType("2013-03-14"), ParamPrefixEnum.GREATERTHAN_OR_EQUALS) + filter( + Patient.BIRTHDATE, + { + value = of(DateType("2013-03-14")) + prefix = ParamPrefixEnum.GREATERTHAN_OR_EQUALS + } + ) } .getQuery() @@ -740,7 +812,15 @@ class SearchTest { fun search_date_less() { val query = Search(ResourceType.Patient) - .apply { filter(Patient.BIRTHDATE, DateType("2013-03-14"), ParamPrefixEnum.LESSTHAN) } + .apply { + filter( + Patient.BIRTHDATE, + { + value = of(DateType("2013-03-14")) + prefix = ParamPrefixEnum.LESSTHAN + } + ) + } .getQuery() assertThat(query.query) @@ -772,7 +852,13 @@ class SearchTest { val query = Search(ResourceType.Patient) .apply { - filter(Patient.BIRTHDATE, DateType("2013-03-14"), ParamPrefixEnum.LESSTHAN_OR_EQUALS) + filter( + Patient.BIRTHDATE, + { + value = of(DateType("2013-03-14")) + prefix = ParamPrefixEnum.LESSTHAN_OR_EQUALS + } + ) } .getQuery() @@ -805,7 +891,13 @@ class SearchTest { val query = Search(ResourceType.Patient) .apply { - filter(Patient.BIRTHDATE, DateTimeType("2013-03-14"), ParamPrefixEnum.STARTS_AFTER) + filter( + Patient.BIRTHDATE, + { + value = of(DateTimeType("2013-03-14")) + prefix = ParamPrefixEnum.STARTS_AFTER + } + ) } .getQuery() @@ -838,7 +930,13 @@ class SearchTest { val query = Search(ResourceType.Patient) .apply { - filter(Patient.BIRTHDATE, DateTimeType("2013-03-14"), ParamPrefixEnum.ENDS_BEFORE) + filter( + Patient.BIRTHDATE, + { + value = of(DateTimeType("2013-03-14")) + prefix = ParamPrefixEnum.ENDS_BEFORE + } + ) } .getQuery() @@ -870,7 +968,15 @@ class SearchTest { fun search_dateTime_not_equal() { val query = Search(ResourceType.Patient) - .apply { filter(Patient.BIRTHDATE, DateTimeType("2013-03-14"), ParamPrefixEnum.NOT_EQUAL) } + .apply { + filter( + Patient.BIRTHDATE, + { + value = of(DateTimeType("2013-03-14")) + prefix = ParamPrefixEnum.NOT_EQUAL + } + ) + } .getQuery() assertThat(query.query) @@ -904,7 +1010,15 @@ class SearchTest { fun search_dateTime_equal() { val query = Search(ResourceType.Patient) - .apply { filter(Patient.BIRTHDATE, DateTimeType("2013-03-14"), ParamPrefixEnum.EQUAL) } + .apply { + filter( + Patient.BIRTHDATE, + { + value = of(DateTimeType("2013-03-14")) + prefix = ParamPrefixEnum.EQUAL + } + ) + } .getQuery() assertThat(query.query) @@ -939,7 +1053,13 @@ class SearchTest { val query = Search(ResourceType.Patient) .apply { - filter(Patient.BIRTHDATE, DateTimeType("2013-03-14"), ParamPrefixEnum.GREATERTHAN) + filter( + Patient.BIRTHDATE, + { + value = of(DateTimeType("2013-03-14")) + prefix = ParamPrefixEnum.GREATERTHAN + } + ) } .getQuery() @@ -974,8 +1094,10 @@ class SearchTest { .apply { filter( Patient.BIRTHDATE, - DateTimeType("2013-03-14"), - ParamPrefixEnum.GREATERTHAN_OR_EQUALS + { + value = of(DateTimeType("2013-03-14")) + prefix = ParamPrefixEnum.GREATERTHAN_OR_EQUALS + } ) } .getQuery() @@ -1008,7 +1130,15 @@ class SearchTest { fun search_dateTime_less() { val query = Search(ResourceType.Patient) - .apply { filter(Patient.BIRTHDATE, DateTimeType("2013-03-14"), ParamPrefixEnum.LESSTHAN) } + .apply { + filter( + Patient.BIRTHDATE, + { + value = of(DateTimeType("2013-03-14")) + prefix = ParamPrefixEnum.LESSTHAN + } + ) + } .getQuery() assertThat(query.query) @@ -1040,7 +1170,13 @@ class SearchTest { val query = Search(ResourceType.Patient) .apply { - filter(Patient.BIRTHDATE, DateTimeType("2013-03-14"), ParamPrefixEnum.LESSTHAN_OR_EQUALS) + filter( + Patient.BIRTHDATE, + { + value = of(DateTimeType("2013-03-14")) + prefix = ParamPrefixEnum.LESSTHAN_OR_EQUALS + } + ) } .getQuery() @@ -1131,7 +1267,7 @@ class SearchTest { val query = Search(ResourceType.Patient) .apply { - filter(Patient.FAMILY) { value = "Jones" } + filter(Patient.FAMILY, { value = "Jones" }) sort(Patient.GIVEN, Order.ASCENDING) count = 10 from = 20 @@ -1181,10 +1317,13 @@ class SearchTest { val query = Search(ResourceType.RiskAssessment) .apply { - filter(RiskAssessment.PROBABILITY) { - prefix = ParamPrefixEnum.EQUAL - value = x.first - } + filter( + RiskAssessment.PROBABILITY, + { + prefix = ParamPrefixEnum.EQUAL + value = x.first + } + ) } .getQuery() assertThat(query.query) @@ -1218,10 +1357,13 @@ class SearchTest { val query = Search(ResourceType.RiskAssessment) .apply { - filter(RiskAssessment.PROBABILITY) { - prefix = ParamPrefixEnum.NOT_EQUAL - value = BigDecimal("100.00") - } + filter( + RiskAssessment.PROBABILITY, + { + prefix = ParamPrefixEnum.NOT_EQUAL + value = BigDecimal("100.00") + } + ) } .getQuery() assertThat(query.query) @@ -1254,10 +1396,13 @@ class SearchTest { val query = Search(ResourceType.RiskAssessment) .apply { - filter(RiskAssessment.PROBABILITY) { - prefix = ParamPrefixEnum.GREATERTHAN - value = BigDecimal("100.00") - } + filter( + RiskAssessment.PROBABILITY, + { + prefix = ParamPrefixEnum.GREATERTHAN + value = BigDecimal("100.00") + } + ) } .getQuery() assertThat(query.query) @@ -1288,10 +1433,13 @@ class SearchTest { val query = Search(ResourceType.RiskAssessment) .apply { - filter(RiskAssessment.PROBABILITY) { - prefix = ParamPrefixEnum.GREATERTHAN_OR_EQUALS - value = BigDecimal("100.00") - } + filter( + RiskAssessment.PROBABILITY, + { + prefix = ParamPrefixEnum.GREATERTHAN_OR_EQUALS + value = BigDecimal("100.00") + } + ) } .getQuery() assertThat(query.query) @@ -1322,10 +1470,13 @@ class SearchTest { val query = Search(ResourceType.RiskAssessment) .apply { - filter(RiskAssessment.PROBABILITY) { - prefix = ParamPrefixEnum.LESSTHAN - value = BigDecimal("100.00") - } + filter( + RiskAssessment.PROBABILITY, + { + prefix = ParamPrefixEnum.LESSTHAN + value = BigDecimal("100.00") + } + ) } .getQuery() assertThat(query.query) @@ -1356,10 +1507,13 @@ class SearchTest { val query = Search(ResourceType.RiskAssessment) .apply { - filter(RiskAssessment.PROBABILITY) { - prefix = ParamPrefixEnum.LESSTHAN_OR_EQUALS - value = BigDecimal("100.00") - } + filter( + RiskAssessment.PROBABILITY, + { + prefix = ParamPrefixEnum.LESSTHAN_OR_EQUALS + value = BigDecimal("100.00") + } + ) } .getQuery() assertThat(query.query) @@ -1392,10 +1546,13 @@ class SearchTest { assertThrows(java.lang.IllegalArgumentException::class.java) { Search(ResourceType.RiskAssessment) .apply { - filter(RiskAssessment.PROBABILITY) { - prefix = ParamPrefixEnum.ENDS_BEFORE - value = BigDecimal("100") - } + filter( + RiskAssessment.PROBABILITY, + { + prefix = ParamPrefixEnum.ENDS_BEFORE + value = BigDecimal("100") + } + ) } .getQuery() } @@ -1407,10 +1564,13 @@ class SearchTest { val query = Search(ResourceType.RiskAssessment) .apply { - filter(RiskAssessment.PROBABILITY) { - prefix = ParamPrefixEnum.ENDS_BEFORE - value = BigDecimal("100.00") - } + filter( + RiskAssessment.PROBABILITY, + { + prefix = ParamPrefixEnum.ENDS_BEFORE + value = BigDecimal("100.00") + } + ) } .getQuery() assertThat(query.query) @@ -1443,10 +1603,13 @@ class SearchTest { assertThrows(java.lang.IllegalArgumentException::class.java) { Search(ResourceType.RiskAssessment) .apply { - filter(RiskAssessment.PROBABILITY) { - prefix = ParamPrefixEnum.STARTS_AFTER - value = BigDecimal("100") - } + filter( + RiskAssessment.PROBABILITY, + { + prefix = ParamPrefixEnum.STARTS_AFTER + value = BigDecimal("100") + } + ) } .getQuery() } @@ -1459,10 +1622,13 @@ class SearchTest { val query = Search(ResourceType.RiskAssessment) .apply { - filter(RiskAssessment.PROBABILITY) { - prefix = ParamPrefixEnum.STARTS_AFTER - value = BigDecimal("100.00") - } + filter( + RiskAssessment.PROBABILITY, + { + prefix = ParamPrefixEnum.STARTS_AFTER + value = BigDecimal("100.00") + } + ) } .getQuery() assertThat(query.query) @@ -1493,10 +1659,13 @@ class SearchTest { val query = Search(ResourceType.RiskAssessment) .apply { - filter(RiskAssessment.PROBABILITY) { - prefix = ParamPrefixEnum.APPROXIMATE - value = BigDecimal("100.00") - } + filter( + RiskAssessment.PROBABILITY, + { + prefix = ParamPrefixEnum.APPROXIMATE + value = BigDecimal("100.00") + } + ) } .getQuery() assertThat(query.query) @@ -1529,11 +1698,14 @@ class SearchTest { val query = Search(ResourceType.Observation) .apply { - filter(Observation.VALUE_QUANTITY) { - prefix = ParamPrefixEnum.EQUAL - unit = "g" - value = BigDecimal("5.403") - } + filter( + Observation.VALUE_QUANTITY, + { + prefix = ParamPrefixEnum.EQUAL + unit = "g" + value = BigDecimal("5.403") + } + ) } .getQuery() assertThat(query.query) @@ -1568,11 +1740,14 @@ class SearchTest { val query = Search(ResourceType.Observation) .apply { - filter(Observation.VALUE_QUANTITY) { - prefix = ParamPrefixEnum.LESSTHAN - unit = "g" - value = BigDecimal("5.403") - } + filter( + Observation.VALUE_QUANTITY, + { + prefix = ParamPrefixEnum.LESSTHAN + unit = "g" + value = BigDecimal("5.403") + } + ) } .getQuery() assertThat(query.query) @@ -1605,11 +1780,14 @@ class SearchTest { val query = Search(ResourceType.Observation) .apply { - filter(Observation.VALUE_QUANTITY) { - prefix = ParamPrefixEnum.LESSTHAN_OR_EQUALS - system = "http://unitsofmeasure.org" - value = BigDecimal("5.403") - } + filter( + Observation.VALUE_QUANTITY, + { + prefix = ParamPrefixEnum.LESSTHAN_OR_EQUALS + system = "http://unitsofmeasure.org" + value = BigDecimal("5.403") + } + ) } .getQuery() assertThat(query.query) @@ -1642,11 +1820,14 @@ class SearchTest { val query = Search(ResourceType.Observation) .apply { - filter(Observation.VALUE_QUANTITY) { - prefix = ParamPrefixEnum.GREATERTHAN - system = "http://unitsofmeasure.org" - value = BigDecimal("5.403") - } + filter( + Observation.VALUE_QUANTITY, + { + prefix = ParamPrefixEnum.GREATERTHAN + system = "http://unitsofmeasure.org" + value = BigDecimal("5.403") + } + ) } .getQuery() assertThat(query.query) @@ -1679,10 +1860,13 @@ class SearchTest { val query = Search(ResourceType.Observation) .apply { - filter(Observation.VALUE_QUANTITY) { - prefix = ParamPrefixEnum.GREATERTHAN_OR_EQUALS - value = BigDecimal("5.403") - } + filter( + Observation.VALUE_QUANTITY, + { + prefix = ParamPrefixEnum.GREATERTHAN_OR_EQUALS + value = BigDecimal("5.403") + } + ) } .getQuery() assertThat(query.query) @@ -1714,10 +1898,13 @@ class SearchTest { val query = Search(ResourceType.Observation) .apply { - filter(Observation.VALUE_QUANTITY) { - prefix = ParamPrefixEnum.NOT_EQUAL - value = BigDecimal("5.403") - } + filter( + Observation.VALUE_QUANTITY, + { + prefix = ParamPrefixEnum.NOT_EQUAL + value = BigDecimal("5.403") + } + ) } .getQuery() assertThat(query.query) @@ -1750,10 +1937,13 @@ class SearchTest { val query = Search(ResourceType.Observation) .apply { - filter(Observation.VALUE_QUANTITY) { - prefix = ParamPrefixEnum.STARTS_AFTER - value = BigDecimal("5.403") - } + filter( + Observation.VALUE_QUANTITY, + { + prefix = ParamPrefixEnum.STARTS_AFTER + value = BigDecimal("5.403") + } + ) } .getQuery() assertThat(query.query) @@ -1785,10 +1975,13 @@ class SearchTest { val query = Search(ResourceType.Observation) .apply { - filter(Observation.VALUE_QUANTITY) { - prefix = ParamPrefixEnum.ENDS_BEFORE - value = BigDecimal("5.403") - } + filter( + Observation.VALUE_QUANTITY, + { + prefix = ParamPrefixEnum.ENDS_BEFORE + value = BigDecimal("5.403") + } + ) } .getQuery() assertThat(query.query) @@ -1820,12 +2013,15 @@ class SearchTest { val query = Search(ResourceType.Observation) .apply { - filter(Observation.VALUE_QUANTITY) { - prefix = ParamPrefixEnum.EQUAL - value = BigDecimal("5403") - system = "http://unitsofmeasure.org" - unit = "mg" - } + filter( + Observation.VALUE_QUANTITY, + { + prefix = ParamPrefixEnum.EQUAL + value = BigDecimal("5403") + system = "http://unitsofmeasure.org" + unit = "mg" + } + ) } .getQuery() assertThat(query.query) @@ -1864,7 +2060,10 @@ class SearchTest { Search(ResourceType.Patient) .apply { has(Condition.SUBJECT) { - filter(Condition.CODE, Coding("http://snomed.info/sct", "44054006", "Diabetes")) + filter( + Condition.CODE, + { value = of(Coding("http://snomed.info/sct", "44054006", "Diabetes")) } + ) } } .getQuery() @@ -1910,23 +2109,31 @@ class SearchTest { has(Immunization.PATIENT) { filter( Immunization.VACCINE_CODE, - Coding( - "http://hl7.org/fhir/sid/cvx", - "140", - "Influenza, seasonal, injectable, preservative free" - ) + { + value = + of( + Coding( + "http://hl7.org/fhir/sid/cvx", + "140", + "Influenza, seasonal, injectable, preservative free" + ) + ) + } ) // Follow Immunization.ImmunizationStatus filter( Immunization.STATUS, - Coding("http://hl7.org/fhir/event-status", "completed", "Body Weight") + { value = of(Coding("http://hl7.org/fhir/event-status", "completed", "Body Weight")) } ) } - filter(Patient.ADDRESS_COUNTRY) { - modifier = StringFilterModifier.MATCHES_EXACTLY - value = "IN" - } + filter( + Patient.ADDRESS_COUNTRY, + { + modifier = StringFilterModifier.MATCHES_EXACTLY + value = "IN" + } + ) } .getQuery() @@ -1984,12 +2191,15 @@ class SearchTest { Search(ResourceType.Patient) .apply { has(Condition.SUBJECT) { - filter(Condition.CODE, Coding("http://snomed.info/sct", "44054006", "Diabetes")) + filter( + Condition.CODE, + { value = of(Coding("http://snomed.info/sct", "44054006", "Diabetes")) } + ) } has(Condition.SUBJECT) { filter( Condition.CODE, - Coding("http://snomed.info/sct", "827069000", "Hypertension stage 1") + { value = of(Coding("http://snomed.info/sct", "827069000", "Hypertension stage 1")) } ) } } @@ -2042,4 +2252,88 @@ class SearchTest { ) ) } + + @Test + fun search_patient_single_search_param_multiple_values_disjunction() { + val query = + Search(ResourceType.Patient) + .apply { + filter( + Patient.GIVEN, + { + value = "John" + modifier = StringFilterModifier.MATCHES_EXACTLY + }, + { + value = "Jane" + modifier = StringFilterModifier.MATCHES_EXACTLY + }, + operation = Operation.OR + ) + } + .getQuery() + + assertThat(query.query) + .isEqualTo( + """ + SELECT a.serializedResource + FROM ResourceEntity a + WHERE a.resourceType = ? + AND a.resourceId IN ( + SELECT resourceId FROM StringIndexEntity + WHERE resourceType = ? AND index_name = ? AND index_value = ? + UNION + SELECT resourceId FROM StringIndexEntity + WHERE resourceType = ? AND index_name = ? AND index_value = ? + ) + """.trimIndent() + ) + + assertThat(query.args) + .isEqualTo(listOf("Patient", "Patient", "given", "John", "Patient", "given", "Jane")) + } + + @Test + fun search_patient_single_search_param_multiple_params_disjunction() { + val query = + Search(ResourceType.Patient) + .apply { + filter( + Patient.GIVEN, + { + value = "John" + modifier = StringFilterModifier.MATCHES_EXACTLY + } + ) + + filter( + Patient.GIVEN, + { + value = "Jane" + modifier = StringFilterModifier.MATCHES_EXACTLY + } + ) + operation = Operation.OR + } + .getQuery() + + assertThat(query.query) + .isEqualTo( + """ + SELECT a.serializedResource + FROM ResourceEntity a + WHERE a.resourceType = ? + AND a.resourceId IN ( + SELECT resourceId FROM StringIndexEntity + WHERE resourceType = ? AND index_name = ? AND index_value = ? + UNION + SELECT resourceId FROM StringIndexEntity + WHERE resourceType = ? AND index_name = ? AND index_value = ? + ) + """.trimIndent() + ) + + assertThat(query.args) + .isEqualTo(listOf("Patient", "Patient", "given", "John", "Patient", "given", "Jane")) + } } diff --git a/reference/src/main/java/com/google/android/fhir/reference/PatientDetailsViewModel.kt b/reference/src/main/java/com/google/android/fhir/reference/PatientDetailsViewModel.kt index 99cba06749..6876a38b7f 100644 --- a/reference/src/main/java/com/google/android/fhir/reference/PatientDetailsViewModel.kt +++ b/reference/src/main/java/com/google/android/fhir/reference/PatientDetailsViewModel.kt @@ -62,7 +62,7 @@ class PatientDetailsViewModel( private suspend fun getPatientObservations(): List { val observations: MutableList = mutableListOf() fhirEngine - .search { filter(Observation.SUBJECT) { value = "Patient/$patientId" } } + .search { filter(Observation.SUBJECT, { value = "Patient/$patientId" }) } .take(MAX_RESOURCE_COUNT) .map { createObservationItem(it, getApplication().resources) } .let { observations.addAll(it) } @@ -72,7 +72,7 @@ class PatientDetailsViewModel( private suspend fun getPatientConditions(): List { val conditions: MutableList = mutableListOf() fhirEngine - .search { filter(Condition.SUBJECT) { value = "Patient/$patientId" } } + .search { filter(Condition.SUBJECT, { value = "Patient/$patientId" }) } .take(MAX_RESOURCE_COUNT) .map { createConditionItem(it, getApplication().resources) } .let { conditions.addAll(it) } @@ -156,7 +156,7 @@ class PatientDetailsViewModel( private suspend fun getPatientRiskAssessment(): RiskAssessmentItem { val riskAssessment = fhirEngine - .search { filter(RiskAssessment.SUBJECT) { value = "Patient/$patientId" } } + .search { filter(RiskAssessment.SUBJECT, { value = "Patient/$patientId" }) } .filter { it.hasOccurrence() } .sortedByDescending { it.occurrenceDateTimeType.value } .firstOrNull() diff --git a/reference/src/main/java/com/google/android/fhir/reference/PatientListViewModel.kt b/reference/src/main/java/com/google/android/fhir/reference/PatientListViewModel.kt index 5b04138ca5..f225420696 100644 --- a/reference/src/main/java/com/google/android/fhir/reference/PatientListViewModel.kt +++ b/reference/src/main/java/com/google/android/fhir/reference/PatientListViewModel.kt @@ -65,10 +65,13 @@ class PatientListViewModel(application: Application, private val fhirEngine: Fhi fhirEngine .search { if (nameQuery.isNotEmpty()) - filter(Patient.NAME) { - modifier = StringFilterModifier.CONTAINS - value = nameQuery - } + filter( + Patient.NAME, + { + modifier = StringFilterModifier.CONTAINS + value = nameQuery + } + ) filterCity(this) sort(Patient.GIVEN, Order.ASCENDING) count = 100 @@ -88,10 +91,13 @@ class PatientListViewModel(application: Application, private val fhirEngine: Fhi } private fun filterCity(search: Search) { - search.filter(Patient.ADDRESS_CITY) { - modifier = StringFilterModifier.MATCHES_EXACTLY - value = "NAIROBI" - } + search.filter( + Patient.ADDRESS_CITY, + { + modifier = StringFilterModifier.MATCHES_EXACTLY + value = "NAIROBI" + } + ) } private suspend fun getRiskAssessments(): Map {