From 8628708f0e00de16257ce95c217d308011a38fd7 Mon Sep 17 00:00:00 2001 From: aditya-07 Date: Tue, 26 Dec 2023 16:29:16 +0530 Subject: [PATCH] Allow sorting in [rev]include results. (#2200) * Allow sorting in [rev]include results * Review comment changes * Fixed failing tests * Lint fails build because of the use of restricted api from package. So, supressing it as couldn't find a workaround for the api * Spotless * Updated failing test * Review comments: Fixed tests * Review comments: Refactored db code to separate out search functions for forward and rev include * Lint fails build because of the use of restricted api UUIDUtil.convertUUIDToByte from androidx.room package. So, supressing it as couldn't find a workaround for the api * Review Comments: Unified the usage of getSortOrder * Review comments: Updated kdoc --- .../android/fhir/db/impl/DatabaseImplTest.kt | 604 ++++++++++++++---- .../com/google/android/fhir/FhirEngine.kt | 40 +- .../com/google/android/fhir/db/Database.kt | 14 +- .../android/fhir/db/impl/DatabaseImpl.kt | 38 +- .../android/fhir/db/impl/dao/ResourceDao.kt | 52 +- .../google/android/fhir/search/MoreSearch.kt | 283 ++++---- .../android/fhir/search/NestedSearch.kt | 18 +- .../search/NumberSearchParameterizedTest.kt | 2 +- .../google/android/fhir/search/SearchTest.kt | 602 +++++++++++++++-- 9 files changed, 1287 insertions(+), 366 deletions(-) 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 3eb337b209..25efca3a2d 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 @@ -25,6 +25,7 @@ import com.google.android.fhir.DateProvider import com.google.android.fhir.FhirServices import com.google.android.fhir.LocalChange import com.google.android.fhir.LocalChangeToken +import com.google.android.fhir.SearchParamName import com.google.android.fhir.SearchResult import com.google.android.fhir.db.Database import com.google.android.fhir.db.ResourceNotFoundException @@ -47,6 +48,7 @@ import com.google.android.fhir.testing.assertResourceEquals import com.google.android.fhir.testing.readFromFile import com.google.android.fhir.testing.readJsonArrayFromFile import com.google.android.fhir.versionId +import com.google.common.truth.Correspondence import com.google.common.truth.Truth.assertThat import java.math.BigDecimal import java.time.Instant @@ -769,7 +771,7 @@ class DatabaseImplTest { .getQuery(), ) - val ids = results.map { it.id } + val ids = results.map { it.resource.id } assertThat(ids) .containsExactly("RiskAssessment/$largerId", "RiskAssessment/$smallerId") .inOrder() @@ -799,7 +801,7 @@ class DatabaseImplTest { Search(ResourceType.Patient).apply { filter(Patient.GIVEN, { value = "eve" }) }.getQuery(), ) - assertThat(result.single().id).isEqualTo("Patient/${patient.id}") + assertThat(result.single().resource.id).isEqualTo("Patient/${patient.id}") } @Test @@ -841,7 +843,7 @@ class DatabaseImplTest { .getQuery(), ) - assertThat(result.single().id).isEqualTo("Patient/${patient.id}") + assertThat(result.single().resource.id).isEqualTo("Patient/${patient.id}") } @Test @@ -894,7 +896,7 @@ class DatabaseImplTest { .getQuery(), ) - assertThat(result.single().id).isEqualTo("Patient/${patient.id}") + assertThat(result.single().resource.id).isEqualTo("Patient/${patient.id}") } @Test @@ -949,7 +951,7 @@ class DatabaseImplTest { .getQuery(), ) - assertThat(result.single().id).isEqualTo("RiskAssessment/${riskAssessment.id}") + assertThat(result.single().resource.id).isEqualTo("RiskAssessment/${riskAssessment.id}") } @Test @@ -1006,7 +1008,7 @@ class DatabaseImplTest { } .getQuery(), ) - assertThat(result.single().id).isEqualTo("RiskAssessment/${riskAssessment.id}") + assertThat(result.single().resource.id).isEqualTo("RiskAssessment/${riskAssessment.id}") } @Test @@ -1064,7 +1066,7 @@ class DatabaseImplTest { .getQuery(), ) - assertThat(result.single().id).isEqualTo("RiskAssessment/${riskAssessment.id}") + assertThat(result.single().resource.id).isEqualTo("RiskAssessment/${riskAssessment.id}") } @Test @@ -1122,7 +1124,7 @@ class DatabaseImplTest { .getQuery(), ) - assertThat(result.single().id).isEqualTo("RiskAssessment/${riskAssessment.id}") + assertThat(result.single().resource.id).isEqualTo("RiskAssessment/${riskAssessment.id}") } @Test @@ -1180,7 +1182,7 @@ class DatabaseImplTest { .getQuery(), ) - assertThat(result.single().id).isEqualTo("RiskAssessment/${riskAssessment.id}") + assertThat(result.single().resource.id).isEqualTo("RiskAssessment/${riskAssessment.id}") } @Test @@ -1237,7 +1239,7 @@ class DatabaseImplTest { } .getQuery(), ) - assertThat(result.single().id).isEqualTo("RiskAssessment/${riskAssessment.id}") + assertThat(result.single().resource.id).isEqualTo("RiskAssessment/${riskAssessment.id}") } @Test @@ -1295,7 +1297,7 @@ class DatabaseImplTest { .getQuery(), ) - assertThat(result.single().id).isEqualTo("RiskAssessment/${riskAssessment.id}") + assertThat(result.single().resource.id).isEqualTo("RiskAssessment/${riskAssessment.id}") } @Test @@ -1353,7 +1355,7 @@ class DatabaseImplTest { .getQuery(), ) - assertThat(result.single().id).isEqualTo("RiskAssessment/${riskAssessment.id}") + assertThat(result.single().resource.id).isEqualTo("RiskAssessment/${riskAssessment.id}") } @Test @@ -1411,7 +1413,7 @@ class DatabaseImplTest { .getQuery(), ) - assertThat(result.single().id).isEqualTo("RiskAssessment/${riskAssessment.id}") + assertThat(result.single().resource.id).isEqualTo("RiskAssessment/${riskAssessment.id}") } @Test @@ -1466,7 +1468,7 @@ class DatabaseImplTest { } .getQuery(), ) - assertThat(result.single().id).isEqualTo("Patient/1") + assertThat(result.single().resource.id).isEqualTo("Patient/1") } @Test @@ -1518,7 +1520,7 @@ class DatabaseImplTest { } .getQuery(), ) - assertThat(result.single().id).isEqualTo("Patient/1") + assertThat(result.single().resource.id).isEqualTo("Patient/1") } @Test @@ -1569,7 +1571,7 @@ class DatabaseImplTest { } .getQuery(), ) - assertThat(result.single().id).isEqualTo("Patient/1") + assertThat(result.single().resource.id).isEqualTo("Patient/1") } @Test @@ -1619,7 +1621,7 @@ class DatabaseImplTest { } .getQuery(), ) - assertThat(result.single().id).isEqualTo("Patient/1") + assertThat(result.single().resource.id).isEqualTo("Patient/1") } @Test @@ -1669,7 +1671,7 @@ class DatabaseImplTest { } .getQuery(), ) - assertThat(result.single().id).isEqualTo("Patient/1") + assertThat(result.single().resource.id).isEqualTo("Patient/1") } @Test @@ -1719,7 +1721,7 @@ class DatabaseImplTest { } .getQuery(), ) - assertThat(result.single().id).isEqualTo("Patient/1") + assertThat(result.single().resource.id).isEqualTo("Patient/1") } @Test @@ -1769,7 +1771,7 @@ class DatabaseImplTest { } .getQuery(), ) - assertThat(result.single().id).isEqualTo("Patient/1") + assertThat(result.single().resource.id).isEqualTo("Patient/1") } @Test @@ -1819,7 +1821,7 @@ class DatabaseImplTest { } .getQuery(), ) - assertThat(result.single().id).isEqualTo("Patient/1") + assertThat(result.single().resource.id).isEqualTo("Patient/1") } @Test @@ -1869,7 +1871,7 @@ class DatabaseImplTest { } .getQuery(), ) - assertThat(result.single().id).isEqualTo("Patient/1") + assertThat(result.single().resource.id).isEqualTo("Patient/1") } @Test @@ -1919,7 +1921,7 @@ class DatabaseImplTest { } .getQuery(), ) - assertThat(result.single().id).isEqualTo("Patient/1") + assertThat(result.single().resource.id).isEqualTo("Patient/1") } @Test @@ -1976,7 +1978,7 @@ class DatabaseImplTest { } .getQuery(), ) - assertThat(result.single().id).isEqualTo("Observation/1") + assertThat(result.single().resource.id).isEqualTo("Observation/1") } @Test @@ -2040,7 +2042,7 @@ class DatabaseImplTest { } .getQuery(), ) - assertThat(result.single().id).isEqualTo("Observation/1") + assertThat(result.single().resource.id).isEqualTo("Observation/1") } @Test @@ -2104,7 +2106,7 @@ class DatabaseImplTest { } .getQuery(), ) - assertThat(result.single().id).isEqualTo("Observation/1") + assertThat(result.single().resource.id).isEqualTo("Observation/1") } @Test @@ -2168,7 +2170,7 @@ class DatabaseImplTest { } .getQuery(), ) - assertThat(result.single().id).isEqualTo("Observation/1") + assertThat(result.single().resource.id).isEqualTo("Observation/1") } @Test @@ -2232,7 +2234,7 @@ class DatabaseImplTest { } .getQuery(), ) - assertThat(result.single().id).isEqualTo("Observation/1") + assertThat(result.single().resource.id).isEqualTo("Observation/1") } @Test @@ -2296,7 +2298,7 @@ class DatabaseImplTest { } .getQuery(), ) - assertThat(result.single().id).isEqualTo("Observation/1") + assertThat(result.single().resource.id).isEqualTo("Observation/1") } @Test @@ -2360,7 +2362,7 @@ class DatabaseImplTest { } .getQuery(), ) - assertThat(result.single().id).isEqualTo("Observation/1") + assertThat(result.single().resource.id).isEqualTo("Observation/1") } @Test @@ -2424,7 +2426,7 @@ class DatabaseImplTest { } .getQuery(), ) - assertThat(result.single().id).isEqualTo("Observation/1") + assertThat(result.single().resource.id).isEqualTo("Observation/1") } @Test @@ -2441,7 +2443,7 @@ class DatabaseImplTest { } .getQuery(), ) - assertThat(result.filter { it.id == patient.id }).hasSize(1) + assertThat(result.filter { it.resource.id == patient.id }).hasSize(1) } @Test @@ -2504,7 +2506,7 @@ class DatabaseImplTest { } .getQuery(), ) - assertThat(result.map { it.logicalId }).containsExactly("100").inOrder() + assertThat(result.map { it.resource.logicalId }).containsExactly("100").inOrder() } @Test @@ -2550,7 +2552,7 @@ class DatabaseImplTest { } .getQuery(), ) - assertThat(result.map { it.logicalId }).containsExactly("100").inOrder() + assertThat(result.map { it.resource.logicalId }).containsExactly("100").inOrder() } @Test @@ -2576,7 +2578,7 @@ class DatabaseImplTest { .apply { sort(Patient.BIRTHDATE, Order.DESCENDING) } .getQuery(), ) - .map { it.id }, + .map { it.resource.id }, ) .containsExactly("Patient/younger-patient", "Patient/older-patient", "Patient/test_patient_1") } @@ -2604,7 +2606,7 @@ class DatabaseImplTest { .apply { sort(Patient.BIRTHDATE, Order.ASCENDING) } .getQuery(), ) - .map { it.id }, + .map { it.resource.id }, ) .containsExactly("Patient/test_patient_1", "Patient/older-patient", "Patient/younger-patient") } @@ -2693,7 +2695,7 @@ class DatabaseImplTest { .getQuery(), ) - assertThat(result.map { it.vaccineCode.codingFirstRep.code }) + assertThat(result.map { it.resource.vaccineCode.codingFirstRep.code }) .containsExactly("XM1NL1", "XM5DF6") .inOrder() } @@ -2768,7 +2770,7 @@ class DatabaseImplTest { .getQuery(), ) - assertThat(result.map { it.vaccineCode.codingFirstRep.code }) + assertThat(result.map { it.resource.vaccineCode.codingFirstRep.code }) .containsExactly("XM1NL1", "XM5DF6") .inOrder() } @@ -2860,7 +2862,7 @@ class DatabaseImplTest { .getQuery(), ) - assertThat(result.map { it.nameFirstRep.nameAsSingleString }) + assertThat(result.map { it.resource.nameFirstRep.nameAsSingleString }) .containsExactly("John Doe", "Jane Doe", "John Roe", "Jane Roe") .inOrder() } @@ -2924,7 +2926,7 @@ class DatabaseImplTest { .getQuery(), ) - assertThat(result.map { it.nameFirstRep.nameAsSingleString }).contains("Darcy Smith") + assertThat(result.map { it.resource.nameFirstRep.nameAsSingleString }).contains("Darcy Smith") } @Test @@ -2985,7 +2987,7 @@ class DatabaseImplTest { .getQuery(), ) - assertThat(result.map { it.nameFirstRep.nameAsSingleString }).contains("Darcy Smith") + assertThat(result.map { it.resource.nameFirstRep.nameAsSingleString }).contains("Darcy Smith") } @Test @@ -3010,7 +3012,7 @@ class DatabaseImplTest { .getQuery(), ) - assertThat(result.map { it.logicalId }) + assertThat(result.map { it.resource.logicalId }) .containsAtLeast("patient-test-002", "patient-test-003", "patient-test-001") .inOrder() } @@ -3100,20 +3102,21 @@ class DatabaseImplTest { .execute(database) assertThat(result) - .isEqualTo( - listOf( - SearchResult( - patient01, - included = mapOf(Patient.GENERAL_PRACTITIONER.paramName to listOf(gp01)), - revIncluded = null, - ), - SearchResult( - patient02, - included = mapOf(Patient.GENERAL_PRACTITIONER.paramName to listOf(gp03)), - revIncluded = null, - ), + .comparingElementsUsing(SearchResultCorrespondence) + .displayingDiffsPairedBy { it.resource.logicalId } + .containsExactly( + SearchResult( + patient01, + included = mapOf(Patient.GENERAL_PRACTITIONER.paramName to listOf(gp01)), + revIncluded = null, + ), + SearchResult( + patient02, + included = mapOf(Patient.GENERAL_PRACTITIONER.paramName to listOf(gp03)), + revIncluded = null, ), ) + .inOrder() } @Test @@ -3190,22 +3193,23 @@ class DatabaseImplTest { .execute(database) assertThat(result) - .isEqualTo( - listOf( - SearchResult( - patient01, - included = null, - revIncluded = - mapOf((ResourceType.Condition to Condition.SUBJECT.paramName) to listOf(con1)), - ), - SearchResult( - patient02, - included = null, - revIncluded = - mapOf((ResourceType.Condition to Condition.SUBJECT.paramName) to listOf(con3)), - ), + .comparingElementsUsing(SearchResultCorrespondence) + .displayingDiffsPairedBy { it.resource.logicalId } + .containsExactly( + SearchResult( + patient01, + included = null, + revIncluded = + mapOf((ResourceType.Condition to Condition.SUBJECT.paramName) to listOf(con1)), + ), + SearchResult( + patient02, + included = null, + revIncluded = + mapOf((ResourceType.Condition to Condition.SUBJECT.paramName) to listOf(con3)), ), ) + .inOrder() } @Test @@ -3509,46 +3513,347 @@ class DatabaseImplTest { .execute(database) assertThat(result) - .isEqualTo( - listOf( - SearchResult( - resources["pa-01"]!!, - mapOf( - "general-practitioner" to listOf(resources["gp-01"]!!, resources["gp-02"]!!), - "organization" to listOf(resources["org-01"]!!), - ), - mapOf( - Pair(ResourceType.Condition, "subject") to - listOf(resources["con-01-pa-01"]!!, resources["con-03-pa-01"]!!), - Pair(ResourceType.Encounter, "subject") to - listOf(resources["en-01-pa-01"]!!, resources["en-02-pa-01"]!!), - ), + .comparingElementsUsing(SearchResultCorrespondence) + .displayingDiffsPairedBy { it.resource.logicalId } + .containsExactly( + SearchResult( + resources["pa-01"]!!, + mapOf( + "general-practitioner" to listOf(resources["gp-01"]!!, resources["gp-02"]!!), + "organization" to listOf(resources["org-01"]!!), ), - SearchResult( - resources["pa-02"]!!, - mapOf( - "general-practitioner" to listOf(resources["gp-01"]!!, resources["gp-02"]!!), - "organization" to listOf(resources["org-02"]!!), - ), - mapOf( - Pair(ResourceType.Condition, "subject") to - listOf(resources["con-01-pa-02"]!!, resources["con-03-pa-02"]!!), - Pair(ResourceType.Encounter, "subject") to - listOf(resources["en-01-pa-02"]!!, resources["en-02-pa-02"]!!), - ), + mapOf( + Pair(ResourceType.Condition, "subject") to + listOf(resources["con-01-pa-01"]!!, resources["con-03-pa-01"]!!), + Pair(ResourceType.Encounter, "subject") to + listOf(resources["en-01-pa-01"]!!, resources["en-02-pa-01"]!!), + ), + ), + SearchResult( + resources["pa-02"]!!, + mapOf( + "general-practitioner" to listOf(resources["gp-01"]!!, resources["gp-02"]!!), + "organization" to listOf(resources["org-02"]!!), ), - SearchResult( - resources["pa-03"]!!, + mapOf( + Pair(ResourceType.Condition, "subject") to + listOf(resources["con-01-pa-02"]!!, resources["con-03-pa-02"]!!), + Pair(ResourceType.Encounter, "subject") to + listOf(resources["en-01-pa-02"]!!, resources["en-02-pa-02"]!!), + ), + ), + SearchResult( + resources["pa-03"]!!, + mapOf( + "general-practitioner" to listOf(resources["gp-01"]!!, resources["gp-02"]!!), + ), + mapOf( + Pair(ResourceType.Condition, "subject") to + listOf(resources["con-01-pa-03"]!!, resources["con-03-pa-03"]!!), + Pair(ResourceType.Encounter, "subject") to + listOf(resources["en-01-pa-03"]!!, resources["en-02-pa-03"]!!), + ), + ), + ) + .inOrder() + } + + @Test + fun search_patient_and_include_practitioners_sorted_by_family_descending(): Unit = runBlocking { + val patient01 = + Patient().apply { + id = "pa-01" + addName( + HumanName().apply { + addGiven("James") + family = "Gorden" + }, + ) + addGeneralPractitioner(Reference("Practitioner/gp-01")) + addGeneralPractitioner(Reference("Practitioner/gp-02")) + addGeneralPractitioner(Reference("Practitioner/gp-04")) + managingOrganization = Reference("Organization/org-01") + } + + val patient02 = + Patient().apply { + id = "pa-02" + addName( + HumanName().apply { + addGiven("James") + family = "Bond" + }, + ) + addGeneralPractitioner(Reference("Practitioner/gp-02")) + addGeneralPractitioner(Reference("Practitioner/gp-03")) + addGeneralPractitioner(Reference("Practitioner/gp-04")) + managingOrganization = Reference("Organization/org-03") + } + val patients = listOf(patient01, patient02) + + val gp01 = + Practitioner().apply { + id = "gp-01" + addName( + HumanName().apply { + family = "Practitioner-01" + addGiven("General-01") + }, + ) + active = true + } + val gp02 = + Practitioner().apply { + id = "gp-02" + addName( + HumanName().apply { + family = "Practitioner-02" + addGiven("General-02") + }, + ) + active = true + } + val gp03 = + Practitioner().apply { + id = "gp-03" + addName( + HumanName().apply { + family = "Practitioner-03" + addGiven("General-03") + }, + ) + active = true + } + + val gp04 = + Practitioner().apply { + id = "gp-04" + addName( + HumanName().apply { + family = "Practitioner-04" + addGiven("General-04") + }, + ) + active = false + } + + val practitioners = listOf(gp01, gp02, gp03, gp04) + + database.insertRemote(*(patients + practitioners).toTypedArray()) + + val result = + Search(ResourceType.Patient) + .apply { + filter( + Patient.GIVEN, + { + value = "James" + modifier = StringFilterModifier.MATCHES_EXACTLY + }, + ) + + include(Patient.GENERAL_PRACTITIONER) { + filter(Practitioner.ACTIVE, { value = of(true) }) + sort(Practitioner.FAMILY, Order.DESCENDING) + } + } + .execute(database) + + assertThat(result) + .comparingElementsUsing(SearchResultCorrespondence) + .displayingDiffsPairedBy { it.resource.logicalId } + .containsExactly( + SearchResult( + patient01, + included = mapOf(Patient.GENERAL_PRACTITIONER.paramName to listOf(gp02, gp01)), + revIncluded = null, + ), + SearchResult( + patient02, + included = mapOf(Patient.GENERAL_PRACTITIONER.paramName to listOf(gp03, gp02)), + revIncluded = null, + ), + ) + .inOrder() + } + + @Test + fun search_patient_and_revInclude_encounters_sorted_by_date_descending(): Unit = runBlocking { + val patient01 = + Patient().apply { + id = "pa-01" + addName( + HumanName().apply { + addGiven("James") + family = "Gorden" + }, + ) + addGeneralPractitioner(Reference("Practitioner/gp-01")) + } + + val patient02 = + Patient().apply { + id = "pa-02" + addName( + HumanName().apply { + addGiven("James") + family = "Bond" + }, + ) + addGeneralPractitioner(Reference("Practitioner/gp-02")) + } + val patients = listOf(patient01, patient02) + + // encounters for patient 1 + val enc1_1 = + Encounter().apply { + id = "enc1-01" + subject = Reference("Patient/pa-01") + status = Encounter.EncounterStatus.ARRIVED + period = + Period().apply { + start = DateType(2010, 1, 1).value + end = DateType(2010, 1, 2).value + } + } + val enc1_2 = + Encounter().apply { + id = "enc1-02" + subject = Reference("Patient/pa-01") + status = Encounter.EncounterStatus.CANCELLED + period = + Period().apply { + start = DateType(2010, 2, 1).value + end = DateType(2010, 2, 2).value + } + } + + val enc1_3 = + Encounter().apply { + id = "enc1-03" + subject = Reference("Patient/pa-01") + status = Encounter.EncounterStatus.ARRIVED + period = + Period().apply { + start = DateType(2010, 3, 1).value + end = DateType(2010, 3, 2).value + } + } + + val enc1_4 = + Encounter().apply { + id = "enc1-04" + subject = Reference("Patient/pa-01") + status = Encounter.EncounterStatus.ARRIVED + period = + Period().apply { + start = DateType(2010, 4, 1).value + end = DateType(2010, 4, 2).value + } + } + + // encounters for patient 2 + val enc2_1 = + Encounter().apply { + id = "enc2-01" + subject = Reference("Patient/pa-02") + status = Encounter.EncounterStatus.ARRIVED + period = + Period().apply { + start = DateType(2020, 1, 1).value + end = DateType(2020, 1, 2).value + } + } + val enc2_2 = + Encounter().apply { + id = "enc2-02" + subject = Reference("Patient/pa-02") + status = Encounter.EncounterStatus.CANCELLED + period = + Period().apply { + start = DateType(2020, 2, 1).value + end = DateType(2020, 2, 2).value + } + } + + val enc2_3 = + Encounter().apply { + id = "enc2-03" + subject = Reference("Patient/pa-02") + status = Encounter.EncounterStatus.ARRIVED + period = + Period().apply { + start = DateType(2020, 3, 1).value + end = DateType(2020, 3, 2).value + } + } + + val enc2_4 = + Encounter().apply { + id = "enc2-04" + subject = Reference("Patient/pa-02") + status = Encounter.EncounterStatus.ARRIVED + period = + Period().apply { + start = DateType(2020, 4, 1).value + end = DateType(2020, 4, 2).value + } + } + + val encounters = listOf(enc1_1, enc1_2, enc1_3, enc1_4, enc2_1, enc2_2, enc2_3, enc2_4) + database.insertRemote(*(patients + encounters).toTypedArray()) + + val result = + Search(ResourceType.Patient) + .apply { + filter( + Patient.GIVEN, + { + value = "James" + modifier = StringFilterModifier.MATCHES_EXACTLY + }, + ) + + revInclude(Encounter.SUBJECT) { + filter( + Encounter.STATUS, + { + value = + of( + Coding( + "http://hl7.org/fhir/encounter-status", + Encounter.EncounterStatus.ARRIVED.toCode(), + "", + ), + ) + }, + ) + sort(Encounter.DATE, Order.DESCENDING) + } + } + .execute(database) + + assertThat(result) + .comparingElementsUsing(SearchResultCorrespondence) + .displayingDiffsPairedBy { it.resource.logicalId } + .containsExactly( + SearchResult( + patient01, + included = null, + revIncluded = mapOf( - "general-practitioner" to listOf(resources["gp-01"]!!, resources["gp-02"]!!), + (ResourceType.Encounter to Encounter.SUBJECT.paramName) to + listOf(enc1_4, enc1_3, enc1_1), ), + ), + SearchResult( + patient02, + included = null, + revIncluded = mapOf( - Pair(ResourceType.Condition, "subject") to - listOf(resources["con-01-pa-03"]!!, resources["con-03-pa-03"]!!), - Pair(ResourceType.Encounter, "subject") to - listOf(resources["en-01-pa-03"]!!, resources["en-02-pa-03"]!!), + (ResourceType.Encounter to Encounter.SUBJECT.paramName) to + listOf(enc2_4, enc2_3, enc2_1), ), - ), ), ) } @@ -3762,16 +4067,18 @@ class DatabaseImplTest { // verify that Observation is searchable i.e. ReferenceIndex is updated with new patient ID // reference val searchedObservations = - database.search( - Search(ResourceType.Observation) - .apply { - filter( - Observation.SUBJECT, - { value = "Patient/$remotelyCreatedPatientResourceId" }, - ) - } - .getQuery(), - ) + database + .search( + Search(ResourceType.Observation) + .apply { + filter( + Observation.SUBJECT, + { value = "Patient/$remotelyCreatedPatientResourceId" }, + ) + } + .getQuery(), + ) + .map { it.resource } assertThat(searchedObservations.size).isEqualTo(1) assertThat(searchedObservations[0].logicalId).isEqualTo(locallyCreatedObservationResourceId) } @@ -3797,5 +4104,78 @@ class DatabaseImplTest { } @JvmStatic @Parameters(name = "encrypted={0}") fun data(): Array = arrayOf(true, false) + + /** + * [Correspondence] to provide a custom [equalityCheck] for the [SearchResult]s. Also provides a + * custom diff formatting for failing cases. + */ + val SearchResultCorrespondence: Correspondence, SearchResult> = + Correspondence.from, SearchResult>( + ::equalityCheck, + "is shallow equals (by logical id comparison) to the ", + ) + .formattingDiffsUsing(::formatDiff) + + private fun equalityCheck( + actual: SearchResult, + expected: SearchResult, + ): Boolean { + return equalsShallow(actual.resource, expected.resource) && + equalsShallow(actual.included, expected.included) && + equalsShallow(actual.revIncluded, expected.revIncluded) + } + + private fun equalsShallow(first: Resource, second: Resource) = + first.resourceType == second.resourceType && first.logicalId == second.logicalId + + private fun equalsShallow(first: List, second: List) = + first.size == second.size && + first.asSequence().zip(second.asSequence()).all { (x, y) -> equalsShallow(x, y) } + + private fun equalsShallow( + first: Map>?, + second: Map>?, + ) = + if (first != null && second != null && first.size == second.size) { + first.entries.asSequence().zip(second.entries.asSequence()).all { (x, y) -> + x.key == y.key && equalsShallow(x.value, y.value) + } + } else { + first?.size == second?.size + } + + @JvmName("equalsShallowRevInclude") + private fun equalsShallow( + first: Map, List>?, + second: Map, List>?, + ) = + if (first != null && second != null && first.size == second.size) { + first.entries.asSequence().zip(second.entries.asSequence()).all { (x, y) -> + x.key == y.key && equalsShallow(x.value, y.value) + } + } else { + first?.size == second?.size + } + + /** + * Ideally, this functions should highlight the diff between the [actual] and [expected]. But, + * we are just highlighting the ids of resources contained in the [SearchResult]. + */ + private fun formatDiff( + actual: SearchResult, + expected: SearchResult, + ): String { + return "Expected : ${expected.asString()} \n Actual ${actual.asString()}" + } + + private fun SearchResult.asString(): String { + return "SearchResult[ resource: " + + resource.logicalId + + ", Included : " + + included?.map { it.key + ": " + it.value.joinToString { it.logicalId } } + + ", RevIncluded : " + + revIncluded?.map { it.key.toString() + ": " + it.value.joinToString { it.logicalId } } + + "]" + } } } diff --git a/engine/src/main/java/com/google/android/fhir/FhirEngine.kt b/engine/src/main/java/com/google/android/fhir/FhirEngine.kt index cc0a971721..a006ab2c29 100644 --- a/engine/src/main/java/com/google/android/fhir/FhirEngine.kt +++ b/engine/src/main/java/com/google/android/fhir/FhirEngine.kt @@ -153,42 +153,4 @@ data class SearchResult( val included: Map>?, /** Matching referenced resources as per the [Search.revInclude] criteria in the query. */ val revIncluded: Map, List>?, -) { - override fun equals(other: Any?) = - other is SearchResult<*> && - equalsShallow(resource, other.resource) && - equalsShallow(included, other.included) && - equalsShallow(revIncluded, other.revIncluded) - - private fun equalsShallow(first: Resource, second: Resource) = - first.resourceType == second.resourceType && first.logicalId == second.logicalId - - private fun equalsShallow(first: List, second: List) = - first.size == second.size && - first.asSequence().zip(second.asSequence()).all { (x, y) -> equalsShallow(x, y) } - - private fun equalsShallow( - first: Map>?, - second: Map>?, - ) = - if (first != null && second != null && first.size == second.size) { - first.entries.asSequence().zip(second.entries.asSequence()).all { (x, y) -> - x.key == y.key && equalsShallow(x.value, y.value) - } - } else { - first?.size == second?.size - } - - @JvmName("equalsShallowRevInclude") - private fun equalsShallow( - first: Map, List>?, - second: Map, List>?, - ) = - if (first != null && second != null && first.size == second.size) { - first.entries.asSequence().zip(second.entries.asSequence()).all { (x, y) -> - x.key == y.key && equalsShallow(x.value, y.value) - } - } else { - first?.size == second?.size - } -} +) diff --git a/engine/src/main/java/com/google/android/fhir/db/Database.kt b/engine/src/main/java/com/google/android/fhir/db/Database.kt index 03444c601b..ddbd3a9401 100644 --- a/engine/src/main/java/com/google/android/fhir/db/Database.kt +++ b/engine/src/main/java/com/google/android/fhir/db/Database.kt @@ -18,7 +18,8 @@ package com.google.android.fhir.db import com.google.android.fhir.LocalChange import com.google.android.fhir.LocalChangeToken -import com.google.android.fhir.db.impl.dao.IndexedIdAndResource +import com.google.android.fhir.db.impl.dao.ForwardIncludeSearchResult +import com.google.android.fhir.db.impl.dao.ReverseIncludeSearchResult import com.google.android.fhir.db.impl.entities.LocalChangeEntity import com.google.android.fhir.db.impl.entities.ResourceEntity import com.google.android.fhir.search.SearchQuery @@ -94,9 +95,11 @@ internal interface Database { */ suspend fun delete(type: ResourceType, id: String) - suspend fun search(query: SearchQuery): List + suspend fun search(query: SearchQuery): List> - suspend fun searchReferencedResources(query: SearchQuery): List + suspend fun searchForwardReferencedResources(query: SearchQuery): List + + suspend fun searchReverseReferencedResources(query: SearchQuery): List suspend fun count(query: SearchQuery): Long @@ -183,3 +186,8 @@ internal interface Database { */ suspend fun purge(type: ResourceType, id: String, forcePurge: Boolean = false) } + +data class ResourceWithUUID( + val uuid: UUID, + val resource: R, +) diff --git a/engine/src/main/java/com/google/android/fhir/db/impl/DatabaseImpl.kt b/engine/src/main/java/com/google/android/fhir/db/impl/DatabaseImpl.kt index 47139cf052..59fe65a8e1 100644 --- a/engine/src/main/java/com/google/android/fhir/db/impl/DatabaseImpl.kt +++ b/engine/src/main/java/com/google/android/fhir/db/impl/DatabaseImpl.kt @@ -27,8 +27,10 @@ import com.google.android.fhir.DatabaseErrorStrategy import com.google.android.fhir.LocalChange import com.google.android.fhir.LocalChangeToken import com.google.android.fhir.db.ResourceNotFoundException +import com.google.android.fhir.db.ResourceWithUUID import com.google.android.fhir.db.impl.DatabaseImpl.Companion.UNENCRYPTED_DATABASE_NAME -import com.google.android.fhir.db.impl.dao.IndexedIdAndResource +import com.google.android.fhir.db.impl.dao.ForwardIncludeSearchResult +import com.google.android.fhir.db.impl.dao.ReverseIncludeSearchResult import com.google.android.fhir.db.impl.entities.ResourceEntity import com.google.android.fhir.index.ResourceIndexer import com.google.android.fhir.logicalId @@ -199,23 +201,43 @@ internal class DatabaseImpl( } } - override suspend fun search(query: SearchQuery): List { + override suspend fun search( + query: SearchQuery, + ): List> { return db.withTransaction { resourceDao .getResources(SimpleSQLiteQuery(query.query, query.args.toTypedArray())) - .map { iParser.parseResource(it) as R } - .distinctBy { it.id } + .map { ResourceWithUUID(it.uuid, iParser.parseResource(it.serializedResource) as R) } + .distinctBy { it.uuid } } } - override suspend fun searchReferencedResources(query: SearchQuery): List { + override suspend fun searchForwardReferencedResources( + query: SearchQuery, + ): List { return db.withTransaction { resourceDao - .getReferencedResources(SimpleSQLiteQuery(query.query, query.args.toTypedArray())) + .getForwardReferencedResources(SimpleSQLiteQuery(query.query, query.args.toTypedArray())) .map { - IndexedIdAndResource( + ForwardIncludeSearchResult( it.matchingIndex, - it.idOfBaseResourceOnWhichThisMatchedInc ?: it.idOfBaseResourceOnWhichThisMatchedRev!!, + it.baseResourceUUID, + iParser.parseResource(it.serializedResource) as Resource, + ) + } + } + } + + override suspend fun searchReverseReferencedResources( + query: SearchQuery, + ): List { + return db.withTransaction { + resourceDao + .getReverseReferencedResources(SimpleSQLiteQuery(query.query, query.args.toTypedArray())) + .map { + ReverseIncludeSearchResult( + it.matchingIndex, + it.baseResourceTypeAndId, iParser.parseResource(it.serializedResource) as Resource, ) } diff --git a/engine/src/main/java/com/google/android/fhir/db/impl/dao/ResourceDao.kt b/engine/src/main/java/com/google/android/fhir/db/impl/dao/ResourceDao.kt index c31e49439c..bae8f3f6b9 100644 --- a/engine/src/main/java/com/google/android/fhir/db/impl/dao/ResourceDao.kt +++ b/engine/src/main/java/com/google/android/fhir/db/impl/dao/ResourceDao.kt @@ -226,12 +226,18 @@ internal abstract class ResourceDao { resourceUuid: UUID, ): ResourceEntity? - @RawQuery abstract suspend fun getResources(query: SupportSQLiteQuery): List + @RawQuery + abstract suspend fun getResources(query: SupportSQLiteQuery): List @RawQuery - abstract suspend fun getReferencedResources( + abstract suspend fun getForwardReferencedResources( query: SupportSQLiteQuery, - ): List + ): List + + @RawQuery + abstract suspend fun getReverseReferencedResources( + query: SupportSQLiteQuery, + ): List @RawQuery abstract suspend fun countResources(query: SupportSQLiteQuery): Long @@ -411,23 +417,39 @@ internal abstract class ResourceDao { } } -/** - * Data class representing the value returned by [getReferencedResources]. The optional fields may - * or may-not contain values based on the search query. - */ -internal data class IndexedIdAndSerializedResource( +internal class ForwardIncludeSearchResponse( + @ColumnInfo(name = "index_name") val matchingIndex: String, + @ColumnInfo(name = "resourceUuid") val baseResourceUUID: UUID, + val serializedResource: String, +) + +internal class ReverseIncludeSearchResponse( @ColumnInfo(name = "index_name") val matchingIndex: String, - @ColumnInfo(name = "index_value") val idOfBaseResourceOnWhichThisMatchedRev: String?, - @ColumnInfo(name = "resourceId") val idOfBaseResourceOnWhichThisMatchedInc: String?, + @ColumnInfo(name = "index_value") val baseResourceTypeAndId: String, val serializedResource: String, ) /** - * Data class representing an included or revIncluded [Resource], index on which the match was done - * and the id of the base [Resource] for which this [Resource] has been included. + * Data class representing a forward included [Resource], index on which the match was done and the + * uuid of the base [Resource] for which this [Resource] has been included. */ -internal data class IndexedIdAndResource( - val matchingIndex: String, - val idOfBaseResourceOnWhichThisMatched: String, +internal data class ForwardIncludeSearchResult( + val searchIndex: String, + val baseResourceUUID: UUID, val resource: Resource, ) + +/** + * Data class representing a reverse included [Resource], index on which the match was done and the + * type and logical id of the base [Resource] for which this [Resource] has been included. + */ +internal data class ReverseIncludeSearchResult( + val searchIndex: String, + val baseResourceTypeWithId: String, + val resource: Resource, +) + +internal data class SerializedResourceWithUuid( + @ColumnInfo(name = "resourceUuid") val uuid: UUID, + val serializedResource: String, +) 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 a75e2dc126..0075d6facf 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 @@ -16,6 +16,9 @@ package com.google.android.fhir.search +import android.annotation.SuppressLint +import androidx.annotation.VisibleForTesting +import androidx.room.util.convertUUIDToByte import ca.uhn.fhir.rest.gclient.DateClientParam import ca.uhn.fhir.rest.gclient.NumberClientParam import ca.uhn.fhir.rest.gclient.StringClientParam @@ -31,11 +34,13 @@ import com.google.android.fhir.logicalId import com.google.android.fhir.ucumUrl import java.math.BigDecimal import java.util.Date +import java.util.UUID import kotlin.math.absoluteValue import kotlin.math.roundToLong import org.hl7.fhir.r4.model.DateTimeType import org.hl7.fhir.r4.model.DateType import org.hl7.fhir.r4.model.Resource +import timber.log.Timber /** * The multiplier used to determine the range for the `ap` search prefix. See @@ -49,35 +54,36 @@ internal suspend fun Search.execute(database: Database): List + return baseResources.map { (uuid, baseResource) -> SearchResult( baseResource, included = includedResources ?.asSequence() - ?.filter { it.idOfBaseResourceOnWhichThisMatched == baseResource.logicalId } - ?.groupBy({ it.matchingIndex }, { it.resource }), + ?.filter { it.baseResourceUUID == uuid } + ?.groupBy({ it.searchIndex }, { it.resource }), revIncluded = revIncludedResources ?.asSequence() ?.filter { - it.idOfBaseResourceOnWhichThisMatched == - "${baseResource.fhirType()}/${baseResource.logicalId}" + it.baseResourceTypeWithId == "${baseResource.fhirType()}/${baseResource.logicalId}" } - ?.groupBy({ it.resource.resourceType to it.matchingIndex }, { it.resource }), + ?.groupBy({ it.resource.resourceType to it.searchIndex }, { it.resource }), ) } } @@ -90,136 +96,138 @@ fun Search.getQuery(isCount: Boolean = false): SearchQuery { return getQuery(isCount, null) } -private fun Search.getRevIncludeQuery(includeIds: List): SearchQuery { - var matchQuery = "" +@VisibleForTesting +internal fun Search.getRevIncludeQuery(includeIds: List): SearchQuery { val args = mutableListOf() - args.addAll(includeIds) + val uuidsString = CharArray(includeIds.size) { '?' }.joinToString() - // creating the match and filter query - revIncludes.forEachIndexed { index, (param, search) -> + fun generateFilterQuery(nestedSearch: NestedSearch): String { + val (param, search) = nestedSearch val resourceToInclude = search.type args.add(resourceToInclude.name) args.add(param.paramName) - matchQuery += " ( a.resourceType = ? and a.index_name IN (?) " - - val allFilters = search.getFilterQueries() + args.addAll(includeIds) + args.add(resourceToInclude.name) - if (allFilters.isNotEmpty()) { - val iterator = allFilters.listIterator() - matchQuery += "AND b.resourceUuid IN (\n" - do { - iterator.next().let { - matchQuery += it.query - args.addAll(it.args) - } + var filterQuery = "" + val filters = search.getFilterQueries() + val iterator = filters.listIterator() + while (iterator.hasNext()) { + iterator.next().let { + filterQuery += it.query + args.addAll(it.args) + } - if (iterator.hasNext()) { - matchQuery += - if (search.operation == Operation.OR) { - "\n UNION \n" - } else { - "\n INTERSECT \n" - } - } - } while (iterator.hasNext()) - matchQuery += "\n)" + if (iterator.hasNext()) { + filterQuery += + if (search.operation == Operation.OR) { + "\n UNION \n" + } else { + "\n INTERSECT \n" + } + } } - - matchQuery += " \n)" - - if (index != revIncludes.lastIndex) matchQuery += " OR " + return filterQuery } - return SearchQuery( - query = + return revIncludes + .map { + val (join, order) = it.search.getSortOrder(otherTable = "re") + args.addAll(join.args) + val filterQuery = generateFilterQuery(it) """ - SELECT a.index_name, a.index_value, b.serializedResource - FROM ReferenceIndexEntity a - JOIN ResourceEntity b - ON a.resourceUuid = b.resourceUuid - AND a.index_value IN( ${ CharArray(includeIds.size) { '?' }.joinToString()} ) - ${if (matchQuery.isEmpty()) "" else "AND ($matchQuery) " } - """ - .trimIndent(), - args = args, - ) + SELECT rie.index_name, rie.index_value, re.serializedResource + FROM ResourceEntity re + JOIN ReferenceIndexEntity rie + ON re.resourceUuid = rie.resourceUuid + ${join.query} + WHERE rie.resourceType = ? AND rie.index_name = ? AND rie.index_value IN ($uuidsString) AND re.resourceType = ? + ${if (filterQuery.isNotEmpty()) "AND re.resourceUuid IN ($filterQuery)" else ""} + $order + """ + .trimIndent() + } + .joinToString("\nUNION ALL\n") { + StringBuilder("SELECT * FROM (\n").append(it.trim()).append("\n)") + } + .split("\n") + .filter { it.isNotBlank() } + .joinToString("\n") { it.trim() } + .let { SearchQuery(it, args) } } -private fun Search.getIncludeQuery(includeIds: List): SearchQuery { - var matchQuery = "" - val args = mutableListOf(type.name) - args.addAll(includeIds) +@SuppressLint("RestrictedApi") +@VisibleForTesting +internal fun Search.getIncludeQuery(includeIds: List): SearchQuery { + val args = mutableListOf() + val baseResourceType = type + val uuidsString = CharArray(includeIds.size) { '?' }.joinToString() - // creating the match and filter query - forwardIncludes.forEachIndexed { index, (param, search) -> + fun generateFilterQuery(nestedSearch: NestedSearch): String { + val (param, search) = nestedSearch val resourceToInclude = search.type - args.add(resourceToInclude.name) + args.add(baseResourceType.name) args.add(param.paramName) - matchQuery += " ( c.resourceType = ? and b.index_name IN (?) " - - val allFilters = search.getFilterQueries() + args.addAll(includeIds.map { convertUUIDToByte(it) }) + args.add(resourceToInclude.name) - if (allFilters.isNotEmpty()) { - val iterator = allFilters.listIterator() - matchQuery += "AND c.resourceUuid IN (\n" - do { - iterator.next().let { - matchQuery += it.query - args.addAll(it.args) - } + var filterQuery = "" + val filters = search.getFilterQueries() + val iterator = filters.listIterator() + while (iterator.hasNext()) { + iterator.next().let { + filterQuery += it.query + args.addAll(it.args) + } - if (iterator.hasNext()) { - matchQuery += - if (search.operation == Operation.OR) { - "\n UNION \n" - } else { - "\n INTERSECT \n" - } - } - } while (iterator.hasNext()) - matchQuery += "\n)" + if (iterator.hasNext()) { + filterQuery += + if (search.operation == Operation.OR) { + "\nUNION\n" + } else { + "\nINTERSECT\n" + } + } } - - matchQuery += " \n)" - - if (index != forwardIncludes.lastIndex) matchQuery += " OR " + return filterQuery } - return SearchQuery( - query = - // spotless:off - """ - SELECT b.index_name, a.resourceId, c.serializedResource from ResourceEntity a - JOIN ReferenceIndexEntity b - On a.resourceUuid = b.resourceUuid - AND a.resourceType = ? - AND a.resourceId IN ( ${ CharArray(includeIds.size) { '?' }.joinToString()} ) - JOIN ResourceEntity c - ON c.resourceType||"/"||c.resourceId = b.index_value - ${if (matchQuery.isEmpty()) "" else "AND ($matchQuery) " } - """.trimIndent(), - // spotless:on - args = args, - ) + return forwardIncludes + .map { + val (join, order) = it.search.getSortOrder(otherTable = "re") + args.addAll(join.args) + val filterQuery = generateFilterQuery(it) + """ + SELECT rie.index_name, rie.resourceUuid, re.serializedResource + FROM ResourceEntity re + JOIN ReferenceIndexEntity rie + ON re.resourceType||"/"||re.resourceId = rie.index_value + ${join.query} + WHERE rie.resourceType = ? AND rie.index_name = ? AND rie.resourceUuid IN ($uuidsString) AND re.resourceType = ? + ${if (filterQuery.isNotEmpty()) "AND re.resourceUuid IN ($filterQuery)" else ""} + $order + """ + .trimIndent() + } + .joinToString("\nUNION ALL\n") { + StringBuilder("SELECT * FROM (\n").append(it.trim()).append("\n)") + } + .split("\n") + .filter { it.isNotBlank() } + .joinToString("\n") { it.trim() } + .let { SearchQuery(it, args) } } -private fun Search.getFilterQueries() = - (stringFilterCriteria + - quantityFilterCriteria + - numberFilterCriteria + - referenceFilterCriteria + - dateTimeFilterCriteria + - tokenFilterCriteria + - uriFilterCriteria) - .map { it.query(type) } - -internal fun Search.getQuery( - isCount: Boolean = false, - nestedContext: NestedContext? = null, -): SearchQuery { +private fun Search.getSortOrder( + otherTable: String, + isReferencedSearch: Boolean = false, +): Pair { var sortJoinStatement = "" var sortOrderStatement = "" - val sortArgs = mutableListOf() + val args = mutableListOf() + if (isReferencedSearch && count != null) { + Timber.e("count not supported for [rev]include search.") + } sort?.let { sort -> val sortTableNames = when (sort) { @@ -232,20 +240,20 @@ internal fun Search.getQuery( listOf(SortTableInfo.DATE_SORT_TABLE_INFO, SortTableInfo.DATE_TIME_SORT_TABLE_INFO) else -> throw NotImplementedError("Unhandled sort parameter of type ${sort::class}: $sort") } - sortJoinStatement = "" - - sortTableNames.forEachIndexed { index, sortTableName -> - val tableAlias = 'b' + index - sortJoinStatement += - // spotless:off + sortJoinStatement = + sortTableNames + .mapIndexed { index, sortTableName -> + val tableAlias = 'b' + index + // spotless:off """ LEFT JOIN ${sortTableName.tableName} $tableAlias - ON a.resourceType = $tableAlias.resourceType AND a.resourceUuid = $tableAlias.resourceUuid AND $tableAlias.index_name = ? + ON $otherTable.resourceType = $tableAlias.resourceType AND $otherTable.resourceUuid = $tableAlias.resourceUuid AND $tableAlias.index_name = ? """ - // spotless:on - sortArgs += sort.paramName - } + // spotless:on + } + .joinToString(separator = "\n") + sortTableNames.forEach { _ -> args.add(sort.paramName) } sortTableNames.forEachIndexed { index, sortTableName -> val tableAlias = 'b' + index @@ -253,13 +261,34 @@ internal fun Search.getQuery( if (index == 0) { """ ORDER BY $tableAlias.${sortTableName.columnName} ${order.sqlString} - """ + """ .trimIndent() } else { ", $tableAlias.${SortTableInfo.DATE_TIME_SORT_TABLE_INFO.columnName} ${order.sqlString}" } } } + return Pair(SearchQuery(sortJoinStatement, args), sortOrderStatement) +} + +private fun Search.getFilterQueries() = + (stringFilterCriteria + + quantityFilterCriteria + + numberFilterCriteria + + referenceFilterCriteria + + dateTimeFilterCriteria + + tokenFilterCriteria + + uriFilterCriteria) + .map { it.query(type) } + +internal fun Search.getQuery( + isCount: Boolean = false, + nestedContext: NestedContext? = null, +): SearchQuery { + val (join, order) = getSortOrder(otherTable = "a") + val sortJoinStatement = join.query + val sortOrderStatement = order + val sortArgs = join.args var filterStatement = "" val filterArgs = mutableListOf() @@ -331,7 +360,7 @@ internal fun Search.getQuery( else -> // spotless:off """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a $sortJoinStatement WHERE a.resourceType = ? 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 fe027fc272..1f63ed63bd 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 @@ -68,7 +68,9 @@ inline fun Search.has( * } * ``` * - * **NOTE**: [include] doesn't support order OR count. + * **NOTE**: + * * [include] doesn't support count. + * * Multiple includes of the same resource type do not guarantee the order of returned resources. */ inline fun Search.include( referenceParam: ReferenceClientParam, @@ -99,7 +101,9 @@ inline fun Search.include( * } * ``` * - * **NOTE**: [include] doesn't support order OR count. + * **NOTE**: + * * [include] doesn't support count. + * * Multiple includes of the same resource type do not guarantee the order of returned resources. */ fun Search.include( resourceType: ResourceType, @@ -129,7 +133,10 @@ fun Search.include( * } * ``` * - * **NOTE**: [revInclude] doesn't support order OR count. + * **NOTE**: + * * [revInclude] doesn't support count. + * * Multiple revIncludes of the same resource type do not guarantee the order of returned + * resources. */ inline fun Search.revInclude( referenceParam: ReferenceClientParam, @@ -160,7 +167,10 @@ inline fun Search.revInclude( * } * ``` * - * **NOTE**: [revInclude] doesn't support order OR count. + * **NOTE**: + * * [revInclude] doesn't support count. + * * Multiple revIncludes of the same resource type do not guarantee the order of returned + * resources. */ fun Search.revInclude( resourceType: ResourceType, diff --git a/engine/src/test/java/com/google/android/fhir/search/NumberSearchParameterizedTest.kt b/engine/src/test/java/com/google/android/fhir/search/NumberSearchParameterizedTest.kt index 7a6b8f6f26..c00ff7dbc5 100644 --- a/engine/src/test/java/com/google/android/fhir/search/NumberSearchParameterizedTest.kt +++ b/engine/src/test/java/com/google/android/fhir/search/NumberSearchParameterizedTest.kt @@ -51,7 +51,7 @@ class NumberSearchParameterizedTest( private val baseQuery: String = """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceUuid IN ( 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 34e9d71692..c9da480147 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 @@ -17,14 +17,17 @@ package com.google.android.fhir.search import android.os.Build +import androidx.room.util.convertUUIDToByte import ca.uhn.fhir.model.api.TemporalPrecisionEnum import ca.uhn.fhir.rest.param.ParamPrefixEnum import com.google.android.fhir.DateProvider import com.google.android.fhir.epochDay +import com.google.common.truth.Correspondence import com.google.common.truth.Truth.assertThat import java.math.BigDecimal import java.time.Instant import java.util.Date +import java.util.UUID import kotlin.math.absoluteValue import kotlin.math.roundToLong import kotlinx.coroutines.runBlocking @@ -35,11 +38,14 @@ import org.hl7.fhir.r4.model.Condition 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.Encounter import org.hl7.fhir.r4.model.Identifier import org.hl7.fhir.r4.model.Immunization import org.hl7.fhir.r4.model.Library import org.hl7.fhir.r4.model.Observation +import org.hl7.fhir.r4.model.Organization import org.hl7.fhir.r4.model.Patient +import org.hl7.fhir.r4.model.Practitioner import org.hl7.fhir.r4.model.ResourceType import org.hl7.fhir.r4.model.RiskAssessment import org.hl7.fhir.r4.model.UriType @@ -60,7 +66,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? """ @@ -92,7 +98,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? LIMIT ? @@ -115,7 +121,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? LIMIT ? OFFSET ? @@ -146,7 +152,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceUuid IN ( @@ -199,7 +205,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceUuid IN ( @@ -239,7 +245,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceUuid IN ( @@ -279,7 +285,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceUuid IN ( @@ -314,7 +320,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceUuid IN ( @@ -357,7 +363,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceUuid IN ( @@ -397,7 +403,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceUuid IN ( @@ -437,7 +443,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceUuid IN ( @@ -477,7 +483,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceUuid IN ( @@ -522,7 +528,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceUuid IN ( @@ -576,7 +582,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceUuid IN ( @@ -616,7 +622,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceUuid IN ( @@ -656,7 +662,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceUuid IN ( @@ -699,7 +705,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceUuid IN ( @@ -742,7 +748,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceUuid IN ( @@ -782,7 +788,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceUuid IN ( @@ -822,7 +828,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceUuid IN ( @@ -862,7 +868,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceUuid IN ( @@ -894,7 +900,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceUuid IN ( @@ -931,7 +937,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceUuid IN ( @@ -968,7 +974,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceUuid IN ( @@ -1005,7 +1011,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceUuid IN ( @@ -1045,7 +1051,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceUuid IN ( @@ -1080,7 +1086,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceUuid IN ( @@ -1125,7 +1131,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceUuid IN ( @@ -1169,7 +1175,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceUuid IN ( @@ -1199,7 +1205,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceUuid IN ( @@ -1228,7 +1234,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceUuid IN ( @@ -1264,7 +1270,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceUuid IN ( @@ -1295,7 +1301,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceUuid IN ( @@ -1334,7 +1340,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceUuid IN ( @@ -1375,7 +1381,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceUuid IN ( @@ -1415,7 +1421,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceUuid IN ( @@ -1455,7 +1461,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceUuid IN ( @@ -1494,7 +1500,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceUuid IN ( @@ -1532,7 +1538,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceUuid IN ( @@ -1571,7 +1577,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceUuid IN ( @@ -1609,7 +1615,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceUuid IN ( @@ -1649,7 +1655,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceUuid IN ( @@ -1681,7 +1687,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceUuid IN ( @@ -1708,7 +1714,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a LEFT JOIN StringIndexEntity b ON a.resourceType = b.resourceType AND a.resourceUuid = b.resourceUuid AND b.index_name = ? @@ -1728,7 +1734,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a LEFT JOIN StringIndexEntity b ON a.resourceType = b.resourceType AND a.resourceUuid = b.resourceUuid AND b.index_name = ? @@ -1750,7 +1756,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a LEFT JOIN NumberIndexEntity b ON a.resourceType = b.resourceType AND a.resourceUuid = b.resourceUuid AND b.index_name = ? @@ -1776,7 +1782,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a LEFT JOIN StringIndexEntity b ON a.resourceType = b.resourceType AND a.resourceUuid = b.resourceUuid AND b.index_name = ? @@ -1821,7 +1827,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceUuid IN ( @@ -1897,7 +1903,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceUuid IN ( @@ -1970,7 +1976,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceUuid IN ( @@ -2032,7 +2038,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a LEFT JOIN DateIndexEntity b ON a.resourceType = b.resourceType AND a.resourceUuid = b.resourceUuid AND b.index_name = ? @@ -2053,7 +2059,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a LEFT JOIN DateIndexEntity b ON a.resourceType = b.resourceType AND a.resourceUuid = b.resourceUuid AND b.index_name = ? @@ -2089,7 +2095,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceUuid IN ( @@ -2130,7 +2136,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceUuid IN ( @@ -2163,7 +2169,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceUuid IN ( @@ -2194,7 +2200,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceUuid IN ( @@ -2232,7 +2238,7 @@ class SearchTest { assertThat(query.query) .isEqualTo( """ - SELECT a.serializedResource + SELECT a.resourceUuid, a.serializedResource FROM ResourceEntity a WHERE a.resourceType = ? AND a.resourceUuid IN ( @@ -2255,8 +2261,490 @@ class SearchTest { ) } + @Test + fun `search include all practitioners`() { + val query = + Search(ResourceType.Patient) + .apply { include(Patient.GENERAL_PRACTITIONER) } + .getIncludeQuery( + listOf( + UUID.fromString("e2c79e28-ed4d-2029-a12c-108d1eb5bedb"), + UUID.fromString("e2c79e28-ed4d-4029-a12c-108d1eb5bedb"), + ), + ) + + assertThat(query.query) + .isEqualTo( + """ + SELECT * FROM ( + SELECT rie.index_name, rie.resourceUuid, re.serializedResource + FROM ResourceEntity re + JOIN ReferenceIndexEntity rie + ON re.resourceType||"/"||re.resourceId = rie.index_value + WHERE rie.resourceType = ? AND rie.index_name = ? AND rie.resourceUuid IN (?, ?) AND re.resourceType = ? + ) + """ + .trimIndent(), + ) + + assertThat(query.args) + .comparingElementsUsing(ArgsComparator) + .containsExactly( + "Patient", + "general-practitioner", + convertUUIDToByte(UUID.fromString("e2c79e28-ed4d-2029-a12c-108d1eb5bedb")), + convertUUIDToByte(UUID.fromString("e2c79e28-ed4d-4029-a12c-108d1eb5bedb")), + "Practitioner", + ) + .inOrder() + } + + @Test + fun `search include all active practitioners`() { + val query = + Search(ResourceType.Patient) + .apply { + include(Patient.GENERAL_PRACTITIONER) { + filter(Practitioner.ACTIVE, { value = of(true) }) + } + } + .getIncludeQuery( + listOf( + UUID.fromString("e2c79e28-ed4d-2029-a12c-108d1eb5bedb"), + UUID.fromString("e2c79e28-ed4d-4029-a12c-108d1eb5bedb"), + ), + ) + + assertThat(query.query) + .isEqualTo( + """ + SELECT * FROM ( + SELECT rie.index_name, rie.resourceUuid, re.serializedResource + FROM ResourceEntity re + JOIN ReferenceIndexEntity rie + ON re.resourceType||"/"||re.resourceId = rie.index_value + WHERE rie.resourceType = ? AND rie.index_name = ? AND rie.resourceUuid IN (?, ?) AND re.resourceType = ? + AND re.resourceUuid IN ( + SELECT resourceUuid FROM TokenIndexEntity + WHERE resourceType = ? AND index_name = ? AND index_value = ? + ) + ) + """ + .trimIndent(), + ) + + assertThat(query.args) + .comparingElementsUsing(ArgsComparator) + .containsExactly( + "Patient", + "general-practitioner", + convertUUIDToByte(UUID.fromString("e2c79e28-ed4d-2029-a12c-108d1eb5bedb")), + convertUUIDToByte(UUID.fromString("e2c79e28-ed4d-4029-a12c-108d1eb5bedb")), + "Practitioner", + "Practitioner", + "active", + "true", + ) + .inOrder() + } + + @Test + fun `search include all active practitioners and sort by given name`() { + val query = + Search(ResourceType.Patient) + .apply { + include(Patient.GENERAL_PRACTITIONER) { + filter(Practitioner.ACTIVE, { value = of(true) }) + sort(Practitioner.GIVEN, Order.DESCENDING) + } + } + .getIncludeQuery( + listOf( + UUID.fromString("e2c79e28-ed4d-2029-a12c-108d1eb5bedb"), + UUID.fromString("e2c79e28-ed4d-4029-a12c-108d1eb5bedb"), + ), + ) + + assertThat(query.query) + .isEqualTo( + """ + SELECT * FROM ( + SELECT rie.index_name, rie.resourceUuid, re.serializedResource + FROM ResourceEntity re + JOIN ReferenceIndexEntity rie + ON re.resourceType||"/"||re.resourceId = rie.index_value + LEFT JOIN StringIndexEntity b + ON re.resourceType = b.resourceType AND re.resourceUuid = b.resourceUuid AND b.index_name = ? + WHERE rie.resourceType = ? AND rie.index_name = ? AND rie.resourceUuid IN (?, ?) AND re.resourceType = ? + AND re.resourceUuid IN ( + SELECT resourceUuid FROM TokenIndexEntity + WHERE resourceType = ? AND index_name = ? AND index_value = ? + ) + ORDER BY b.index_value DESC + ) + """ + .trimIndent(), + ) + + assertThat(query.args) + .comparingElementsUsing(ArgsComparator) + .containsExactly( + "given", + "Patient", + "general-practitioner", + convertUUIDToByte(UUID.fromString("e2c79e28-ed4d-2029-a12c-108d1eb5bedb")), + convertUUIDToByte(UUID.fromString("e2c79e28-ed4d-4029-a12c-108d1eb5bedb")), + "Practitioner", + "Practitioner", + "active", + "true", + ) + .inOrder() + } + + @Test + fun `search include practitioners and organizations`() { + val query = + Search(ResourceType.Patient) + .apply { + include(Patient.GENERAL_PRACTITIONER) { + filter(Practitioner.ACTIVE, { value = of(true) }) + sort(Practitioner.GIVEN, Order.DESCENDING) + } + + include(Patient.ORGANIZATION) { + filter(Organization.ACTIVE, { value = of(true) }) + sort(Organization.NAME, Order.DESCENDING) + } + } + .getIncludeQuery( + listOf( + UUID.fromString("e2c79e28-ed4d-2029-a12c-108d1eb5bedb"), + UUID.fromString("e2c79e28-ed4d-4029-a12c-108d1eb5bedb"), + ), + ) + + assertThat(query.query) + .isEqualTo( + """ + SELECT * FROM ( + SELECT rie.index_name, rie.resourceUuid, re.serializedResource + FROM ResourceEntity re + JOIN ReferenceIndexEntity rie + ON re.resourceType||"/"||re.resourceId = rie.index_value + LEFT JOIN StringIndexEntity b + ON re.resourceType = b.resourceType AND re.resourceUuid = b.resourceUuid AND b.index_name = ? + WHERE rie.resourceType = ? AND rie.index_name = ? AND rie.resourceUuid IN (?, ?) AND re.resourceType = ? + AND re.resourceUuid IN ( + SELECT resourceUuid FROM TokenIndexEntity + WHERE resourceType = ? AND index_name = ? AND index_value = ? + ) + ORDER BY b.index_value DESC + ) + UNION ALL + SELECT * FROM ( + SELECT rie.index_name, rie.resourceUuid, re.serializedResource + FROM ResourceEntity re + JOIN ReferenceIndexEntity rie + ON re.resourceType||"/"||re.resourceId = rie.index_value + LEFT JOIN StringIndexEntity b + ON re.resourceType = b.resourceType AND re.resourceUuid = b.resourceUuid AND b.index_name = ? + WHERE rie.resourceType = ? AND rie.index_name = ? AND rie.resourceUuid IN (?, ?) AND re.resourceType = ? + AND re.resourceUuid IN ( + SELECT resourceUuid FROM TokenIndexEntity + WHERE resourceType = ? AND index_name = ? AND index_value = ? + ) + ORDER BY b.index_value DESC + ) + """ + .trimIndent(), + ) + + assertThat(query.args) + .comparingElementsUsing(ArgsComparator) + .containsExactly( + "given", + "Patient", + "general-practitioner", + convertUUIDToByte(UUID.fromString("e2c79e28-ed4d-2029-a12c-108d1eb5bedb")), + convertUUIDToByte(UUID.fromString("e2c79e28-ed4d-4029-a12c-108d1eb5bedb")), + "Practitioner", + "Practitioner", + "active", + "true", + "name", + "Patient", + "organization", + convertUUIDToByte(UUID.fromString("e2c79e28-ed4d-2029-a12c-108d1eb5bedb")), + convertUUIDToByte(UUID.fromString("e2c79e28-ed4d-4029-a12c-108d1eb5bedb")), + "Organization", + "Organization", + "active", + "true", + ) + .inOrder() + } + + @Test + fun `search revInclude all conditions for patients`() { + val query = + Search(ResourceType.Patient) + .apply { revInclude(Condition.SUBJECT) } + .getRevIncludeQuery(listOf("Patient/pa01", "Patient/pa02")) + + assertThat(query.query) + .isEqualTo( + """ + SELECT * FROM ( + SELECT rie.index_name, rie.index_value, re.serializedResource + FROM ResourceEntity re + JOIN ReferenceIndexEntity rie + ON re.resourceUuid = rie.resourceUuid + WHERE rie.resourceType = ? AND rie.index_name = ? AND rie.index_value IN (?, ?) AND re.resourceType = ? + ) + """ + .trimIndent(), + ) + + assertThat(query.args) + .containsExactly("Condition", "subject", "Patient/pa01", "Patient/pa02", "Condition") + .inOrder() + } + + @Test + fun `search revInclude diabetic conditions for patients`() { + val query = + Search(ResourceType.Patient) + .apply { + revInclude(Condition.SUBJECT) { + filter( + Condition.CODE, + { + value = + of( + CodeableConcept(Coding("http://snomed.info/sct", "44054006", "Diabetes")), + ) + }, + ) + } + } + .getRevIncludeQuery(listOf("Patient/pa01", "Patient/pa02")) + + assertThat(query.query) + .isEqualTo( + """ + SELECT * FROM ( + SELECT rie.index_name, rie.index_value, re.serializedResource + FROM ResourceEntity re + JOIN ReferenceIndexEntity rie + ON re.resourceUuid = rie.resourceUuid + WHERE rie.resourceType = ? AND rie.index_name = ? AND rie.index_value IN (?, ?) AND re.resourceType = ? + AND re.resourceUuid IN ( + SELECT resourceUuid FROM TokenIndexEntity + WHERE resourceType = ? AND index_name = ? AND (index_value = ? AND IFNULL(index_system,'') = ?) + ) + ) + """ + .trimIndent(), + ) + + assertThat(query.args) + .containsExactly( + "Condition", + "subject", + "Patient/pa01", + "Patient/pa02", + "Condition", + "Condition", + "code", + "44054006", + "http://snomed.info/sct", + ) + .inOrder() + } + + @Test + fun `search revInclude diabetic conditions for patients and sort by recorded date`() { + val query = + Search(ResourceType.Patient) + .apply { + revInclude(Condition.SUBJECT) { + filter( + Condition.CODE, + { + value = + of( + CodeableConcept(Coding("http://snomed.info/sct", "44054006", "Diabetes")), + ) + }, + ) + sort(Condition.RECORDED_DATE, Order.DESCENDING) + } + } + .getRevIncludeQuery(listOf("Patient/pa01", "Patient/pa02")) + + assertThat(query.query) + .isEqualTo( + """ + SELECT * FROM ( + SELECT rie.index_name, rie.index_value, re.serializedResource + FROM ResourceEntity re + JOIN ReferenceIndexEntity rie + ON re.resourceUuid = rie.resourceUuid + LEFT JOIN DateIndexEntity b + ON re.resourceType = b.resourceType AND re.resourceUuid = b.resourceUuid AND b.index_name = ? + LEFT JOIN DateTimeIndexEntity c + ON re.resourceType = c.resourceType AND re.resourceUuid = c.resourceUuid AND c.index_name = ? + WHERE rie.resourceType = ? AND rie.index_name = ? AND rie.index_value IN (?, ?) AND re.resourceType = ? + AND re.resourceUuid IN ( + SELECT resourceUuid FROM TokenIndexEntity + WHERE resourceType = ? AND index_name = ? AND (index_value = ? AND IFNULL(index_system,'') = ?) + ) + ORDER BY b.index_from DESC, c.index_from DESC + ) + """ + .trimIndent(), + ) + + assertThat(query.args) + .containsExactly( + "recorded-date", + "recorded-date", + "Condition", + "subject", + "Patient/pa01", + "Patient/pa02", + "Condition", + "Condition", + "code", + "44054006", + "http://snomed.info/sct", + ) + .inOrder() + } + + @Test + fun `search revInclude encounters and conditions filtered and sorted`() { + val query = + Search(ResourceType.Patient) + .apply { + revInclude(Encounter.SUBJECT) { + filter( + Encounter.STATUS, + { + value = + of( + Coding( + "http://hl7.org/fhir/encounter-status", + Encounter.EncounterStatus.ARRIVED.toCode(), + "", + ), + ) + }, + ) + sort(Encounter.DATE, Order.DESCENDING) + } + + revInclude(Condition.SUBJECT) { + filter( + Condition.CODE, + { + value = + of( + CodeableConcept(Coding("http://snomed.info/sct", "44054006", "Diabetes")), + ) + }, + ) + sort(Condition.RECORDED_DATE, Order.DESCENDING) + } + } + .getRevIncludeQuery(listOf("Patient/pa01", "Patient/pa02")) + + assertThat(query.query) + .isEqualTo( + """ + SELECT * FROM ( + SELECT rie.index_name, rie.index_value, re.serializedResource + FROM ResourceEntity re + JOIN ReferenceIndexEntity rie + ON re.resourceUuid = rie.resourceUuid + LEFT JOIN DateIndexEntity b + ON re.resourceType = b.resourceType AND re.resourceUuid = b.resourceUuid AND b.index_name = ? + LEFT JOIN DateTimeIndexEntity c + ON re.resourceType = c.resourceType AND re.resourceUuid = c.resourceUuid AND c.index_name = ? + WHERE rie.resourceType = ? AND rie.index_name = ? AND rie.index_value IN (?, ?) AND re.resourceType = ? + AND re.resourceUuid IN ( + SELECT resourceUuid FROM TokenIndexEntity + WHERE resourceType = ? AND index_name = ? AND (index_value = ? AND IFNULL(index_system,'') = ?) + ) + ORDER BY b.index_from DESC, c.index_from DESC + ) + UNION ALL + SELECT * FROM ( + SELECT rie.index_name, rie.index_value, re.serializedResource + FROM ResourceEntity re + JOIN ReferenceIndexEntity rie + ON re.resourceUuid = rie.resourceUuid + LEFT JOIN DateIndexEntity b + ON re.resourceType = b.resourceType AND re.resourceUuid = b.resourceUuid AND b.index_name = ? + LEFT JOIN DateTimeIndexEntity c + ON re.resourceType = c.resourceType AND re.resourceUuid = c.resourceUuid AND c.index_name = ? + WHERE rie.resourceType = ? AND rie.index_name = ? AND rie.index_value IN (?, ?) AND re.resourceType = ? + AND re.resourceUuid IN ( + SELECT resourceUuid FROM TokenIndexEntity + WHERE resourceType = ? AND index_name = ? AND (index_value = ? AND IFNULL(index_system,'') = ?) + ) + ORDER BY b.index_from DESC, c.index_from DESC + ) + """ + .trimIndent(), + ) + + assertThat(query.args) + .containsExactly( + "date", + "date", + "Encounter", + "subject", + "Patient/pa01", + "Patient/pa02", + "Encounter", + "Encounter", + "status", + "arrived", + "http://hl7.org/fhir/encounter-status", + "recorded-date", + "recorded-date", + "Condition", + "subject", + "Patient/pa01", + "Patient/pa02", + "Condition", + "Condition", + "code", + "44054006", + "http://snomed.info/sct", + ) + .inOrder() + } + private companion object { const val mockEpochTimeStamp = 1628516301000 const val APPROXIMATION_COEFFICIENT = 0.1 + + /** + * Custom implementation to equality check values of [com.google.common.truth.IterableSubject]. + */ + val ArgsComparator: Correspondence = + Correspondence.from( + { a, b -> + if (a is ByteArray && b is ByteArray) { + a.contentEquals(b) + } else { + a == b + } + }, + "", + ) } }