From f33cc649a49017e4646c76cbd4299f081fa775c8 Mon Sep 17 00:00:00 2001 From: Aditya Khajuria Date: Thu, 13 Apr 2023 13:54:14 +0530 Subject: [PATCH 01/16] Inital changes to support inline resources via revInclude --- .../android/fhir/db/impl/DatabaseImplTest.kt | 143 ++++++++++++++++++ .../google/android/fhir/search/MoreSearch.kt | 29 +++- .../google/android/fhir/search/SearchDsl.kt | 7 +- 3 files changed, 177 insertions(+), 2 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 e46963ab6a..6ae2ee5ff9 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 @@ -54,6 +54,7 @@ import org.hl7.fhir.r4.model.Condition import org.hl7.fhir.r4.model.DateTimeType import org.hl7.fhir.r4.model.DateType import org.hl7.fhir.r4.model.DecimalType +import org.hl7.fhir.r4.model.Encounter import org.hl7.fhir.r4.model.Enumerations import org.hl7.fhir.r4.model.Extension import org.hl7.fhir.r4.model.HumanName @@ -65,6 +66,7 @@ import org.hl7.fhir.r4.model.Patient import org.hl7.fhir.r4.model.Practitioner import org.hl7.fhir.r4.model.Quantity import org.hl7.fhir.r4.model.Reference +import org.hl7.fhir.r4.model.Resource import org.hl7.fhir.r4.model.ResourceType import org.hl7.fhir.r4.model.RiskAssessment import org.hl7.fhir.r4.model.SearchParameter @@ -3075,6 +3077,147 @@ class DatabaseImplTest { assertThat(result.map { it.nameFirstRep.nameAsSingleString }).contains("Darcy Smith") } + @Test + fun search_revInclude(): Unit = runBlocking { + val resources = + listOf( + Patient().apply { id = "pa-01" }, + Patient().apply { id = "pa-02" }, + Patient().apply { id = "pa-04" }, + Encounter().apply { + id = "en-01" + subject = Reference("Patient/pa-01") + }, + Encounter().apply { + id = "en-02" + subject = Reference("Patient/pa-02") + }, + Encounter().apply { + id = "en-03" + subject = Reference("Patient/pa-03") + } + ) + database.insertRemote(*resources.toTypedArray()) + + val result = + database.search( + Search(ResourceType.Patient) + .apply { revInclude(ResourceType.Encounter, Encounter.SUBJECT) } + .getQuery() + ) + assertThat(result.map { it.logicalId }) + .containsExactly("pa-01", "pa-02", "pa-04", "en-01", "en-02", "test_patient_1") + } + + @Test + fun search_patient_revInclude(): Unit = runBlocking { + val resources = + listOf( + Patient().apply { + id = "pa-01" + addName( + HumanName().apply { + addGiven("James") + family = "Gorden" + } + ) + }, + Patient().apply { id = "pa-02" }, + Patient().apply { id = "pa-04" }, + Encounter().apply { + id = "en-01" + subject = Reference("Patient/pa-01") + }, + Encounter().apply { + id = "en-02" + subject = Reference("Patient/pa-02") + }, + Encounter().apply { + id = "en-03" + subject = Reference("Patient/pa-01") + } + ) + database.insertRemote(*resources.toTypedArray()) + + val result = + database.search( + Search(ResourceType.Patient) + .apply { + filter(Patient.GIVEN, { value = "James" }) + revInclude(ResourceType.Encounter, Encounter.SUBJECT) + } + .getQuery() + ) + assertThat(result.map { it.logicalId }).containsExactly("pa-01", "en-01", "en-03") + } + + @Test + fun search_patient_has_revInclude(): Unit = runBlocking { + val diabetesCodeableConcept = + CodeableConcept(Coding("http://snomed.info/sct", "44054006", "Diabetes")) + val hyperTensionCodeableConcept = + CodeableConcept(Coding("http://snomed.info/sct", "827069000", "Hypertension stage 1")) + val resources = + listOf( + Patient().apply { + id = "pa-01" + addName( + HumanName().apply { + addGiven("James") + family = "Gorden" + } + ) + }, + Patient().apply { + id = "pa-02" + addName( + HumanName().apply { + addGiven("James") + family = "Bond" + } + ) + }, + Patient().apply { id = "pa-04" }, + Encounter().apply { + id = "en-01" + subject = Reference("Patient/pa-01") + }, + Encounter().apply { + id = "en-02" + subject = Reference("Patient/pa-02") + }, + Encounter().apply { + id = "en-03" + subject = Reference("Patient/pa-01") + }, + Condition().apply { + id = "con-01" + code = diabetesCodeableConcept + subject = Reference("Patient/pa-01") + }, + Condition().apply { + id = "con-02" + code = hyperTensionCodeableConcept + subject = Reference("Patient/pa-02") + } + ) + database.insertRemote(*resources.toTypedArray()) + + val result = + database.search( + Search(ResourceType.Patient) + .apply { + filter(Patient.GIVEN, { value = "James" }) + has(Condition.SUBJECT) { + filter(Condition.CODE, { value = of(diabetesCodeableConcept) }) + } + revInclude(ResourceType.Encounter, Encounter.SUBJECT) + } + .getQuery() + ) + assertThat(result.map { it.logicalId }).containsExactly("pa-01", "en-01", "en-03") + } + private companion object { const val mockEpochTimeStamp = 1628516301000 const val TEST_PATIENT_1_ID = "test_patient_1" 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 42af3fecf4..7c8bda0e85 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 @@ -18,6 +18,7 @@ package com.google.android.fhir.search import ca.uhn.fhir.rest.gclient.DateClientParam import ca.uhn.fhir.rest.gclient.NumberClientParam +import ca.uhn.fhir.rest.gclient.ReferenceClientParam import ca.uhn.fhir.rest.gclient.StringClientParam import ca.uhn.fhir.rest.param.ParamPrefixEnum import com.google.android.fhir.ConverterException @@ -34,6 +35,7 @@ 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 org.hl7.fhir.r4.model.ResourceType /** * The multiplier used to determine the range for the `ap` search prefix. See @@ -166,6 +168,21 @@ internal fun Search.getQuery( $limitStatement) """ } + revIncludeMap.isNotEmpty() -> { + """ + select serializedResource from ResourceEntity where resourceUuid in ( + with UUIDS as ( select '${type.name}/' || a.resourceId from ResourceEntity a + $sortJoinStatement + WHERE a.resourceType = ? + $filterStatement + $sortOrderStatement + $limitStatement + ) + Select resourceUuid from ResourceEntity where '${type.name}/' || resourceId in UUIDS + ${revIncludeMap.toSQLQuery()} + ) + """.trimIndent() + } else -> """ SELECT a.serializedResource @@ -295,7 +312,8 @@ internal fun getConditionParamPair( (prefix != ParamPrefixEnum.STARTS_AFTER && prefix != ParamPrefixEnum.ENDS_BEFORE) ) { "Prefix $prefix not allowed for Integer type" } return when (prefix) { - ParamPrefixEnum.EQUAL, null -> { + ParamPrefixEnum.EQUAL, + null -> { val precision = value.getRange() ConditionParam( "index_value >= ? AND index_value < ?", @@ -441,3 +459,12 @@ private fun getApproximateDateRange( } private data class ApproximateDateRange(val start: Long, val end: Long) + +private fun Map>.toSQLQuery() = + map { it -> + val indexes = it.value.joinToString { "\'${it.paramName}\'" } + """ + SELECT DISTINCT resourceUuid from ReferenceIndexEntity WHERE resourceType = '${it.key}' AND index_name IN ($indexes) AND index_value IN UUIDS + """.trimIndent() + } + .joinToString(prefix = "\nUNION\n", separator = " \nUNION\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 9bf11882d2..2121d88e11 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 @@ -1,5 +1,5 @@ /* - * Copyright 2021 Google LLC + * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -53,6 +53,7 @@ data class Search(val type: ResourceType, var count: Int? = null, var from: Int? internal val uriFilterCriteria = mutableListOf() internal var sort: IParam? = null internal var order: Order? = null + internal val revIncludeMap = mutableMapOf>() @PublishedApi internal var nestedSearches = mutableListOf() var operation = Operation.AND @@ -142,6 +143,10 @@ data class Search(val type: ResourceType, var count: Int? = null, var from: Int? sort = parameter this.order = order } + + fun revInclude(resourceType: ResourceType, vararg clientParam: ReferenceClientParam) { + revIncludeMap.computeIfAbsent(resourceType) { mutableListOf() }.addAll(clientParam) + } } enum class Order { From df863ac368eb2782da29041c6d53a5e8ab05ecef Mon Sep 17 00:00:00 2001 From: Aditya Khajuria Date: Mon, 24 Apr 2023 12:51:48 +0530 Subject: [PATCH 02/16] Updated test and added doc --- .../android/fhir/db/impl/DatabaseImplTest.kt | 74 ------------------- .../google/android/fhir/search/MoreSearch.kt | 23 +++--- .../com/google/android/fhir/search/Search.kt | 7 ++ .../google/android/fhir/search/SearchDsl.kt | 17 +++++ 4 files changed, 36 insertions(+), 85 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 6ae2ee5ff9..fca7ca4f0f 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 @@ -3077,80 +3077,6 @@ class DatabaseImplTest { assertThat(result.map { it.nameFirstRep.nameAsSingleString }).contains("Darcy Smith") } - @Test - fun search_revInclude(): Unit = runBlocking { - val resources = - listOf( - Patient().apply { id = "pa-01" }, - Patient().apply { id = "pa-02" }, - Patient().apply { id = "pa-04" }, - Encounter().apply { - id = "en-01" - subject = Reference("Patient/pa-01") - }, - Encounter().apply { - id = "en-02" - subject = Reference("Patient/pa-02") - }, - Encounter().apply { - id = "en-03" - subject = Reference("Patient/pa-03") - } - ) - database.insertRemote(*resources.toTypedArray()) - - val result = - database.search( - Search(ResourceType.Patient) - .apply { revInclude(ResourceType.Encounter, Encounter.SUBJECT) } - .getQuery() - ) - assertThat(result.map { it.logicalId }) - .containsExactly("pa-01", "pa-02", "pa-04", "en-01", "en-02", "test_patient_1") - } - - @Test - fun search_patient_revInclude(): Unit = runBlocking { - val resources = - listOf( - Patient().apply { - id = "pa-01" - addName( - HumanName().apply { - addGiven("James") - family = "Gorden" - } - ) - }, - Patient().apply { id = "pa-02" }, - Patient().apply { id = "pa-04" }, - Encounter().apply { - id = "en-01" - subject = Reference("Patient/pa-01") - }, - Encounter().apply { - id = "en-02" - subject = Reference("Patient/pa-02") - }, - Encounter().apply { - id = "en-03" - subject = Reference("Patient/pa-01") - } - ) - database.insertRemote(*resources.toTypedArray()) - - val result = - database.search( - Search(ResourceType.Patient) - .apply { - filter(Patient.GIVEN, { value = "James" }) - revInclude(ResourceType.Encounter, Encounter.SUBJECT) - } - .getQuery() - ) - assertThat(result.map { it.logicalId }).containsExactly("pa-01", "en-01", "en-03") - } - @Test fun search_patient_has_revInclude(): Unit = runBlocking { val diabetesCodeableConcept = 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 7c8bda0e85..9f4c43c22c 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 @@ -170,17 +170,18 @@ internal fun Search.getQuery( } revIncludeMap.isNotEmpty() -> { """ - select serializedResource from ResourceEntity where resourceUuid in ( - with UUIDS as ( select '${type.name}/' || a.resourceId from ResourceEntity a - $sortJoinStatement - WHERE a.resourceType = ? - $filterStatement - $sortOrderStatement - $limitStatement - ) - Select resourceUuid from ResourceEntity where '${type.name}/' || resourceId in UUIDS - ${revIncludeMap.toSQLQuery()} - ) + select serializedResource from ResourceEntity where resourceUuid in ( + with UUIDS as ( select '${type.name}/' || a.resourceId from ResourceEntity a + $sortJoinStatement + WHERE a.resourceType = ? + $filterStatement + $sortOrderStatement + $limitStatement + ) + Select resourceUuid + FROM ResourceEntity + WHERE '${type.name}/' || resourceId in UUIDS ${revIncludeMap.toSQLQuery()} + ) """.trimIndent() } else -> diff --git a/engine/src/main/java/com/google/android/fhir/search/Search.kt b/engine/src/main/java/com/google/android/fhir/search/Search.kt index 26ab0dbd19..39bbe785cb 100644 --- a/engine/src/main/java/com/google/android/fhir/search/Search.kt +++ b/engine/src/main/java/com/google/android/fhir/search/Search.kt @@ -19,6 +19,7 @@ package com.google.android.fhir.search import com.google.android.fhir.FhirEngine import com.google.android.fhir.search.query.XFhirQueryTranslator.translate import org.hl7.fhir.r4.model.Resource +import org.hl7.fhir.r4.model.ResourceType suspend inline fun FhirEngine.search(init: Search.() -> Unit): List { val search = Search(type = R::class.java.newInstance().resourceType) @@ -35,3 +36,9 @@ suspend inline fun FhirEngine.count(init: Search.() -> Un suspend fun FhirEngine.search(xFhirQuery: String): List { return this.search(translate(xFhirQuery)) } + +suspend fun FhirEngine.search(resourceType: ResourceType, init: Search.() -> Unit): List { + val search = Search(type = resourceType) + search.init() + return this.search(search) +} 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 2121d88e11..dc19a0e1d7 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 @@ -144,6 +144,23 @@ data class Search(val type: ResourceType, var count: Int? = null, var from: Int? this.order = order } + /** + * Allows user to include additional resources to be included in the search results that reference + * the resource on which [revInclude] is being called. The developers may call [revInclude] + * multiple times with different [ResourceType] to allow search api to return multiple referenced + * resource types. + * + * e.g. The below example would return all the Patients with given-name as James and their + * associated Encounters and Conditions. + * + * ``` + * fhirEngine.search(Search(ResourceType.Patient).apply { + * filter(Patient.GIVEN, { value = "James" }) + * revInclude(ResourceType.Encounter, Encounter.PATIENT) + * revInclude(ResourceType.Condition, Condition.PATIENT) + * }) + * ``` + */ fun revInclude(resourceType: ResourceType, vararg clientParam: ReferenceClientParam) { revIncludeMap.computeIfAbsent(resourceType) { mutableListOf() }.addAll(clientParam) } From e0f3c0f8131b3d159aa19948989485b3f7567fd8 Mon Sep 17 00:00:00 2001 From: Aditya Khajuria Date: Fri, 28 Apr 2023 13:48:27 +0530 Subject: [PATCH 03/16] RevInclude to return mapped response --- .../com/google/android/fhir/FhirEngine.kt | 4 ++ .../com/google/android/fhir/db/Database.kt | 3 ++ .../android/fhir/db/impl/DatabaseImpl.kt | 12 +++++ .../android/fhir/db/impl/dao/ResourceDao.kt | 16 ++++++ .../android/fhir/impl/FhirEngineImpl.kt | 25 +++++++++ .../google/android/fhir/search/MoreSearch.kt | 54 +++++++++++++------ .../com/google/android/fhir/search/Search.kt | 8 +++ .../google/android/fhir/testing/Utilities.kt | 6 +++ 8 files changed, 112 insertions(+), 16 deletions(-) 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 7d96f52c6e..4924c14a7c 100644 --- a/engine/src/main/java/com/google/android/fhir/FhirEngine.kt +++ b/engine/src/main/java/com/google/android/fhir/FhirEngine.kt @@ -48,6 +48,10 @@ interface FhirEngine { */ suspend fun search(search: Search): List + suspend fun searchWithRevInclude( + search: Search + ): Map>> + /** * Synchronizes the [upload] result in the database. [upload] operation may result in multiple * calls to the server to upload the data. Result of each call will be emitted by [upload] and the 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 4d5f7d8f1b..6a7e4d21b3 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 @@ -16,6 +16,7 @@ package com.google.android.fhir.db +import com.google.android.fhir.db.impl.dao.IndexedIdAndResource import com.google.android.fhir.db.impl.dao.LocalChangeToken import com.google.android.fhir.db.impl.dao.SquashedLocalChange import com.google.android.fhir.db.impl.entities.LocalChangeEntity @@ -94,6 +95,8 @@ internal interface Database { suspend fun search(query: SearchQuery): List + suspend fun searchRev(query: SearchQuery): List + suspend fun count(query: SearchQuery): Long /** 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 c606513ee6..9b0fd900de 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 @@ -25,6 +25,7 @@ import ca.uhn.fhir.parser.IParser import com.google.android.fhir.DatabaseErrorStrategy import com.google.android.fhir.db.ResourceNotFoundException 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.LocalChangeToken import com.google.android.fhir.db.impl.dao.LocalChangeUtils import com.google.android.fhir.db.impl.dao.SquashedLocalChange @@ -186,6 +187,17 @@ internal class DatabaseImpl( } } + override suspend fun searchRev(query: SearchQuery): List { + return db.withTransaction { + resourceDao.getResourcesRev(SimpleSQLiteQuery(query.query, query.args.toTypedArray())).map { + IndexedIdAndResource( + it.idOfBaseResourceOnWhichThisMatched, + iParser.parseResource(it.serializedResource) as Resource + ) + } + } + } + override suspend fun count(query: SearchQuery): Long { return db.withTransaction { resourceDao.countResources(SimpleSQLiteQuery(query.query, query.args.toTypedArray())) 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 15048a0fed..84b64b91ce 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 @@ -17,6 +17,7 @@ package com.google.android.fhir.db.impl.dao import androidx.annotation.VisibleForTesting +import androidx.room.ColumnInfo import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy @@ -150,6 +151,11 @@ internal abstract class ResourceDao { @RawQuery abstract suspend fun getResources(query: SupportSQLiteQuery): List + @RawQuery + abstract suspend fun getResourcesRev( + query: SupportSQLiteQuery + ): List + @RawQuery abstract suspend fun countResources(query: SupportSQLiteQuery): Long private suspend fun insertResource(resource: Resource): String { @@ -279,3 +285,13 @@ internal abstract class ResourceDao { } } } + +internal data class IndexedIdAndSerializedResource( + @ColumnInfo(name = "index_value") val idOfBaseResourceOnWhichThisMatched: String, + val serializedResource: String +) + +internal data class IndexedIdAndResource( + val idOfBaseResourceOnWhichThisMatched: String, + val resource: Resource +) diff --git a/engine/src/main/java/com/google/android/fhir/impl/FhirEngineImpl.kt b/engine/src/main/java/com/google/android/fhir/impl/FhirEngineImpl.kt index 93c9c800cd..06c8688438 100644 --- a/engine/src/main/java/com/google/android/fhir/impl/FhirEngineImpl.kt +++ b/engine/src/main/java/com/google/android/fhir/impl/FhirEngineImpl.kt @@ -27,6 +27,8 @@ import com.google.android.fhir.logicalId import com.google.android.fhir.search.Search import com.google.android.fhir.search.count import com.google.android.fhir.search.execute +import com.google.android.fhir.search.getIncludeQuery +import com.google.android.fhir.search.getQuery import com.google.android.fhir.sync.ConflictResolver import com.google.android.fhir.sync.Resolved import java.time.OffsetDateTime @@ -60,6 +62,29 @@ internal class FhirEngineImpl(private val database: Database, private val contex return search.execute(database) } + override suspend fun searchWithRevInclude( + search: Search + ): Map>> { + val baseResources = database.search(search.getQuery()) // .subList(0,2) + val includedResources = + database.searchRev( + search.getIncludeQuery( + includeIds = baseResources.map { "\'${it.resourceType}/${it.logicalId}\'" } + ) + ) + val resultMap = mutableMapOf>>() + baseResources.forEach { patient -> + resultMap[patient] = + includedResources + .filter { + it.idOfBaseResourceOnWhichThisMatched == "${patient.fhirType()}/${patient.logicalId}" + } + .map { it.resource } + .groupBy { it.resourceType } + } + return resultMap + } + override suspend fun count(search: Search): Long { return search.count(database) } 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 9f4c43c22c..da0443a967 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 @@ -55,6 +55,28 @@ fun Search.getQuery(isCount: Boolean = false): SearchQuery { return getQuery(isCount, null) } +internal fun Search.getIncludeQuery(includeIds: List): SearchQuery { + val match = + revIncludeMap + .map { + " ( a.resourceType = '${it.key}' and a.index_name IN (${it.value.joinToString { "\'${it.paramName}\'" }}) ) " + } + .joinToString(separator = "OR") + + return SearchQuery( + query = + """ + SELECT a.index_value, b.serializedResource + FROM ReferenceIndexEntity a + JOIN ResourceEntity b + ON a.resourceUuid = b.resourceUuid + AND a.index_value IN( ${includeIds.joinToString()} ) + AND ($match) + """.trimIndent(), + args = listOf() + ) +} + internal fun Search.getQuery( isCount: Boolean = false, nestedContext: NestedContext? = null @@ -168,22 +190,22 @@ internal fun Search.getQuery( $limitStatement) """ } - revIncludeMap.isNotEmpty() -> { - """ - select serializedResource from ResourceEntity where resourceUuid in ( - with UUIDS as ( select '${type.name}/' || a.resourceId from ResourceEntity a - $sortJoinStatement - WHERE a.resourceType = ? - $filterStatement - $sortOrderStatement - $limitStatement - ) - Select resourceUuid - FROM ResourceEntity - WHERE '${type.name}/' || resourceId in UUIDS ${revIncludeMap.toSQLQuery()} - ) - """.trimIndent() - } + // revIncludeMap.isNotEmpty() -> { + // """ + // select serializedResource from ResourceEntity where resourceUuid in ( + // with UUIDS as ( select '${type.name}/' || a.resourceId from ResourceEntity a + // $sortJoinStatement + // WHERE a.resourceType = ? + // $filterStatement + // $sortOrderStatement + // $limitStatement + // ) + // Select resourceUuid + // FROM ResourceEntity + // WHERE '${type.name}/' || resourceId in UUIDS ${revIncludeMap.toSQLQuery()} + // ) + // """.trimIndent() + // } else -> """ SELECT a.serializedResource diff --git a/engine/src/main/java/com/google/android/fhir/search/Search.kt b/engine/src/main/java/com/google/android/fhir/search/Search.kt index 39bbe785cb..6b52683de2 100644 --- a/engine/src/main/java/com/google/android/fhir/search/Search.kt +++ b/engine/src/main/java/com/google/android/fhir/search/Search.kt @@ -42,3 +42,11 @@ suspend fun FhirEngine.search(resourceType: ResourceType, init: Search.() -> Uni search.init() return this.search(search) } + +suspend inline fun FhirEngine.searchWithRevInclude( + init: Search.() -> Unit +): Map>> { + val search = Search(type = R::class.java.newInstance().resourceType) + search.init() + return this.searchWithRevInclude(search) +} diff --git a/engine/src/main/java/com/google/android/fhir/testing/Utilities.kt b/engine/src/main/java/com/google/android/fhir/testing/Utilities.kt index e2cde8ad94..f22a408e1a 100644 --- a/engine/src/main/java/com/google/android/fhir/testing/Utilities.kt +++ b/engine/src/main/java/com/google/android/fhir/testing/Utilities.kt @@ -140,6 +140,12 @@ object TestFhirEngineImpl : FhirEngine { return emptyList() } + override suspend fun searchWithRevInclude( + search: Search + ): Map>> { + TODO("Not yet implemented") + } + override suspend fun syncUpload( upload: suspend (List) -> Flow> ) { From 1ccf1087a47c90cbb7cbbe56b7d26d5183c8655e Mon Sep 17 00:00:00 2001 From: Aditya Khajuria Date: Fri, 5 May 2023 04:22:49 +0530 Subject: [PATCH 04/16] Test code for forward include --- .../com/google/android/fhir/FhirEngine.kt | 1 + .../android/fhir/db/impl/DatabaseImpl.kt | 2 +- .../android/fhir/db/impl/dao/ResourceDao.kt | 3 +- .../android/fhir/impl/FhirEngineImpl.kt | 11 +++++-- .../google/android/fhir/search/MoreSearch.kt | 32 ++++++++++++++++++- .../com/google/android/fhir/search/Search.kt | 10 +++++- .../google/android/fhir/search/SearchDsl.kt | 9 ++++-- .../google/android/fhir/testing/Utilities.kt | 1 + 8 files changed, 61 insertions(+), 8 deletions(-) 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 4924c14a7c..5e911ef151 100644 --- a/engine/src/main/java/com/google/android/fhir/FhirEngine.kt +++ b/engine/src/main/java/com/google/android/fhir/FhirEngine.kt @@ -49,6 +49,7 @@ interface FhirEngine { suspend fun search(search: Search): List suspend fun searchWithRevInclude( + isRevInclude: Boolean, search: Search ): Map>> 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 9b0fd900de..4fbb89b1f1 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 @@ -191,7 +191,7 @@ internal class DatabaseImpl( return db.withTransaction { resourceDao.getResourcesRev(SimpleSQLiteQuery(query.query, query.args.toTypedArray())).map { IndexedIdAndResource( - it.idOfBaseResourceOnWhichThisMatched, + it.idOfBaseResourceOnWhichThisMatchedInc ?: it.idOfBaseResourceOnWhichThisMatchedRev!!, 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 84b64b91ce..3195cca2ce 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 @@ -287,7 +287,8 @@ internal abstract class ResourceDao { } internal data class IndexedIdAndSerializedResource( - @ColumnInfo(name = "index_value") val idOfBaseResourceOnWhichThisMatched: String, + @ColumnInfo(name = "index_value") val idOfBaseResourceOnWhichThisMatchedRev: String?, + @ColumnInfo(name = "resourceId") val idOfBaseResourceOnWhichThisMatchedInc: String?, val serializedResource: String ) diff --git a/engine/src/main/java/com/google/android/fhir/impl/FhirEngineImpl.kt b/engine/src/main/java/com/google/android/fhir/impl/FhirEngineImpl.kt index 06c8688438..e52feed2c9 100644 --- a/engine/src/main/java/com/google/android/fhir/impl/FhirEngineImpl.kt +++ b/engine/src/main/java/com/google/android/fhir/impl/FhirEngineImpl.kt @@ -63,13 +63,18 @@ internal class FhirEngineImpl(private val database: Database, private val contex } override suspend fun searchWithRevInclude( + isRevInclude: Boolean, search: Search ): Map>> { val baseResources = database.search(search.getQuery()) // .subList(0,2) val includedResources = database.searchRev( search.getIncludeQuery( - includeIds = baseResources.map { "\'${it.resourceType}/${it.logicalId}\'" } + isRevInclude, + includeIds = + baseResources.map { + if (isRevInclude) "\'${it.resourceType}/${it.logicalId}\'" else "\'${it.logicalId}\'" + } ) ) val resultMap = mutableMapOf>>() @@ -77,7 +82,9 @@ internal class FhirEngineImpl(private val database: Database, private val contex resultMap[patient] = includedResources .filter { - it.idOfBaseResourceOnWhichThisMatched == "${patient.fhirType()}/${patient.logicalId}" + if (isRevInclude) + it.idOfBaseResourceOnWhichThisMatched == "${patient.fhirType()}/${patient.logicalId}" + else it.idOfBaseResourceOnWhichThisMatched == patient.logicalId } .map { it.resource } .groupBy { it.resourceType } 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 da0443a967..c6c125b2c8 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 @@ -55,7 +55,12 @@ fun Search.getQuery(isCount: Boolean = false): SearchQuery { return getQuery(isCount, null) } -internal fun Search.getIncludeQuery(includeIds: List): SearchQuery { +internal fun Search.getIncludeQuery(isRevInclude: Boolean, includeIds: List) = + if (isRevInclude) { + getRevIncludeQuery(includeIds) + } else getIncludeQuery(includeIds) + +private fun Search.getRevIncludeQuery(includeIds: List): SearchQuery { val match = revIncludeMap .map { @@ -77,6 +82,31 @@ internal fun Search.getIncludeQuery(includeIds: List): SearchQuery { ) } +private fun Search.getIncludeQuery(includeIds: List): SearchQuery { + val match = + includeMap + .map { + " ( c.resourceType = '${it.key}' and b.index_name IN (${it.value.joinToString { "\'${it.paramName}\'" }}) ) " + } + .joinToString(separator = "OR") + // SELECT a.resourceType||"/"||a.resourceId, c.serializedResource from ResourceEntity a + + return SearchQuery( + query = + """ + SELECT a.resourceId, c.serializedResource from ResourceEntity a + JOIN ReferenceIndexEntity b + On a.resourceUuid = b.resourceUuid + AND a.resourceType = 'Observation' + AND a.resourceId in ( ${includeIds.joinToString()} ) + JOIN ResourceEntity c + ON c.resourceType||"/"||c.resourceId = b.index_value + AND ($match) + """.trimIndent(), + args = listOf() + ) +} + internal fun Search.getQuery( isCount: Boolean = false, nestedContext: NestedContext? = null diff --git a/engine/src/main/java/com/google/android/fhir/search/Search.kt b/engine/src/main/java/com/google/android/fhir/search/Search.kt index 6b52683de2..d96d4f7bf7 100644 --- a/engine/src/main/java/com/google/android/fhir/search/Search.kt +++ b/engine/src/main/java/com/google/android/fhir/search/Search.kt @@ -48,5 +48,13 @@ suspend inline fun FhirEngine.searchWithRevInclude( ): Map>> { val search = Search(type = R::class.java.newInstance().resourceType) search.init() - return this.searchWithRevInclude(search) + return this.searchWithRevInclude(true, search) +} + +suspend inline fun FhirEngine.searchWithForwardInclude( + init: Search.() -> Unit +): Map>> { + val search = Search(type = R::class.java.newInstance().resourceType) + search.init() + return this.searchWithRevInclude(false, search) } 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 dc19a0e1d7..b8ee1a96b6 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 @@ -54,6 +54,7 @@ data class Search(val type: ResourceType, var count: Int? = null, var from: Int? internal var sort: IParam? = null internal var order: Order? = null internal val revIncludeMap = mutableMapOf>() + internal val includeMap = mutableMapOf>() @PublishedApi internal var nestedSearches = mutableListOf() var operation = Operation.AND @@ -161,8 +162,12 @@ data class Search(val type: ResourceType, var count: Int? = null, var from: Int? * }) * ``` */ - fun revInclude(resourceType: ResourceType, vararg clientParam: ReferenceClientParam) { - revIncludeMap.computeIfAbsent(resourceType) { mutableListOf() }.addAll(clientParam) + fun revInclude(resourceType: ResourceType, clientParam: ReferenceClientParam) { + revIncludeMap.computeIfAbsent(resourceType) { mutableListOf() }.add(clientParam) + } + + fun include(clientParam: ReferenceClientParam, resourceType: ResourceType) { + includeMap.computeIfAbsent(resourceType) { mutableListOf() }.add(clientParam) } } diff --git a/engine/src/main/java/com/google/android/fhir/testing/Utilities.kt b/engine/src/main/java/com/google/android/fhir/testing/Utilities.kt index f22a408e1a..f50821189f 100644 --- a/engine/src/main/java/com/google/android/fhir/testing/Utilities.kt +++ b/engine/src/main/java/com/google/android/fhir/testing/Utilities.kt @@ -141,6 +141,7 @@ object TestFhirEngineImpl : FhirEngine { } override suspend fun searchWithRevInclude( + isRevInclude: Boolean, search: Search ): Map>> { TODO("Not yet implemented") From 543478cfa67247af9af4d00125bb85a746c832a6 Mon Sep 17 00:00:00 2001 From: Aditya Khajuria Date: Fri, 5 May 2023 21:31:55 +0530 Subject: [PATCH 05/16] Fixed include query --- .../src/main/java/com/google/android/fhir/search/MoreSearch.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c6c125b2c8..a81081c4e1 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 @@ -97,7 +97,7 @@ private fun Search.getIncludeQuery(includeIds: List): SearchQuery { SELECT a.resourceId, c.serializedResource from ResourceEntity a JOIN ReferenceIndexEntity b On a.resourceUuid = b.resourceUuid - AND a.resourceType = 'Observation' + AND a.resourceType = '${type.name}' AND a.resourceId in ( ${includeIds.joinToString()} ) JOIN ResourceEntity c ON c.resourceType||"/"||c.resourceId = b.index_value From 379ed166551113e81f947d9cfb9ff1cb99fcf0ef Mon Sep 17 00:00:00 2001 From: Aditya Khajuria Date: Thu, 29 Jun 2023 15:29:34 +0530 Subject: [PATCH 06/16] Unify search api to include include and revInclude functionality --- .../android/fhir/demo/FhirApplication.kt | 2 +- .../fhir/demo/PatientDetailsViewModel.kt | 181 ++++---- .../android/fhir/demo/PatientListViewModel.kt | 28 +- .../android/fhir/db/impl/DatabaseImplTest.kt | 400 +++++++++++++----- .../com/google/android/fhir/FhirEngine.kt | 19 +- .../com/google/android/fhir/db/Database.kt | 2 +- .../android/fhir/db/impl/DatabaseImpl.kt | 2 +- .../android/fhir/impl/FhirEngineImpl.kt | 35 +- .../com/google/android/fhir/search/ISearch.kt | 88 ++++ .../google/android/fhir/search/MoreSearch.kt | 198 ++++++--- .../android/fhir/search/NestedSearch.kt | 126 +++++- .../com/google/android/fhir/search/Search.kt | 28 +- .../google/android/fhir/search/SearchDsl.kt | 68 +-- .../android/fhir/search/SearchDslMarker.kt | 6 +- .../google/android/fhir/testing/Utilities.kt | 10 +- 15 files changed, 791 insertions(+), 402 deletions(-) create mode 100644 engine/src/main/java/com/google/android/fhir/search/ISearch.kt diff --git a/demo/src/main/java/com/google/android/fhir/demo/FhirApplication.kt b/demo/src/main/java/com/google/android/fhir/demo/FhirApplication.kt index cfb30758c7..5d04b6d317 100644 --- a/demo/src/main/java/com/google/android/fhir/demo/FhirApplication.kt +++ b/demo/src/main/java/com/google/android/fhir/demo/FhirApplication.kt @@ -68,7 +68,7 @@ class FhirApplication : Application(), DataCaptureConfig.Provider { dataCaptureConfig = DataCaptureConfig().apply { urlResolver = ReferenceUrlResolver(this@FhirApplication as Context) - xFhirQueryResolver = XFhirQueryResolver { fhirEngine.search(it) } + xFhirQueryResolver = XFhirQueryResolver { fhirEngine.search(it).map { it.resource } } } } diff --git a/demo/src/main/java/com/google/android/fhir/demo/PatientDetailsViewModel.kt b/demo/src/main/java/com/google/android/fhir/demo/PatientDetailsViewModel.kt index 7f4397678a..275e32afec 100644 --- a/demo/src/main/java/com/google/android/fhir/demo/PatientDetailsViewModel.kt +++ b/demo/src/main/java/com/google/android/fhir/demo/PatientDetailsViewModel.kt @@ -27,8 +27,8 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.google.android.fhir.FhirEngine -import com.google.android.fhir.get import com.google.android.fhir.logicalId +import com.google.android.fhir.search.revInclude import com.google.android.fhir.search.search import java.text.SimpleDateFormat import java.time.LocalDate @@ -41,6 +41,8 @@ import org.apache.commons.lang3.StringUtils import org.hl7.fhir.r4.model.Condition import org.hl7.fhir.r4.model.Observation import org.hl7.fhir.r4.model.Patient +import org.hl7.fhir.r4.model.Resource +import org.hl7.fhir.r4.model.ResourceType import org.hl7.fhir.r4.model.RiskAssessment import org.hl7.fhir.r4.model.codesystems.RiskProbability @@ -60,108 +62,128 @@ class PatientDetailsViewModel( viewModelScope.launch { livePatientData.value = getPatientDetailDataModel() } } - private suspend fun getPatient(): PatientListViewModel.PatientItem { - val patient = fhirEngine.get(patientId) - return patient.toPatientItem(0) - } + private suspend fun getPatientDetailDataModel(): List { + val searchResult = + fhirEngine.search { + filter(Resource.RES_ID, { value = of(patientId) }) - private suspend fun getPatientObservations(): List { - val observations: MutableList = mutableListOf() - fhirEngine - .search { filter(Observation.SUBJECT, { value = "Patient/$patientId" }) } - .take(MAX_RESOURCE_COUNT) - .map { createObservationItem(it, getApplication().resources) } - .let { observations.addAll(it) } - return observations - } + revInclude(RiskAssessment.SUBJECT) + revInclude(Observation.SUBJECT) + revInclude(Condition.SUBJECT) + } + val data = mutableListOf() - private suspend fun getPatientConditions(): List { - val conditions: MutableList = mutableListOf() - fhirEngine - .search { filter(Condition.SUBJECT, { value = "Patient/$patientId" }) } - .take(MAX_RESOURCE_COUNT) - .map { createConditionItem(it, getApplication().resources) } - .let { conditions.addAll(it) } - return conditions - } + searchResult.first().let { + data.addPatientDetailData( + it.resource, + getRiskItem(it.revIncluded?.get(ResourceType.RiskAssessment) as List?) + ) - private suspend fun getPatientDetailDataModel(): List { - val data = mutableListOf() - val patient = getPatient() - patient.riskItem = getPatientRiskAssessment() + it.revIncluded?.get(ResourceType.Observation)?.let { + data.addObservationsData(it as List) + } + it.revIncluded?.get(ResourceType.Condition)?.let { + data.addConditionsData(it as List) + } + } + return data + } - val observations = getPatientObservations() - val conditions = getPatientConditions() + private fun getRiskItem(riskAssessments: List?) = + riskAssessments + ?.filter { it.hasOccurrence() } + ?.maxByOrNull { it.occurrenceDateTimeType.value } + .let { + RiskAssessmentItem( + getRiskAssessmentStatusColor(it), + getRiskAssessmentStatus(it), + getLastContactedDate(it), + getPatientDetailsCardColor(it) + ) + } - patient.let { patientItem -> - data.add(PatientDetailOverview(patientItem, firstInGroup = true)) - data.add( - PatientDetailProperty( - PatientProperty(getString(R.string.patient_property_mobile), patientItem.phone) + private fun MutableList.addPatientDetailData( + patient: Patient, + riskAssessment: RiskAssessmentItem + ) { + patient + .toPatientItem(0) + .apply { riskItem = riskAssessment } + .let { patientItem -> + add(PatientDetailOverview(patientItem, firstInGroup = true)) + add( + PatientDetailProperty( + PatientProperty(getString(R.string.patient_property_mobile), patientItem.phone) + ) ) - ) - data.add( - PatientDetailProperty( - PatientProperty(getString(R.string.patient_property_id), patientItem.resourceId) + add( + PatientDetailProperty( + PatientProperty(getString(R.string.patient_property_id), patientItem.resourceId) + ) ) - ) - data.add( - PatientDetailProperty( - PatientProperty( - getString(R.string.patient_property_address), - "${patientItem.city}, ${patientItem.country} " + add( + PatientDetailProperty( + PatientProperty( + getString(R.string.patient_property_address), + "${patientItem.city}, ${patientItem.country} " + ) ) ) - ) - data.add( - PatientDetailProperty( - PatientProperty( - getString(R.string.patient_property_dob), - patientItem.dob?.localizedString ?: "" + add( + PatientDetailProperty( + PatientProperty( + getString(R.string.patient_property_dob), + patientItem.dob?.localizedString ?: "" + ) ) ) - ) - data.add( - PatientDetailProperty( - PatientProperty( - getString(R.string.patient_property_gender), - patientItem.gender.replaceFirstChar { - if (it.isLowerCase()) it.titlecase(Locale.ROOT) else it.toString() - } - ), - lastInGroup = true + add( + PatientDetailProperty( + PatientProperty( + getString(R.string.patient_property_gender), + patientItem.gender.replaceFirstChar { + if (it.isLowerCase()) it.titlecase(Locale.ROOT) else it.toString() + } + ), + lastInGroup = true + ) ) - ) - } + } + } + private fun MutableList.addObservationsData(observations: List) { if (observations.isNotEmpty()) { - data.add(PatientDetailHeader(getString(R.string.header_observation))) + add(PatientDetailHeader(getString(R.string.header_observation))) - val observationDataModel = - observations.mapIndexed { index, observationItem -> + observations + .take(MAX_RESOURCE_COUNT) + .map { createObservationItem(it, getApplication().resources) } + .mapIndexed { index, observationItem -> PatientDetailObservation( observationItem, firstInGroup = index == 0, lastInGroup = index == observations.size - 1 ) } - data.addAll(observationDataModel) + .let { addAll(it) } } + } + private fun MutableList.addConditionsData(conditions: List) { if (conditions.isNotEmpty()) { - data.add(PatientDetailHeader(getString(R.string.header_conditions))) - val conditionDataModel = - conditions.mapIndexed { index, conditionItem -> + add(PatientDetailHeader(getString(R.string.header_conditions))) + conditions + .take(MAX_RESOURCE_COUNT) + .map { createConditionItem(it, getApplication().resources) } + .mapIndexed { index, conditionItem -> PatientDetailCondition( conditionItem, firstInGroup = index == 0, lastInGroup = index == conditions.size - 1 ) } - data.addAll(conditionDataModel) + .let { addAll(it) } } - - return data } private val LocalDate.localizedString: String @@ -177,21 +199,6 @@ class PatientDetailsViewModel( private fun getString(resId: Int) = getApplication().resources.getString(resId) - private suspend fun getPatientRiskAssessment(): RiskAssessmentItem { - val riskAssessment = - fhirEngine - .search { filter(RiskAssessment.SUBJECT, { value = "Patient/$patientId" }) } - .filter { it.hasOccurrence() } - .sortedByDescending { it.occurrenceDateTimeType.value } - .firstOrNull() - return RiskAssessmentItem( - getRiskAssessmentStatusColor(riskAssessment), - getRiskAssessmentStatus(riskAssessment), - getLastContactedDate(riskAssessment), - getPatientDetailsCardColor(riskAssessment) - ) - } - private fun getRiskAssessmentStatusColor(riskAssessment: RiskAssessment?): Int { riskAssessment?.let { return when (it.prediction.first().qualitativeRisk.coding.first().code) { diff --git a/demo/src/main/java/com/google/android/fhir/demo/PatientListViewModel.kt b/demo/src/main/java/com/google/android/fhir/demo/PatientListViewModel.kt index a9244ef4ba..9c2e3dc713 100644 --- a/demo/src/main/java/com/google/android/fhir/demo/PatientListViewModel.kt +++ b/demo/src/main/java/com/google/android/fhir/demo/PatientListViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Google LLC + * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,6 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.google.android.fhir.FhirEngine import com.google.android.fhir.search.Order -import com.google.android.fhir.search.Search import com.google.android.fhir.search.StringFilterModifier import com.google.android.fhir.search.count import com.google.android.fhir.search.search @@ -82,7 +81,6 @@ class PatientListViewModel(application: Application, private val fhirEngine: Fhi } ) } - filterCity(this) } } @@ -99,12 +97,11 @@ class PatientListViewModel(application: Application, private val fhirEngine: Fhi } ) } - filterCity(this) sort(Patient.GIVEN, Order.ASCENDING) count = 100 from = 0 } - .mapIndexed { index, fhirPatient -> fhirPatient.toPatientItem(index + 1) } + .mapIndexed { index, fhirPatient -> fhirPatient.resource.toPatientItem(index + 1) } .let { patients.addAll(it) } val risks = getRiskAssessments() @@ -116,19 +113,16 @@ class PatientListViewModel(application: Application, private val fhirEngine: Fhi return patients } - private fun filterCity(search: Search) { - search.filter(Patient.ADDRESS_CITY, { value = "NAIROBI" }) - } - private suspend fun getRiskAssessments(): Map { - return fhirEngine.search {}.groupBy { it.subject.reference }.mapValues { entry - -> - entry - .value - .filter { it.hasOccurrence() } - .sortedByDescending { it.occurrenceDateTimeType.value } - .firstOrNull() - } + return fhirEngine + .search {} + .groupBy { it.resource.subject.reference } + .mapValues { entry -> + entry.value + .filter { it.resource.hasOccurrence() } + .maxByOrNull { it.resource.occurrenceDateTimeType.value } + ?.resource + } } /** The Patient's details for display purposes. */ 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 fca7ca4f0f..053be87a0a 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 @@ -33,8 +33,11 @@ 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 +import com.google.android.fhir.search.execute import com.google.android.fhir.search.getQuery import com.google.android.fhir.search.has +import com.google.android.fhir.search.include +import com.google.android.fhir.search.revInclude import com.google.android.fhir.testing.assertJsonArrayEqualsIgnoringOrder import com.google.android.fhir.testing.assertResourceEquals import com.google.android.fhir.testing.readFromFile @@ -62,11 +65,12 @@ import org.hl7.fhir.r4.model.Identifier import org.hl7.fhir.r4.model.Immunization import org.hl7.fhir.r4.model.Meta 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.Period import org.hl7.fhir.r4.model.Practitioner import org.hl7.fhir.r4.model.Quantity import org.hl7.fhir.r4.model.Reference -import org.hl7.fhir.r4.model.Resource import org.hl7.fhir.r4.model.ResourceType import org.hl7.fhir.r4.model.RiskAssessment import org.hl7.fhir.r4.model.SearchParameter @@ -2560,96 +2564,6 @@ class DatabaseImplTest { assertThat(result.map { it.logicalId }).containsExactly("100").inOrder() } - @Test - fun search_practitioner_has_patient_has_conditions_diabetes_and_hypertension() = runBlocking { - // Running this test with more resources than required to try and hit all the cases - // patient 1 has 2 practitioners & both conditions - // patient 2 has both conditions but no associated practitioner - // patient 3 has 1 practitioner & 1 condition - val diabetesCodeableConcept = - CodeableConcept(Coding("http://snomed.info/sct", "44054006", "Diabetes")) - val hyperTensionCodeableConcept = - CodeableConcept(Coding("http://snomed.info/sct", "827069000", "Hypertension stage 1")) - 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 = - database.search( - Search(ResourceType.Practitioner) - .apply { - has(Patient.GENERAL_PRACTITIONER) { - has(Condition.SUBJECT) { - filter( - Condition.CODE, - { value = of(Coding("http://snomed.info/sct", "44054006", "Diabetes")) } - ) - } - } - has(Patient.GENERAL_PRACTITIONER) { - has(Condition.SUBJECT) { - filter( - Condition.CODE, - { - value = - of(Coding("http://snomed.info/sct", "827069000", "Hypertension stage 1")) - } - ) - } - } - } - .getQuery() - ) - - assertThat(result.map { it.logicalId }) - .containsExactly("practitioner-001", "practitioner-002") - .inOrder() - } - @Test fun search_sortDescending_Date(): Unit = runBlocking { database.insert( @@ -3078,12 +2992,15 @@ class DatabaseImplTest { } @Test - fun search_patient_has_revInclude(): Unit = runBlocking { + fun search_patient_with_reference_resources(): Unit = runBlocking { val diabetesCodeableConcept = CodeableConcept(Coding("http://snomed.info/sct", "44054006", "Diabetes")) val hyperTensionCodeableConcept = CodeableConcept(Coding("http://snomed.info/sct", "827069000", "Hypertension stage 1")) - val resources = + val migraineCodeableConcept = + CodeableConcept(Coding("http://snomed.info/sct", "37796009", "Migraine")) + + val patients = listOf( Patient().apply { id = "pa-01" @@ -3093,6 +3010,10 @@ class DatabaseImplTest { family = "Gorden" } ) + addGeneralPractitioner(Reference("Practitioner/gp-01")) + addGeneralPractitioner(Reference("Practitioner/gp-02")) + addGeneralPractitioner(Reference("Practitioner/gp-03")) + managingOrganization = Reference("Organization/org-01") }, Patient().apply { id = "pa-02" @@ -3102,46 +3023,293 @@ class DatabaseImplTest { family = "Bond" } ) + addGeneralPractitioner(Reference("Practitioner/gp-01")) + addGeneralPractitioner(Reference("Practitioner/gp-02")) + addGeneralPractitioner(Reference("Practitioner/gp-03")) + managingOrganization = Reference("Organization/org-02") }, - Patient().apply { id = "pa-04" }, - Encounter().apply { - id = "en-01" + Patient().apply { + id = "pa-03" + addName( + HumanName().apply { + addGiven("John") + family = "Doe" + } + ) + addGeneralPractitioner(Reference("Practitioner/gp-01")) + addGeneralPractitioner(Reference("Practitioner/gp-02")) + addGeneralPractitioner(Reference("Practitioner/gp-03")) + managingOrganization = Reference("Organization/org-03") + } + ) + + val practitioners = + listOf( + Practitioner().apply { + id = "gp-01" + addName( + HumanName().apply { + family = "Practitioner-01" + addGiven("General-01") + } + ) + active = true + }, + Practitioner().apply { + id = "gp-02" + addName( + HumanName().apply { + family = "Practitioner-02" + addGiven("General-02") + } + ) + active = true + }, + Practitioner().apply { + id = "gp-03" + addName( + HumanName().apply { + family = "Practitioner-03" + addGiven("General-03") + } + ) + active = false + } + ) + + val organizations = + listOf( + Organization().apply { + id = "org-01" + name = "Organization-01" + active = true + }, + Organization().apply { + id = "org-02" + name = "Organization-02" + active = true + }, + Organization().apply { + id = "org-03" + name = "Organization-03" + active = false + } + ) + + val conditions = + listOf( + Condition().apply { + id = "con-01-pa-01" + code = diabetesCodeableConcept subject = Reference("Patient/pa-01") }, - Encounter().apply { - id = "en-02" - subject = Reference("Patient/pa-02") + Condition().apply { + id = "con-02-pa-01" + code = hyperTensionCodeableConcept + subject = Reference("Patient/pa-01") }, - Encounter().apply { - id = "en-03" + Condition().apply { + id = "con-03-pa-01" + code = migraineCodeableConcept subject = Reference("Patient/pa-01") }, Condition().apply { - id = "con-01" + id = "con-01-pa-02" code = diabetesCodeableConcept - subject = Reference("Patient/pa-01") + subject = Reference("Patient/pa-02") }, Condition().apply { - id = "con-02" + id = "con-02-pa-02" code = hyperTensionCodeableConcept subject = Reference("Patient/pa-02") + }, + Condition().apply { + id = "con-03-pa-02" + code = migraineCodeableConcept + subject = Reference("Patient/pa-02") + }, + Condition().apply { + id = "con-01-pa-03" + code = diabetesCodeableConcept + subject = Reference("Patient/pa-03") + }, + Condition().apply { + id = "con-02-pa-03" + code = hyperTensionCodeableConcept + subject = Reference("Patient/pa-03") + }, + Condition().apply { + id = "con-03-pa-03" + code = migraineCodeableConcept + subject = Reference("Patient/pa-03") + }, + ) + + val encounters = + listOf( + Encounter().apply { + id = "en-01-pa-01" + subject = Reference("Patient/pa-01") + period = + Period().apply { + start = DateType(2023, 2, 1).value + end = DateType(2023, 11, 1).value + } + }, + Encounter().apply { + id = "en-02-pa-01" + subject = Reference("Patient/pa-01") + period = + Period().apply { + start = DateType(2023, 2, 1).value + end = DateType(2023, 11, 1).value + } + }, + Encounter().apply { + id = "en-03-pa-01" + subject = Reference("Patient/pa-01") + period = + Period().apply { + start = DateType(2022, 2, 1).value + end = DateType(2022, 11, 1).value + } + }, + Encounter().apply { + id = "en-01-pa-02" + subject = Reference("Patient/pa-02") + period = + Period().apply { + start = DateType(2023, 2, 1).value + end = DateType(2023, 11, 1).value + } + }, + Encounter().apply { + id = "en-02-pa-02" + subject = Reference("Patient/pa-02") + period = + Period().apply { + start = DateType(2023, 2, 1).value + end = DateType(2023, 11, 1).value + } + }, + Encounter().apply { + id = "en-03-pa-02" + subject = Reference("Patient/pa-02") + period = + Period().apply { + start = DateType(2022, 2, 1).value + end = DateType(2022, 11, 1).value + } + }, + Encounter().apply { + id = "en-01-pa-03" + subject = Reference("Patient/pa-03") + period = + Period().apply { + start = DateType(2023, 2, 1).value + end = DateType(2023, 11, 1).value + } + }, + Encounter().apply { + id = "en-02-pa-03" + subject = Reference("Patient/pa-03") + period = + Period().apply { + start = DateType(2023, 2, 1).value + end = DateType(2023, 11, 1).value + } + }, + Encounter().apply { + id = "en-03-pa-03" + subject = Reference("Patient/pa-03") + period = + Period().apply { + start = DateType(2022, 2, 1).value + end = DateType(2022, 11, 1).value + } } ) - database.insertRemote(*resources.toTypedArray()) + // 3 Patients. + // Each has 3 conditions, only 2 should match + // Each has 3 encounters, only 2 should match + + // Each has 3 GP, only 2 should match + database.insertRemote( + *(patients + practitioners + organizations + conditions + encounters).toTypedArray() + ) val result = - database.search( - Search(ResourceType.Patient) - .apply { - filter(Patient.GIVEN, { value = "James" }) - has(Condition.SUBJECT) { - filter(Condition.CODE, { value = of(diabetesCodeableConcept) }) - } - revInclude(ResourceType.Encounter, Encounter.SUBJECT) + Search(ResourceType.Patient) + .apply { + revInclude(Condition.SUBJECT) { + filter(Condition.CODE, { value = of(diabetesCodeableConcept) }) + filter(Condition.CODE, { value = of(migraineCodeableConcept) }) + operations(Operation.OR) } - .getQuery() - ) - assertThat(result.map { it.logicalId }).containsExactly("pa-01", "en-01", "en-03") + + revInclude(Encounter.SUBJECT) { + filter( + Encounter.DATE, + { + value = of(DateTimeType("2023-01-01")) + prefix = ParamPrefixEnum.GREATERTHAN_OR_EQUALS + } + ) + } + + include(Patient.GENERAL_PRACTITIONER) { + filter(Practitioner.ACTIVE, { value = of(true) }) + filter( + Practitioner.FAMILY, + { + value = "Practitioner" + modifier = StringFilterModifier.STARTS_WITH + } + ) + operations(Operation.AND) + } + + include(Patient.ORGANIZATION) { + filter( + Organization.NAME, + { + value = "Organization" + modifier = StringFilterModifier.STARTS_WITH + } + ) + filter(Practitioner.ACTIVE, { value = of(true) }) + operations(Operation.AND) + } + } + .execute(database) + + assertThat(result[0].resource.logicalId).isEqualTo("pa-01") + assertThat(result[0].included!![ResourceType.Practitioner]!!.map { it.logicalId }) + .containsExactly("gp-01", "gp-02") + assertThat(result[0].included!![ResourceType.Organization]!!.map { it.logicalId }) + .containsExactly("org-01") + assertThat(result[0].revIncluded!![ResourceType.Condition]!!.map { it.logicalId }) + .containsExactly("con-01-pa-01", "con-03-pa-01") + assertThat(result[0].revIncluded!![ResourceType.Encounter]!!.map { it.logicalId }) + .containsExactly("en-01-pa-01", "en-02-pa-01") + + assertThat(result[1].resource.logicalId).isEqualTo("pa-02") + assertThat(result[1].included!![ResourceType.Practitioner]!!.map { it.logicalId }) + .containsExactly("gp-01", "gp-02") + assertThat(result[1].included!![ResourceType.Organization]!!.map { it.logicalId }) + .containsExactly("org-02") + assertThat(result[1].revIncluded!![ResourceType.Condition]!!.map { it.logicalId }) + .containsExactly("con-01-pa-02", "con-03-pa-02") + assertThat(result[1].revIncluded!![ResourceType.Encounter]!!.map { it.logicalId }) + .containsExactly("en-01-pa-02", "en-02-pa-02") + + assertThat(result[2].resource.logicalId).isEqualTo("pa-03") + assertThat(result[2].included!![ResourceType.Practitioner]!!.map { it.logicalId }) + .containsExactly("gp-01", "gp-02") + assertThat(result[2].revIncluded!![ResourceType.Condition]!!.map { it.logicalId }) + .containsExactly("con-01-pa-03", "con-03-pa-03") + assertThat(result[2].revIncluded!![ResourceType.Encounter]!!.map { it.logicalId }) + .containsExactly("en-01-pa-03", "en-02-pa-03") } private companion object { 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 5e911ef151..b7cb20f460 100644 --- a/engine/src/main/java/com/google/android/fhir/FhirEngine.kt +++ b/engine/src/main/java/com/google/android/fhir/FhirEngine.kt @@ -46,12 +46,7 @@ interface FhirEngine { /** * Searches the database and returns a list resources according to the [search] specifications. */ - suspend fun search(search: Search): List - - suspend fun searchWithRevInclude( - isRevInclude: Boolean, - search: Search - ): Map>> + suspend fun search(search: Search): List> /** * Synchronizes the [upload] result in the database. [upload] operation may result in multiple @@ -132,3 +127,15 @@ suspend inline fun FhirEngine.get(id: String): R { suspend inline fun FhirEngine.delete(id: String) { delete(getResourceType(R::class.java), id) } + +typealias ReferencedResources = Map> + +/** It contains the searched resource and referenced resources as per the search query. */ +data class SearchResult( + /** Matching resource as per the query. */ + val resource: R, + /** Matching referenced resources as per the [Search.include] criteria in the query. */ + val included: ReferencedResources?, + /** Matching referenced resources as per the [Search.revInclude] criteria in the query. */ + val revIncluded: ReferencedResources? +) 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 6a7e4d21b3..0cd93251d4 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 @@ -95,7 +95,7 @@ internal interface Database { suspend fun search(query: SearchQuery): List - suspend fun searchRev(query: SearchQuery): List + suspend fun searchReferencedResources(query: SearchQuery): List suspend fun count(query: SearchQuery): Long 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 c079fbbec0..d6a192ea46 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 @@ -187,7 +187,7 @@ internal class DatabaseImpl( } } - override suspend fun searchRev(query: SearchQuery): List { + override suspend fun searchReferencedResources(query: SearchQuery): List { return db.withTransaction { resourceDao.getResourcesRev(SimpleSQLiteQuery(query.query, query.args.toTypedArray())).map { IndexedIdAndResource( diff --git a/engine/src/main/java/com/google/android/fhir/impl/FhirEngineImpl.kt b/engine/src/main/java/com/google/android/fhir/impl/FhirEngineImpl.kt index ec16362721..74d3d73d2e 100644 --- a/engine/src/main/java/com/google/android/fhir/impl/FhirEngineImpl.kt +++ b/engine/src/main/java/com/google/android/fhir/impl/FhirEngineImpl.kt @@ -20,6 +20,7 @@ import android.content.Context import com.google.android.fhir.DatastoreUtil import com.google.android.fhir.FhirEngine import com.google.android.fhir.LocalChange +import com.google.android.fhir.SearchResult import com.google.android.fhir.db.Database import com.google.android.fhir.db.impl.dao.LocalChangeToken import com.google.android.fhir.db.impl.dao.toLocalChange @@ -27,8 +28,6 @@ import com.google.android.fhir.logicalId import com.google.android.fhir.search.Search import com.google.android.fhir.search.count import com.google.android.fhir.search.execute -import com.google.android.fhir.search.getIncludeQuery -import com.google.android.fhir.search.getQuery import com.google.android.fhir.sync.ConflictResolver import com.google.android.fhir.sync.Resolved import java.time.OffsetDateTime @@ -58,40 +57,10 @@ internal class FhirEngineImpl(private val database: Database, private val contex database.delete(type, id) } - override suspend fun search(search: Search): List { + override suspend fun search(search: Search): List> { return search.execute(database) } - override suspend fun searchWithRevInclude( - isRevInclude: Boolean, - search: Search - ): Map>> { - val baseResources = database.search(search.getQuery()) // .subList(0,2) - val includedResources = - database.searchRev( - search.getIncludeQuery( - isRevInclude, - includeIds = - baseResources.map { - if (isRevInclude) "\'${it.resourceType}/${it.logicalId}\'" else "\'${it.logicalId}\'" - } - ) - ) - val resultMap = mutableMapOf>>() - baseResources.forEach { patient -> - resultMap[patient] = - includedResources - .filter { - if (isRevInclude) - it.idOfBaseResourceOnWhichThisMatched == "${patient.fhirType()}/${patient.logicalId}" - else it.idOfBaseResourceOnWhichThisMatched == patient.logicalId - } - .map { it.resource } - .groupBy { it.resourceType } - } - return resultMap - } - override suspend fun count(search: Search): Long { return search.count(database) } diff --git a/engine/src/main/java/com/google/android/fhir/search/ISearch.kt b/engine/src/main/java/com/google/android/fhir/search/ISearch.kt new file mode 100644 index 0000000000..4e0f3d8dcc --- /dev/null +++ b/engine/src/main/java/com/google/android/fhir/search/ISearch.kt @@ -0,0 +1,88 @@ +/* + * Copyright 2022 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 + +import ca.uhn.fhir.rest.gclient.DateClientParam +import ca.uhn.fhir.rest.gclient.NumberClientParam +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.gclient.UriClientParam +import com.google.android.fhir.search.filter.DateParamFilterCriterion +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 com.google.android.fhir.search.filter.UriParamFilterCriterion + +@DslMarker @Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE) annotation class ISearchDsl + +@ISearchDsl +interface ISearch { + + fun filter( + stringParameter: StringClientParam, + vararg init: StringParamFilterCriterion.() -> Unit, + operation: Operation = Operation.OR + ) + + fun filter( + referenceParameter: ReferenceClientParam, + vararg init: ReferenceParamFilterCriterion.() -> Unit, + operation: Operation = Operation.OR + ) + + fun filter( + dateParameter: DateClientParam, + vararg init: DateParamFilterCriterion.() -> Unit, + operation: Operation = Operation.OR + ) + + fun filter( + quantityParameter: QuantityClientParam, + vararg init: QuantityParamFilterCriterion.() -> Unit, + operation: Operation = Operation.OR + ) + + fun filter( + tokenParameter: TokenClientParam, + vararg init: TokenParamFilterCriterion.() -> Unit, + operation: Operation = Operation.OR + ) + + fun filter( + numberParameter: NumberClientParam, + vararg init: NumberParamFilterCriterion.() -> Unit, + operation: Operation = Operation.OR + ) + + fun filter( + uriParam: UriClientParam, + vararg init: UriParamFilterCriterion.() -> Unit, + operation: Operation = Operation.OR + ) + + fun sort(parameter: StringClientParam, order: Order) + + fun sort(parameter: NumberClientParam, order: Order) + + fun sort(parameter: DateClientParam, order: Order) + + fun operations(operation: Operation) +} 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 a81081c4e1..808139905c 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 @@ -18,15 +18,16 @@ package com.google.android.fhir.search import ca.uhn.fhir.rest.gclient.DateClientParam import ca.uhn.fhir.rest.gclient.NumberClientParam -import ca.uhn.fhir.rest.gclient.ReferenceClientParam import ca.uhn.fhir.rest.gclient.StringClientParam import ca.uhn.fhir.rest.param.ParamPrefixEnum import com.google.android.fhir.ConverterException import com.google.android.fhir.DateProvider +import com.google.android.fhir.SearchResult 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.logicalId import com.google.android.fhir.ucumUrl import java.math.BigDecimal import java.util.Date @@ -35,7 +36,6 @@ 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 org.hl7.fhir.r4.model.ResourceType /** * The multiplier used to determine the range for the `ap` search prefix. See @@ -43,8 +43,43 @@ import org.hl7.fhir.r4.model.ResourceType */ private const val APPROXIMATION_COEFFICIENT = 0.1 -internal suspend fun Search.execute(database: Database): List { - return database.search(getQuery()) +internal suspend fun Search.execute(database: Database): List> { + val baseResources = database.search(getQuery()) + val includedResources = + if (forwardIncludes.isEmpty() || baseResources.isEmpty()) { + null + } else { + database.searchReferencedResources( + getIncludeQuery(includeIds = baseResources.map { it.logicalId }) + ) + } + val revIncludedResources = + if (revIncludes.isEmpty() || baseResources.isEmpty()) { + null + } else { + database.searchReferencedResources( + getRevIncludeQuery(includeIds = baseResources.map { "${it.resourceType}/${it.logicalId}" }) + ) + } + + return baseResources.map { baseResource -> + SearchResult( + baseResource, + included = + includedResources + ?.filter { it.idOfBaseResourceOnWhichThisMatched == baseResource.logicalId } + ?.map { it.resource } + ?.groupBy { it.resourceType }, + revIncluded = + revIncludedResources + ?.filter { + it.idOfBaseResourceOnWhichThisMatched == + "${baseResource.fhirType()}/${baseResource.logicalId}" + } + ?.map { it.resource } + ?.groupBy { it.resourceType } + ) + } } internal suspend fun Search.count(database: Database): Long { @@ -55,41 +90,99 @@ fun Search.getQuery(isCount: Boolean = false): SearchQuery { return getQuery(isCount, null) } -internal fun Search.getIncludeQuery(isRevInclude: Boolean, includeIds: List) = - if (isRevInclude) { - getRevIncludeQuery(includeIds) - } else getIncludeQuery(includeIds) - private fun Search.getRevIncludeQuery(includeIds: List): SearchQuery { - val match = - revIncludeMap - .map { - " ( a.resourceType = '${it.key}' and a.index_name IN (${it.value.joinToString { "\'${it.paramName}\'" }}) ) " - } - .joinToString(separator = "OR") + var matchQuery = "" + val args = mutableListOf() + args.addAll(includeIds) + + // creating the match and filter query + revIncludes.forEachIndexed { index, (param, search) -> + val resourceToInclude = search.type + args.add(resourceToInclude.name) + args.add(param.paramName) + matchQuery += " ( a.resourceType = ? and a.index_name IN (?) " + + val allFilters = search.getFilterQueries() + + if (allFilters.isNotEmpty()) { + val iterator = allFilters.listIterator() + matchQuery += "AND b.resourceUuid IN (\n" + do { + iterator.next().let { + matchQuery += 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)" + } + + matchQuery += " \n)" + + if (index != revIncludes.lastIndex) matchQuery += " OR " + } return SearchQuery( query = """ - SELECT a.index_value, b.serializedResource + SELECT a.index_value, b.serializedResource FROM ReferenceIndexEntity a JOIN ResourceEntity b ON a.resourceUuid = b.resourceUuid - AND a.index_value IN( ${includeIds.joinToString()} ) - AND ($match) + AND a.index_value IN( ${ CharArray(includeIds.size) { '?' }.joinToString()} ) + ${if (matchQuery.isEmpty()) "" else "AND ($matchQuery) " } """.trimIndent(), - args = listOf() + args = args ) } private fun Search.getIncludeQuery(includeIds: List): SearchQuery { - val match = - includeMap - .map { - " ( c.resourceType = '${it.key}' and b.index_name IN (${it.value.joinToString { "\'${it.paramName}\'" }}) ) " - } - .joinToString(separator = "OR") - // SELECT a.resourceType||"/"||a.resourceId, c.serializedResource from ResourceEntity a + var matchQuery = "" + val args = mutableListOf(type.name) + args.addAll(includeIds) + + // creating the match and filter query + forwardIncludes.forEachIndexed { index, (param, search) -> + val resourceToInclude = search.type + args.add(resourceToInclude.name) + args.add(param.paramName) + matchQuery += " ( c.resourceType = ? and b.index_name IN (?) " + + val allFilters = search.getFilterQueries() + + if (allFilters.isNotEmpty()) { + val iterator = allFilters.listIterator() + matchQuery += "AND c.resourceUuid IN (\n" + do { + iterator.next().let { + matchQuery += 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)" + } + + matchQuery += " \n)" + + if (index != forwardIncludes.lastIndex) matchQuery += " OR " + } return SearchQuery( query = @@ -97,16 +190,26 @@ private fun Search.getIncludeQuery(includeIds: List): SearchQuery { SELECT a.resourceId, c.serializedResource from ResourceEntity a JOIN ReferenceIndexEntity b On a.resourceUuid = b.resourceUuid - AND a.resourceType = '${type.name}' - AND a.resourceId in ( ${includeIds.joinToString()} ) + AND a.resourceType = ? + AND a.resourceId IN ( ${ CharArray(includeIds.size) { '?' }.joinToString()} ) JOIN ResourceEntity c ON c.resourceType||"/"||c.resourceId = b.index_value - AND ($match) + ${if (matchQuery.isEmpty()) "" else "AND ($matchQuery) " } """.trimIndent(), - args = listOf() + args = 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 @@ -155,15 +258,7 @@ internal fun Search.getQuery( var filterStatement = "" val filterArgs = mutableListOf() - val filterQuery = - (stringFilterCriteria + - quantityFilterCriteria + - numberFilterCriteria + - referenceFilterCriteria + - dateTimeFilterCriteria + - tokenFilterCriteria + - uriFilterCriteria) - .map { it.query(type) } + val filterQuery = getFilterQueries() filterQuery.forEachIndexed { i, it -> filterStatement += """ @@ -220,22 +315,6 @@ internal fun Search.getQuery( $limitStatement) """ } - // revIncludeMap.isNotEmpty() -> { - // """ - // select serializedResource from ResourceEntity where resourceUuid in ( - // with UUIDS as ( select '${type.name}/' || a.resourceId from ResourceEntity a - // $sortJoinStatement - // WHERE a.resourceType = ? - // $filterStatement - // $sortOrderStatement - // $limitStatement - // ) - // Select resourceUuid - // FROM ResourceEntity - // WHERE '${type.name}/' || resourceId in UUIDS ${revIncludeMap.toSQLQuery()} - // ) - // """.trimIndent() - // } else -> """ SELECT a.serializedResource @@ -512,12 +591,3 @@ private fun getApproximateDateRange( } private data class ApproximateDateRange(val start: Long, val end: Long) - -private fun Map>.toSQLQuery() = - map { it -> - val indexes = it.value.joinToString { "\'${it.paramName}\'" } - """ - SELECT DISTINCT resourceUuid from ReferenceIndexEntity WHERE resourceType = '${it.key}' AND index_name IN ($indexes) AND index_value IN UUIDS - """.trimIndent() - } - .joinToString(prefix = "\nUNION\n", separator = " \nUNION\n") 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 fcf5bd55ce..fabfbeece2 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 @@ -41,7 +41,7 @@ internal data class NestedContext(val parentType: ResourceType, val param: IPara */ inline fun Search.has( referenceParam: ReferenceClientParam, - init: Search.() -> Unit + init: @ISearchDsl ISearch.() -> Unit ) { nestedSearches.add( NestedSearch(referenceParam, Search(type = R::class.java.newInstance().resourceType)).apply { @@ -50,6 +50,128 @@ inline fun Search.has( ) } +/** + * Allows user to include additional resources to be included in the search results that reference + * the resource on which [include] is being called. The developers may call [include] multiple times + * with different [ResourceType] to allow search api to return multiple referenced resource types. + * + * e.g. The below example would return all the Patients with given-name as James and their + * associated active [Practitioner] and Organizations. + * + * ``` + * fhirEngine.search { + * filter(Patient.GIVEN, { value = "James" }) + * include(Patient.GENERAL_PRACTITIONER) { + * filter(Practitioner.ACTIVE, { value = of(true) }) + * } + * include(Patient.ORGANIZATION) + * } + * ``` + * **NOTE**: [include] doesn't support order OR count. + */ +inline fun Search.include( + referenceParam: ReferenceClientParam, + init: @ISearchDsl ISearch.() -> Unit +) { + forwardIncludes.add( + NestedSearch(referenceParam, Search(type = R::class.java.newInstance().resourceType)).apply { + search.init() + } + ) +} + +/** + * Allows user to include additional resources to be included in the search results that reference + * the resource on which [include] is being called. The developers may call [include] multiple times + * with different [ResourceType] to allow search api to return multiple referenced resource types. + * + * e.g. The below example would return all the Patients with given-name as James and their + * associated active [Practitioner] and Organizations. + * + * ``` + * fhirEngine.search { + * filter(Patient.GIVEN, { value = "James" }) + * include(ResourceType.Practitioner, Patient.GENERAL_PRACTITIONER) { + * filter(Practitioner.ACTIVE, { value = of(true) }) + * } + * include(ResourceType.Organization,Patient.ORGANIZATION) + * } + * ``` + * + * **NOTE**: [include] doesn't support order OR count. + */ +fun Search.include( + resourceType: ResourceType, + referenceParam: ReferenceClientParam, + init: @ISearchDsl ISearch.() -> Unit = {} +) { + forwardIncludes.add( + NestedSearch(referenceParam, Search(type = resourceType)).apply { search.init() } + ) +} + +/** + * Allows user to include additional resources to be included in the search results that reference + * the resource on which [revInclude] is being called. The developers may call [revInclude] multiple + * times with different [ResourceType] to allow search api to return multiple referenced resource + * types. + * + * e.g. The below example would return all the Patients with given-name as James and their + * associated Encounters and diabetic Conditions. + * + * ``` + * fhirEngine.search { + * filter(Patient.GIVEN, { value = "James" }) + * revInclude( Encounter.PATIENT) + * revInclude( Condition.PATIENT) { + * filter(Condition.CODE, { value = of(diabetesCodeableConcept) }) + * } + * } + * ``` + * + * **NOTE**: [revInclude] doesn't support order OR count. + */ +inline fun Search.revInclude( + referenceParam: ReferenceClientParam, + init: @ISearchDsl ISearch.() -> Unit = {} +) { + + revIncludes.add( + NestedSearch(referenceParam, Search(type = R::class.java.newInstance().resourceType)).apply { + search.init() + } + ) +} + +/** + * Allows user to include additional resources to be included in the search results that reference + * the resource on which [revInclude] is being called. The developers may call [revInclude] multiple + * times with different [ResourceType] to allow search api to return multiple referenced resource + * types. + * + * e.g. The below example would return all the Patients with given-name as James and their + * associated Encounters and Conditions. + * + * ``` + * fhirEngine.search { + * filter(Patient.GIVEN, { value = "James" }) + * revInclude(ResourceType.Encounter, Encounter.PATIENT) + * revInclude(ResourceType.Condition, Condition.PATIENT) { + * filter(Condition.CODE, { value = of(diabetesCodeableConcept) }) + * } + * } + * ``` + * + * **NOTE**: [revInclude] doesn't support order OR count. + */ +fun Search.revInclude( + resourceType: ResourceType, + referenceParam: ReferenceClientParam, + init: @ISearchDsl ISearch.() -> Unit = {} +) { + revIncludes.add(NestedSearch(referenceParam, Search(type = resourceType)).apply { search.init() }) +} + /** * Provide limited support for reverse chaining on [Search] (See * [this](https://www.hl7.org/fhir/search.html#has)). @@ -67,7 +189,7 @@ inline fun Search.has( fun Search.has( resourceType: ResourceType, referenceParam: ReferenceClientParam, - init: Search.() -> Unit + init: @ISearchDsl ISearch.() -> Unit ) { nestedSearches.add( NestedSearch(referenceParam, Search(type = resourceType)).apply { search.init() } diff --git a/engine/src/main/java/com/google/android/fhir/search/Search.kt b/engine/src/main/java/com/google/android/fhir/search/Search.kt index d96d4f7bf7..17729106c5 100644 --- a/engine/src/main/java/com/google/android/fhir/search/Search.kt +++ b/engine/src/main/java/com/google/android/fhir/search/Search.kt @@ -17,11 +17,14 @@ package com.google.android.fhir.search import com.google.android.fhir.FhirEngine +import com.google.android.fhir.SearchResult import com.google.android.fhir.search.query.XFhirQueryTranslator.translate import org.hl7.fhir.r4.model.Resource import org.hl7.fhir.r4.model.ResourceType -suspend inline fun FhirEngine.search(init: Search.() -> Unit): List { +suspend inline fun FhirEngine.search( + init: Search.() -> Unit +): List> { val search = Search(type = R::class.java.newInstance().resourceType) search.init() return this.search(search) @@ -33,28 +36,15 @@ suspend inline fun FhirEngine.count(init: Search.() -> Un return this.count(search) } -suspend fun FhirEngine.search(xFhirQuery: String): List { +suspend fun FhirEngine.search(xFhirQuery: String): List> { return this.search(translate(xFhirQuery)) } -suspend fun FhirEngine.search(resourceType: ResourceType, init: Search.() -> Unit): List { +suspend fun FhirEngine.search( + resourceType: ResourceType, + init: Search.() -> Unit +): List> { val search = Search(type = resourceType) search.init() return this.search(search) } - -suspend inline fun FhirEngine.searchWithRevInclude( - init: Search.() -> Unit -): Map>> { - val search = Search(type = R::class.java.newInstance().resourceType) - search.init() - return this.searchWithRevInclude(true, search) -} - -suspend inline fun FhirEngine.searchWithForwardInclude( - init: Search.() -> Unit -): Map>> { - val search = Search(type = R::class.java.newInstance().resourceType) - search.init() - return this.searchWithRevInclude(false, search) -} 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 b8ee1a96b6..7b4ddf36d5 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 @@ -38,12 +38,10 @@ import com.google.android.fhir.search.filter.TokenParamFilterCriteria import com.google.android.fhir.search.filter.TokenParamFilterCriterion import com.google.android.fhir.search.filter.UriFilterCriteria import com.google.android.fhir.search.filter.UriParamFilterCriterion -import org.hl7.fhir.r4.model.Patient import org.hl7.fhir.r4.model.ResourceType @SearchDslMarker -data class Search(val type: ResourceType, var count: Int? = null, var from: Int? = null) { - internal val p = Patient() +class Search(val type: ResourceType, var count: Int? = null, var from: Int? = null) : ISearch { internal val stringFilterCriteria = mutableListOf() internal val dateTimeFilterCriteria = mutableListOf() internal val numberFilterCriteria = mutableListOf() @@ -53,25 +51,26 @@ data class Search(val type: ResourceType, var count: Int? = null, var from: Int? internal val uriFilterCriteria = mutableListOf() internal var sort: IParam? = null internal var order: Order? = null - internal val revIncludeMap = mutableMapOf>() - internal val includeMap = mutableMapOf>() @PublishedApi internal var nestedSearches = mutableListOf() + @PublishedApi internal var revIncludes = mutableListOf() + @PublishedApi internal var forwardIncludes = mutableListOf() + var operation = Operation.AND - fun filter( + override fun filter( stringParameter: StringClientParam, vararg init: StringParamFilterCriterion.() -> Unit, - operation: Operation = Operation.OR + operation: Operation ) { val filters = mutableListOf() init.forEach { StringParamFilterCriterion(stringParameter).apply(it).also(filters::add) } stringFilterCriteria.add(StringParamFilterCriteria(stringParameter, filters, operation)) } - fun filter( + override fun filter( referenceParameter: ReferenceClientParam, vararg init: ReferenceParamFilterCriterion.() -> Unit, - operation: Operation = Operation.OR + operation: Operation ) { val filters = mutableListOf() init.forEach { ReferenceParamFilterCriterion(referenceParameter).apply(it).also(filters::add) } @@ -80,94 +79,73 @@ data class Search(val type: ResourceType, var count: Int? = null, var from: Int? ) } - fun filter( + override fun filter( dateParameter: DateClientParam, vararg init: DateParamFilterCriterion.() -> Unit, - operation: Operation = Operation.OR + operation: Operation ) { val filters = mutableListOf() init.forEach { DateParamFilterCriterion(dateParameter).apply(it).also(filters::add) } dateTimeFilterCriteria.add(DateClientParamFilterCriteria(dateParameter, filters, operation)) } - fun filter( + override fun filter( quantityParameter: QuantityClientParam, vararg init: QuantityParamFilterCriterion.() -> Unit, - operation: Operation = Operation.OR + operation: Operation ) { val filters = mutableListOf() init.forEach { QuantityParamFilterCriterion(quantityParameter).apply(it).also(filters::add) } quantityFilterCriteria.add(QuantityParamFilterCriteria(quantityParameter, filters, operation)) } - fun filter( + override fun filter( tokenParameter: TokenClientParam, vararg init: TokenParamFilterCriterion.() -> Unit, - operation: Operation = Operation.OR + operation: Operation ) { val filters = mutableListOf() init.forEach { TokenParamFilterCriterion(tokenParameter).apply(it).also(filters::add) } tokenFilterCriteria.add(TokenParamFilterCriteria(tokenParameter, filters, operation)) } - fun filter( + override fun filter( numberParameter: NumberClientParam, vararg init: NumberParamFilterCriterion.() -> Unit, - operation: Operation = Operation.OR + operation: Operation ) { val filters = mutableListOf() init.forEach { NumberParamFilterCriterion(numberParameter).apply(it).also(filters::add) } numberFilterCriteria.add(NumberParamFilterCriteria(numberParameter, filters, operation)) } - fun filter( + override fun filter( uriParam: UriClientParam, vararg init: UriParamFilterCriterion.() -> Unit, - operation: Operation = Operation.OR + operation: Operation ) { val filters = mutableListOf() init.forEach { UriParamFilterCriterion(uriParam).apply(it).also(filters::add) } uriFilterCriteria.add(UriFilterCriteria(uriParam, filters, operation)) } - fun sort(parameter: StringClientParam, order: Order) { + override fun sort(parameter: StringClientParam, order: Order) { sort = parameter this.order = order } - fun sort(parameter: NumberClientParam, order: Order) { + override fun sort(parameter: NumberClientParam, order: Order) { sort = parameter this.order = order } - fun sort(parameter: DateClientParam, order: Order) { + override fun sort(parameter: DateClientParam, order: Order) { sort = parameter this.order = order } - /** - * Allows user to include additional resources to be included in the search results that reference - * the resource on which [revInclude] is being called. The developers may call [revInclude] - * multiple times with different [ResourceType] to allow search api to return multiple referenced - * resource types. - * - * e.g. The below example would return all the Patients with given-name as James and their - * associated Encounters and Conditions. - * - * ``` - * fhirEngine.search(Search(ResourceType.Patient).apply { - * filter(Patient.GIVEN, { value = "James" }) - * revInclude(ResourceType.Encounter, Encounter.PATIENT) - * revInclude(ResourceType.Condition, Condition.PATIENT) - * }) - * ``` - */ - fun revInclude(resourceType: ResourceType, clientParam: ReferenceClientParam) { - revIncludeMap.computeIfAbsent(resourceType) { mutableListOf() }.add(clientParam) - } - - fun include(clientParam: ReferenceClientParam, resourceType: ResourceType) { - includeMap.computeIfAbsent(resourceType) { mutableListOf() }.add(clientParam) + override fun operations(operation: Operation) { + this.operation = operation } } diff --git a/engine/src/main/java/com/google/android/fhir/search/SearchDslMarker.kt b/engine/src/main/java/com/google/android/fhir/search/SearchDslMarker.kt index 114f4f1ccf..0ee4b3e24b 100644 --- a/engine/src/main/java/com/google/android/fhir/search/SearchDslMarker.kt +++ b/engine/src/main/java/com/google/android/fhir/search/SearchDslMarker.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Google LLC + * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,4 +16,6 @@ package com.google.android.fhir.search -@DslMarker internal annotation class SearchDslMarker +@DslMarker +@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE) +internal annotation class SearchDslMarker diff --git a/engine/src/main/java/com/google/android/fhir/testing/Utilities.kt b/engine/src/main/java/com/google/android/fhir/testing/Utilities.kt index 152eb7ab1d..01c5086236 100644 --- a/engine/src/main/java/com/google/android/fhir/testing/Utilities.kt +++ b/engine/src/main/java/com/google/android/fhir/testing/Utilities.kt @@ -22,6 +22,7 @@ import ca.uhn.fhir.context.FhirVersionEnum import ca.uhn.fhir.parser.IParser import com.google.android.fhir.FhirEngine import com.google.android.fhir.LocalChange +import com.google.android.fhir.SearchResult import com.google.android.fhir.db.impl.dao.LocalChangeToken import com.google.android.fhir.search.Search import com.google.android.fhir.sync.BundleRequest @@ -136,17 +137,10 @@ object TestFhirEngineImpl : FhirEngine { override suspend fun delete(type: ResourceType, id: String) {} - override suspend fun search(search: Search): List { + override suspend fun search(search: Search): List> { return emptyList() } - override suspend fun searchWithRevInclude( - isRevInclude: Boolean, - search: Search - ): Map>> { - TODO("Not yet implemented") - } - override suspend fun syncUpload( upload: suspend (List) -> Flow> ) { From 75fac23d876dc6dff490124f50c78e0ed2cfd30c Mon Sep 17 00:00:00 2001 From: Aditya Khajuria Date: Thu, 29 Jun 2023 19:16:30 +0530 Subject: [PATCH 07/16] Workflow library: Incorporated changes related to FhirEngine.search --- .../com/google/android/fhir/workflow/FhirEngineDal.kt | 3 ++- .../android/fhir/workflow/FhirEngineRetrieveProvider.kt | 2 +- .../fhir/workflow/FhirEngineTerminologyProvider.kt | 9 ++++++--- .../google/android/fhir/workflow/FhirEngineDalTest.kt | 2 +- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineDal.kt b/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineDal.kt index 8610be6a96..1eaaf98c72 100644 --- a/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineDal.kt +++ b/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineDal.kt @@ -76,7 +76,8 @@ internal class FhirEngineDal( override fun search(resourceType: String): Iterable = runBlockingOrThrowMainThreadException { val search = Search(type = ResourceType.fromCode(resourceType)) - knowledgeManager.loadResources(resourceType = resourceType) + fhirEngine.search(search) + knowledgeManager.loadResources(resourceType = resourceType) + + fhirEngine.search(search).map { it.resource } } override fun searchByUrl(resourceType: String, url: String): Iterable = diff --git a/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineRetrieveProvider.kt b/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineRetrieveProvider.kt index d7a5717952..5d640d1236 100644 --- a/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineRetrieveProvider.kt +++ b/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineRetrieveProvider.kt @@ -74,7 +74,7 @@ internal class FhirEngineRetrieveProvider(private val fhirEngine: FhirEngine) : filterByCode(codePath, codes, search) filterByValueSet(codePath, valueSet, search) filterByDateRange(datePath, dateLowPath, dateHighPath, dateRange, search) - fhirEngine.search(search) + fhirEngine.search(search).map { it.resource } } } diff --git a/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineTerminologyProvider.kt b/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineTerminologyProvider.kt index 2093a7a415..feb6233681 100644 --- a/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineTerminologyProvider.kt +++ b/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineTerminologyProvider.kt @@ -80,7 +80,7 @@ internal class FhirEngineTerminologyProvider( filter(CodeSystem.SYSTEM, { value = codeSystem.id }) } .first() - .concept + .resource.concept .first { it.code == code.code } .let { Code().apply { @@ -101,12 +101,15 @@ internal class FhirEngineTerminologyProvider( if (url == null) return emptyList() return knowledgeManager .loadResources(resourceType = ResourceType.ValueSet.name, url = url) - .map { it as ValueSet } + fhirEngine.search { filter(ValueSet.URL, { value = url }) } + .map { it as ValueSet } + + fhirEngine.search { filter(ValueSet.URL, { value = url }) }.map { it.resource } } private suspend fun searchByIdentifier(identifier: String?): List { if (identifier == null) return emptyList() - return fhirEngine.search { filter(ValueSet.IDENTIFIER, { value = of(identifier) }) } + return fhirEngine + .search { filter(ValueSet.IDENTIFIER, { value = of(identifier) }) } + .map { it.resource } } private suspend fun searchById(id: String): List = diff --git a/workflow/src/test/java/com/google/android/fhir/workflow/FhirEngineDalTest.kt b/workflow/src/test/java/com/google/android/fhir/workflow/FhirEngineDalTest.kt index 2d82721986..ddb43959f8 100644 --- a/workflow/src/test/java/com/google/android/fhir/workflow/FhirEngineDalTest.kt +++ b/workflow/src/test/java/com/google/android/fhir/workflow/FhirEngineDalTest.kt @@ -91,7 +91,7 @@ class FhirEngineDalTest { testPatient.name = listOf(HumanName().addGiven("Eve")) fhirEngineDal.update(testPatient) - val result = fhirEngine.search {}.single() + val result = fhirEngine.search {}.single().resource assertThat(result.nameFirstRep.givenAsSingleString).isEqualTo("Eve") } From fe10fa0d79a692eddf4f530f3929fe821ebcacb8 Mon Sep 17 00:00:00 2001 From: Aditya Khajuria Date: Thu, 20 Jul 2023 14:33:18 +0530 Subject: [PATCH 08/16] Review comments: Changed the search result type to include the search param as well. --- .../android/fhir/db/impl/DatabaseImplTest.kt | 75 +++++++++++++++---- .../com/google/android/fhir/FhirEngine.kt | 4 +- .../android/fhir/db/impl/DatabaseImpl.kt | 15 ++-- .../android/fhir/db/impl/dao/ResourceDao.kt | 12 ++- .../fhir/search/{ISearch.kt => BaseSearch.kt} | 20 +++-- .../google/android/fhir/search/MoreSearch.kt | 15 ++-- .../android/fhir/search/NestedSearch.kt | 12 +-- .../google/android/fhir/search/SearchDsl.kt | 12 +-- .../android/fhir/impl/FhirEngineImplTest.kt | 12 +-- 9 files changed, 127 insertions(+), 50 deletions(-) rename engine/src/main/java/com/google/android/fhir/search/{ISearch.kt => BaseSearch.kt} (86%) 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 053be87a0a..f1af5749d2 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 @@ -3244,7 +3244,7 @@ class DatabaseImplTest { revInclude(Condition.SUBJECT) { filter(Condition.CODE, { value = of(diabetesCodeableConcept) }) filter(Condition.CODE, { value = of(migraineCodeableConcept) }) - operations(Operation.OR) + operation = Operation.OR } revInclude(Encounter.SUBJECT) { @@ -3266,7 +3266,7 @@ class DatabaseImplTest { modifier = StringFilterModifier.STARTS_WITH } ) - operations(Operation.AND) + operation = Operation.AND } include(Patient.ORGANIZATION) { @@ -3278,37 +3278,84 @@ class DatabaseImplTest { } ) filter(Practitioner.ACTIVE, { value = of(true) }) - operations(Operation.AND) + operation = Operation.AND } } .execute(database) assertThat(result[0].resource.logicalId).isEqualTo("pa-01") - assertThat(result[0].included!![ResourceType.Practitioner]!!.map { it.logicalId }) + assertThat( + result[0] + .included!![ResourceType.Practitioner]!![Patient.GENERAL_PRACTITIONER.paramName]!!.map { + it.logicalId + } + ) .containsExactly("gp-01", "gp-02") - assertThat(result[0].included!![ResourceType.Organization]!!.map { it.logicalId }) + assertThat( + result[0].included!![ResourceType.Organization]!![Patient.ORGANIZATION.paramName]!!.map { + it.logicalId + } + ) .containsExactly("org-01") - assertThat(result[0].revIncluded!![ResourceType.Condition]!!.map { it.logicalId }) + assertThat( + result[0].revIncluded!![ResourceType.Condition]!![Condition.SUBJECT.paramName]!!.map { + it.logicalId + } + ) .containsExactly("con-01-pa-01", "con-03-pa-01") - assertThat(result[0].revIncluded!![ResourceType.Encounter]!!.map { it.logicalId }) + assertThat( + result[0].revIncluded!![ResourceType.Encounter]!![Encounter.SUBJECT.paramName]!!.map { + it.logicalId + } + ) .containsExactly("en-01-pa-01", "en-02-pa-01") assertThat(result[1].resource.logicalId).isEqualTo("pa-02") - assertThat(result[1].included!![ResourceType.Practitioner]!!.map { it.logicalId }) + assertThat( + result[1] + .included!![ResourceType.Practitioner]!![Patient.GENERAL_PRACTITIONER.paramName]!!.map { + it.logicalId + } + ) .containsExactly("gp-01", "gp-02") - assertThat(result[1].included!![ResourceType.Organization]!!.map { it.logicalId }) + assertThat( + result[1].included!![ResourceType.Organization]!![Patient.ORGANIZATION.paramName]!!.map { + it.logicalId + } + ) .containsExactly("org-02") - assertThat(result[1].revIncluded!![ResourceType.Condition]!!.map { it.logicalId }) + assertThat( + result[1].revIncluded!![ResourceType.Condition]!![Condition.SUBJECT.paramName]!!.map { + it.logicalId + } + ) .containsExactly("con-01-pa-02", "con-03-pa-02") - assertThat(result[1].revIncluded!![ResourceType.Encounter]!!.map { it.logicalId }) + assertThat( + result[1].revIncluded!![ResourceType.Encounter]!![Encounter.SUBJECT.paramName]!!.map { + it.logicalId + } + ) .containsExactly("en-01-pa-02", "en-02-pa-02") assertThat(result[2].resource.logicalId).isEqualTo("pa-03") - assertThat(result[2].included!![ResourceType.Practitioner]!!.map { it.logicalId }) + assertThat( + result[2] + .included!![ResourceType.Practitioner]!![Patient.GENERAL_PRACTITIONER.paramName]!!.map { + it.logicalId + } + ) .containsExactly("gp-01", "gp-02") - assertThat(result[2].revIncluded!![ResourceType.Condition]!!.map { it.logicalId }) + assertThat( + result[2].revIncluded!![ResourceType.Condition]!![Condition.SUBJECT.paramName]!!.map { + it.logicalId + } + ) .containsExactly("con-01-pa-03", "con-03-pa-03") - assertThat(result[2].revIncluded!![ResourceType.Encounter]!!.map { it.logicalId }) + assertThat( + result[2].revIncluded!![ResourceType.Encounter]!![Encounter.SUBJECT.paramName]!!.map { + it.logicalId + } + ) .containsExactly("en-01-pa-03", "en-02-pa-03") } 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 b7cb20f460..7b7b6e3743 100644 --- a/engine/src/main/java/com/google/android/fhir/FhirEngine.kt +++ b/engine/src/main/java/com/google/android/fhir/FhirEngine.kt @@ -128,7 +128,9 @@ suspend inline fun FhirEngine.delete(id: String) { delete(getResourceType(R::class.java), id) } -typealias ReferencedResources = Map> +typealias SearchParamName = String + +typealias ReferencedResources = Map>> /** It contains the searched resource and referenced resources as per the search query. */ data class SearchResult( 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 36d1bb6662..8e2dff8c1e 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 @@ -189,12 +189,15 @@ internal class DatabaseImpl( override suspend fun searchReferencedResources(query: SearchQuery): List { return db.withTransaction { - resourceDao.getResourcesRev(SimpleSQLiteQuery(query.query, query.args.toTypedArray())).map { - IndexedIdAndResource( - it.idOfBaseResourceOnWhichThisMatchedInc ?: it.idOfBaseResourceOnWhichThisMatchedRev!!, - iParser.parseResource(it.serializedResource) as Resource - ) - } + resourceDao + .getReferencedResources(SimpleSQLiteQuery(query.query, query.args.toTypedArray())) + .map { + IndexedIdAndResource( + it.matchingIndex, + it.idOfBaseResourceOnWhichThisMatchedInc ?: it.idOfBaseResourceOnWhichThisMatchedRev!!, + 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 3195cca2ce..bc1bf7ba14 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 @@ -152,7 +152,7 @@ internal abstract class ResourceDao { @RawQuery abstract suspend fun getResources(query: SupportSQLiteQuery): List @RawQuery - abstract suspend fun getResourcesRev( + abstract suspend fun getReferencedResources( query: SupportSQLiteQuery ): List @@ -286,13 +286,23 @@ 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( + @ColumnInfo(name = "index_name") val matchingIndex: String, @ColumnInfo(name = "index_value") val idOfBaseResourceOnWhichThisMatchedRev: String?, @ColumnInfo(name = "resourceId") val idOfBaseResourceOnWhichThisMatchedInc: 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. + */ internal data class IndexedIdAndResource( + val matchingIndex: String, val idOfBaseResourceOnWhichThisMatched: String, val resource: Resource ) diff --git a/engine/src/main/java/com/google/android/fhir/search/ISearch.kt b/engine/src/main/java/com/google/android/fhir/search/BaseSearch.kt similarity index 86% rename from engine/src/main/java/com/google/android/fhir/search/ISearch.kt rename to engine/src/main/java/com/google/android/fhir/search/BaseSearch.kt index 4e0f3d8dcc..088942baac 100644 --- a/engine/src/main/java/com/google/android/fhir/search/ISearch.kt +++ b/engine/src/main/java/com/google/android/fhir/search/BaseSearch.kt @@ -31,10 +31,22 @@ import com.google.android.fhir.search.filter.StringParamFilterCriterion import com.google.android.fhir.search.filter.TokenParamFilterCriterion import com.google.android.fhir.search.filter.UriParamFilterCriterion -@DslMarker @Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE) annotation class ISearchDsl +@DslMarker @Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE) annotation class BaseSearchDsl -@ISearchDsl -interface ISearch { +/** + * Defines the basic functionality provided by the search including [filter], [sort] and logical + * [operation] on filters. + */ +@BaseSearchDsl +interface BaseSearch { + /** Logical operator between the filters. */ + var operation: Operation + + /** Count of the maximum expected search results. */ + var count: Int? + + /** Index from which the matching search results should be returned. */ + var from: Int? fun filter( stringParameter: StringClientParam, @@ -83,6 +95,4 @@ interface ISearch { fun sort(parameter: NumberClientParam, order: Order) fun sort(parameter: DateClientParam, order: Order) - - fun operations(operation: Operation) } 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 808139905c..a7217c3641 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 @@ -26,6 +26,7 @@ import com.google.android.fhir.SearchResult import com.google.android.fhir.UcumValue import com.google.android.fhir.UnitConverter import com.google.android.fhir.db.Database +import com.google.android.fhir.db.impl.dao.IndexedIdAndResource import com.google.android.fhir.epochDay import com.google.android.fhir.logicalId import com.google.android.fhir.ucumUrl @@ -68,16 +69,14 @@ internal suspend fun Search.execute(database: Database): List): SearchQuery { return SearchQuery( query = """ - SELECT a.index_value, b.serializedResource + SELECT a.index_name, a.index_value, b.serializedResource FROM ReferenceIndexEntity a JOIN ResourceEntity b ON a.resourceUuid = b.resourceUuid @@ -144,6 +143,10 @@ private fun Search.getRevIncludeQuery(includeIds: List): SearchQuery { ) } +private fun List.toReferencedResources() = + groupBy { it.resource.resourceType } + .mapValues { it.value.groupBy { it.matchingIndex }.mapValues { it.value.map { it.resource } } } + private fun Search.getIncludeQuery(includeIds: List): SearchQuery { var matchQuery = "" val args = mutableListOf(type.name) @@ -187,7 +190,7 @@ private fun Search.getIncludeQuery(includeIds: List): SearchQuery { return SearchQuery( query = """ - SELECT a.resourceId, c.serializedResource from ResourceEntity a + SELECT b.index_name, a.resourceId, c.serializedResource from ResourceEntity a JOIN ReferenceIndexEntity b On a.resourceUuid = b.resourceUuid AND 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 fabfbeece2..f0c44c88ac 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 @@ -41,7 +41,7 @@ internal data class NestedContext(val parentType: ResourceType, val param: IPara */ inline fun Search.has( referenceParam: ReferenceClientParam, - init: @ISearchDsl ISearch.() -> Unit + init: @BaseSearchDsl BaseSearch.() -> Unit ) { nestedSearches.add( NestedSearch(referenceParam, Search(type = R::class.java.newInstance().resourceType)).apply { @@ -71,7 +71,7 @@ inline fun Search.has( */ inline fun Search.include( referenceParam: ReferenceClientParam, - init: @ISearchDsl ISearch.() -> Unit + init: @BaseSearchDsl BaseSearch.() -> Unit = {} ) { forwardIncludes.add( NestedSearch(referenceParam, Search(type = R::class.java.newInstance().resourceType)).apply { @@ -103,7 +103,7 @@ inline fun Search.include( fun Search.include( resourceType: ResourceType, referenceParam: ReferenceClientParam, - init: @ISearchDsl ISearch.() -> Unit = {} + init: @BaseSearchDsl BaseSearch.() -> Unit = {} ) { forwardIncludes.add( NestedSearch(referenceParam, Search(type = resourceType)).apply { search.init() } @@ -133,7 +133,7 @@ fun Search.include( */ inline fun Search.revInclude( referenceParam: ReferenceClientParam, - init: @ISearchDsl ISearch.() -> Unit = {} + init: @BaseSearchDsl BaseSearch.() -> Unit = {} ) { revIncludes.add( @@ -167,7 +167,7 @@ inline fun Search.revInclude( fun Search.revInclude( resourceType: ResourceType, referenceParam: ReferenceClientParam, - init: @ISearchDsl ISearch.() -> Unit = {} + init: @BaseSearchDsl BaseSearch.() -> Unit = {} ) { revIncludes.add(NestedSearch(referenceParam, Search(type = resourceType)).apply { search.init() }) } @@ -189,7 +189,7 @@ fun Search.revInclude( fun Search.has( resourceType: ResourceType, referenceParam: ReferenceClientParam, - init: @ISearchDsl ISearch.() -> Unit + init: @BaseSearchDsl BaseSearch.() -> Unit ) { nestedSearches.add( NestedSearch(referenceParam, Search(type = resourceType)).apply { search.init() } 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 7b4ddf36d5..759a70f812 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 @@ -41,7 +41,11 @@ import com.google.android.fhir.search.filter.UriParamFilterCriterion import org.hl7.fhir.r4.model.ResourceType @SearchDslMarker -class Search(val type: ResourceType, var count: Int? = null, var from: Int? = null) : ISearch { +class Search( + val type: ResourceType, + override var count: Int? = null, + override var from: Int? = null +) : BaseSearch { internal val stringFilterCriteria = mutableListOf() internal val dateTimeFilterCriteria = mutableListOf() internal val numberFilterCriteria = mutableListOf() @@ -55,7 +59,7 @@ class Search(val type: ResourceType, var count: Int? = null, var from: Int? = nu @PublishedApi internal var revIncludes = mutableListOf() @PublishedApi internal var forwardIncludes = mutableListOf() - var operation = Operation.AND + override var operation = Operation.AND override fun filter( stringParameter: StringClientParam, @@ -143,10 +147,6 @@ class Search(val type: ResourceType, var count: Int? = null, var from: Int? = nu sort = parameter this.order = order } - - override fun operations(operation: Operation) { - this.operation = operation - } } enum class Order { diff --git a/engine/src/test/java/com/google/android/fhir/impl/FhirEngineImplTest.kt b/engine/src/test/java/com/google/android/fhir/impl/FhirEngineImplTest.kt index 7f8f679970..6da1e6a2c4 100644 --- a/engine/src/test/java/com/google/android/fhir/impl/FhirEngineImplTest.kt +++ b/engine/src/test/java/com/google/android/fhir/impl/FhirEngineImplTest.kt @@ -180,7 +180,9 @@ class FhirEngineImplTest { val result = fhirEngine.search("Patient?gender=female") assertThat(result.size).isEqualTo(2) - assertThat(result.all { (it as Patient).gender == Enumerations.AdministrativeGender.FEMALE }) + assertThat( + result.all { (it.resource as Patient).gender == Enumerations.AdministrativeGender.FEMALE } + ) .isTrue() } @@ -195,7 +197,7 @@ class FhirEngineImplTest { fhirEngine.create(*patients.toTypedArray()) - val result = fhirEngine.search("Patient?_sort=-name").map { it as Patient } + val result = fhirEngine.search("Patient?_sort=-name").map { it.resource as Patient } assertThat(result.mapNotNull { it.nameFirstRep.given.firstOrNull()?.value }) .isEqualTo(listOf("C", "B", "A")) @@ -212,7 +214,7 @@ class FhirEngineImplTest { fhirEngine.create(*patients.toTypedArray()) - val result = fhirEngine.search("Patient?_count=1").map { it as Patient } + val result = fhirEngine.search("Patient?_count=1").map { it.resource as Patient } assertThat(result.size).isEqualTo(1) } @@ -258,7 +260,7 @@ class FhirEngineImplTest { fhirEngine.create(*patients.toTypedArray()) - val result = fhirEngine.search("Patient?_tag=Tag1").map { it as Patient } + val result = fhirEngine.search("Patient?_tag=Tag1").map { it.resource as Patient } assertThat(result.size).isEqualTo(1) assertThat(result.all { patient -> patient.meta.tag.all { it.code == "Tag1" } }).isTrue() @@ -292,7 +294,7 @@ class FhirEngineImplTest { .search( "Patient?_profile=http://fhir.org/STU3/StructureDefinition/Example-Patient-Profile-1" ) - .map { it as Patient } + .map { it.resource as Patient } assertThat(result.size).isEqualTo(1) assertThat( From 7b55c7ea0020f54e8c1129a41c16afe020e417a7 Mon Sep 17 00:00:00 2001 From: Aditya Khajuria Date: Thu, 20 Jul 2023 16:16:16 +0530 Subject: [PATCH 09/16] Updated docs --- .../android/fhir/search/NestedSearch.kt | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) 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 f0c44c88ac..fcbb56809f 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 @@ -51,9 +51,9 @@ inline fun Search.has( } /** - * Allows user to include additional resources to be included in the search results that reference - * the resource on which [include] is being called. The developers may call [include] multiple times - * with different [ResourceType] to allow search api to return multiple referenced resource types. + * Includes additional resources in the search results that reference that reference the resource on + * which [include] is being called. The developers may call [include] multiple times with different + * [ResourceType] to allow search api to return multiple referenced resource types. * * e.g. The below example would return all the Patients with given-name as James and their * associated active [Practitioner] and Organizations. @@ -81,9 +81,9 @@ inline fun Search.include( } /** - * Allows user to include additional resources to be included in the search results that reference - * the resource on which [include] is being called. The developers may call [include] multiple times - * with different [ResourceType] to allow search api to return multiple referenced resource types. + * Includes additional resources in the search results that reference the resource on which + * [include] is being called. The developers may call [include] multiple times with different + * [ResourceType] to allow search api to return multiple referenced resource types. * * e.g. The below example would return all the Patients with given-name as James and their * associated active [Practitioner] and Organizations. @@ -111,10 +111,9 @@ fun Search.include( } /** - * Allows user to include additional resources to be included in the search results that reference - * the resource on which [revInclude] is being called. The developers may call [revInclude] multiple - * times with different [ResourceType] to allow search api to return multiple referenced resource - * types. + * Includes additional resources in the search results that reference the resource on which + * [revInclude] is being called. The developers may call [revInclude] multiple times with different + * [ResourceType] to allow search api to return multiple referenced resource types. * * e.g. The below example would return all the Patients with given-name as James and their * associated Encounters and diabetic Conditions. @@ -144,10 +143,9 @@ inline fun Search.revInclude( } /** - * Allows user to include additional resources to be included in the search results that reference - * the resource on which [revInclude] is being called. The developers may call [revInclude] multiple - * times with different [ResourceType] to allow search api to return multiple referenced resource - * types. + * Includes additional resources in the search results that reference the resource on which + * [revInclude] is being called. The developers may call [revInclude] multiple times with different + * [ResourceType] to allow search api to return multiple referenced resource types. * * e.g. The below example would return all the Patients with given-name as James and their * associated Encounters and Conditions. From 3db1d92fd981260aa990f74de2108fdc4380702e Mon Sep 17 00:00:00 2001 From: Aditya Khajuria Date: Thu, 20 Jul 2023 16:31:51 +0530 Subject: [PATCH 10/16] Added spotless toggle to skip indentation for specific code portions --- buildSrc/src/main/kotlin/SpotlessConfig.kt | 1 + .../google/android/fhir/search/MoreSearch.kt | 23 ++++++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/buildSrc/src/main/kotlin/SpotlessConfig.kt b/buildSrc/src/main/kotlin/SpotlessConfig.kt index 22e7482c42..97b307baea 100644 --- a/buildSrc/src/main/kotlin/SpotlessConfig.kt +++ b/buildSrc/src/main/kotlin/SpotlessConfig.kt @@ -36,6 +36,7 @@ fun Project.configureSpotless() { // It is necessary to tell spotless the top level of a file in order to apply config to it // See: https://github.com/diffplug/spotless/issues/135 ) + toggleOffOn() } kotlinGradle { target("*.gradle.kts") 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 a7217c3641..525fdedc9e 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 @@ -189,7 +189,8 @@ private fun Search.getIncludeQuery(includeIds: List): SearchQuery { return SearchQuery( query = - """ + // spotless:off + """ SELECT b.index_name, a.resourceId, c.serializedResource from ResourceEntity a JOIN ReferenceIndexEntity b On a.resourceUuid = b.resourceUuid @@ -199,6 +200,7 @@ private fun Search.getIncludeQuery(includeIds: List): SearchQuery { ON c.resourceType||"/"||c.resourceId = b.index_value ${if (matchQuery.isEmpty()) "" else "AND ($matchQuery) " } """.trimIndent(), + // spotless:on args = args ) } @@ -238,11 +240,12 @@ internal fun Search.getQuery( val tableAlias = 'b' + index sortJoinStatement += - """ + // spotless:off + """ LEFT JOIN ${sortTableName.tableName} $tableAlias ON a.resourceType = $tableAlias.resourceType AND a.resourceUuid = $tableAlias.resourceUuid AND $tableAlias.index_name = ? """ - + // spotless:on sortArgs += sort.paramName } @@ -264,12 +267,14 @@ internal fun Search.getQuery( val filterQuery = getFilterQueries() filterQuery.forEachIndexed { i, it -> filterStatement += + // spotless:off """ ${if (i == 0) "AND a.resourceUuid IN (" else "a.resourceUuid IN ("} ${it.query} ) ${if (i != filterQuery.lastIndex) "${operation.logicalOperator} " else ""} """.trimIndent() + // spotless:on filterArgs.addAll(it.args) } @@ -292,7 +297,8 @@ internal fun Search.getQuery( val query = when { isCount -> { - """ + // spotless:off + """ SELECT COUNT(*) FROM ResourceEntity a $sortJoinStatement @@ -301,11 +307,13 @@ internal fun Search.getQuery( $sortOrderStatement $limitStatement """ + // spotless:on } nestedContext != null -> { whereArgs.add(nestedContext.param.paramName) val start = "${nestedContext.parentType.name}/".length + 1 - """ + // spotless:off + """ SELECT resourceUuid FROM ResourceEntity a WHERE a.resourceId IN ( @@ -317,9 +325,11 @@ internal fun Search.getQuery( $sortOrderStatement $limitStatement) """ + // spotless:on } else -> - """ + // spotless:off + """ SELECT a.serializedResource FROM ResourceEntity a $sortJoinStatement @@ -328,6 +338,7 @@ internal fun Search.getQuery( $sortOrderStatement $limitStatement """ + // spotless:on } .split("\n") .filter { it.isNotBlank() } From 35bf0262db62021528575b7af8a5917a93083778 Mon Sep 17 00:00:00 2001 From: Aditya Khajuria Date: Tue, 25 Jul 2023 17:13:41 +0530 Subject: [PATCH 11/16] Added individual tests for include and revInclude --- buildSrc/src/main/kotlin/SpotlessConfig.kt | 2 +- .../android/fhir/db/impl/DatabaseImplTest.kt | 229 +++++++++++++++--- .../com/google/android/fhir/FhirEngine.kt | 45 +++- .../com/google/android/fhir/db/Database.kt | 2 +- .../google/android/fhir/search/BaseSearch.kt | 2 +- .../google/android/fhir/search/MoreSearch.kt | 3 +- .../android/fhir/search/NestedSearch.kt | 2 +- .../com/google/android/fhir/search/Search.kt | 2 +- .../google/android/fhir/search/SearchDsl.kt | 2 +- .../android/fhir/search/SearchDslMarker.kt | 2 +- .../android/fhir/workflow/FhirEngineDal.kt | 2 +- .../workflow/FhirEngineRetrieveProvider.kt | 2 +- .../workflow/FhirEngineTerminologyProvider.kt | 2 +- .../fhir/workflow/FhirEngineDalTest.kt | 2 +- 14 files changed, 252 insertions(+), 47 deletions(-) diff --git a/buildSrc/src/main/kotlin/SpotlessConfig.kt b/buildSrc/src/main/kotlin/SpotlessConfig.kt index 97b307baea..6f699f22bc 100644 --- a/buildSrc/src/main/kotlin/SpotlessConfig.kt +++ b/buildSrc/src/main/kotlin/SpotlessConfig.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 Google LLC + * Copyright 2022-2023 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. 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 90adaacc19..83524c35d8 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 ca.uhn.fhir.rest.param.ParamPrefixEnum import com.google.android.fhir.DateProvider import com.google.android.fhir.FhirServices import com.google.android.fhir.LocalChange +import com.google.android.fhir.SearchResult import com.google.android.fhir.db.Database import com.google.android.fhir.db.ResourceNotFoundException import com.google.android.fhir.db.impl.dao.toLocalChange @@ -2991,7 +2992,7 @@ class DatabaseImplTest { assertThat(result.map { it.nameFirstRep.nameAsSingleString }).contains("Darcy Smith") } - + @Test fun search_patient_with_local_lastUpdated() = runBlocking { database.insert( @@ -3019,6 +3020,199 @@ class DatabaseImplTest { .inOrder() } + @Test + fun search_patient_and_include_practitioners(): 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")) + } + + val patient02 = + Patient().apply { + id = "pa-02" + addName( + HumanName().apply { + addGiven("James") + family = "Bond" + } + ) + addGeneralPractitioner(Reference("Practitioner/gp-02")) + addGeneralPractitioner(Reference("Practitioner/gp-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 = false + } + val gp03 = + Practitioner().apply { + id = "gp-03" + addName( + HumanName().apply { + family = "Practitioner-03" + addGiven("General-03") + } + ) + active = true + } + + val practitioners = listOf(gp01, gp02, gp03) + + 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) }) + } + } + .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 + ) + ) + ) + } + + @Test + fun search_patient_and_revInclude_conditions(): 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) + val diabetesCodeableConcept = + CodeableConcept(Coding("http://snomed.info/sct", "44054006", "Diabetes")) + val hyperTensionCodeableConcept = + CodeableConcept(Coding("http://snomed.info/sct", "827069000", "Hypertension stage 1")) + val migraineCodeableConcept = + CodeableConcept(Coding("http://snomed.info/sct", "37796009", "Migraine")) + + val con1 = + Condition().apply { + id = "con-01" + code = diabetesCodeableConcept + subject = Reference("Patient/pa-01") + } + val con2 = + Condition().apply { + id = "con-02" + code = hyperTensionCodeableConcept + subject = Reference("Patient/pa-01") + } + val con3 = + Condition().apply { + id = "con-03" + code = migraineCodeableConcept + subject = Reference("Patient/pa-02") + } + val conditions = listOf(con1, con2, con3) + + database.insertRemote(*(patients + conditions).toTypedArray()) + + val result = + Search(ResourceType.Patient) + .apply { + filter( + Patient.GIVEN, + { + value = "James" + modifier = StringFilterModifier.MATCHES_EXACTLY + } + ) + revInclude(Condition.SUBJECT) { + filter(Condition.CODE, { value = of(diabetesCodeableConcept) }) + filter(Condition.CODE, { value = of(migraineCodeableConcept) }) + operation = Operation.OR + } + } + .execute(database) + + assertThat(result) + .isEqualTo( + listOf( + SearchResult( + patient01, + included = null, + revIncluded = + mapOf(ResourceType.Condition to mapOf(Condition.SUBJECT.paramName to listOf(con1))) + ), + SearchResult( + patient02, + included = null, + revIncluded = + mapOf(ResourceType.Condition to mapOf(Condition.SUBJECT.paramName to listOf(con3))) + ) + ) + ) + } + @Test fun search_patient_with_reference_resources(): Unit = runBlocking { val diabetesCodeableConcept = @@ -3312,18 +3506,9 @@ class DatabaseImplTest { .execute(database) assertThat(result[0].resource.logicalId).isEqualTo("pa-01") - assertThat( - result[0] - .included!![ResourceType.Practitioner]!![Patient.GENERAL_PRACTITIONER.paramName]!!.map { - it.logicalId - } - ) + assertThat(result[0].included!![Patient.GENERAL_PRACTITIONER.paramName]!!.map { it.logicalId }) .containsExactly("gp-01", "gp-02") - assertThat( - result[0].included!![ResourceType.Organization]!![Patient.ORGANIZATION.paramName]!!.map { - it.logicalId - } - ) + assertThat(result[0].included!![Patient.ORGANIZATION.paramName]!!.map { it.logicalId }) .containsExactly("org-01") assertThat( result[0].revIncluded!![ResourceType.Condition]!![Condition.SUBJECT.paramName]!!.map { @@ -3339,18 +3524,9 @@ class DatabaseImplTest { .containsExactly("en-01-pa-01", "en-02-pa-01") assertThat(result[1].resource.logicalId).isEqualTo("pa-02") - assertThat( - result[1] - .included!![ResourceType.Practitioner]!![Patient.GENERAL_PRACTITIONER.paramName]!!.map { - it.logicalId - } - ) + assertThat(result[1].included!![Patient.GENERAL_PRACTITIONER.paramName]!!.map { it.logicalId }) .containsExactly("gp-01", "gp-02") - assertThat( - result[1].included!![ResourceType.Organization]!![Patient.ORGANIZATION.paramName]!!.map { - it.logicalId - } - ) + assertThat(result[1].included!![Patient.ORGANIZATION.paramName]!!.map { it.logicalId }) .containsExactly("org-02") assertThat( result[1].revIncluded!![ResourceType.Condition]!![Condition.SUBJECT.paramName]!!.map { @@ -3366,12 +3542,7 @@ class DatabaseImplTest { .containsExactly("en-01-pa-02", "en-02-pa-02") assertThat(result[2].resource.logicalId).isEqualTo("pa-03") - assertThat( - result[2] - .included!![ResourceType.Practitioner]!![Patient.GENERAL_PRACTITIONER.paramName]!!.map { - it.logicalId - } - ) + assertThat(result[2].included!![Patient.GENERAL_PRACTITIONER.paramName]!!.map { it.logicalId }) .containsExactly("gp-01", "gp-02") assertThat( result[2].revIncluded!![ResourceType.Condition]!![Condition.SUBJECT.paramName]!!.map { 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 7b7b6e3743..4167e32386 100644 --- a/engine/src/main/java/com/google/android/fhir/FhirEngine.kt +++ b/engine/src/main/java/com/google/android/fhir/FhirEngine.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 Google LLC + * Copyright 2022-2023 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -130,14 +130,47 @@ suspend inline fun FhirEngine.delete(id: String) { typealias SearchParamName = String -typealias ReferencedResources = Map>> - /** It contains the searched resource and referenced resources as per the search query. */ data class SearchResult( /** Matching resource as per the query. */ val resource: R, /** Matching referenced resources as per the [Search.include] criteria in the query. */ - val included: ReferencedResources?, + val included: Map>?, /** Matching referenced resources as per the [Search.revInclude] criteria in the query. */ - val revIncluded: ReferencedResources? -) + val revIncluded: Map>>? +) { + override fun equals(other: Any?) = + other is SearchResult<*> && + equalsShallow(resource, other.resource) && + equalsShallow(included, other.included) && + equalsShallow2(revIncluded, other.revIncluded) + + private fun equalsShallow(first: Resource, second: Resource) = + first.resourceType == second.resourceType && first.logicalId == second.logicalId + + private fun equalsShallow( + first: Map>?, + second: Map>? + ) = + if (first != null && second != null && first.size == second.size) { + first.entries.zip(second.entries).all { (x, y) -> + x.key == y.key && + x.value.size == y.value.size && + x.value.zip(y.value).all { (x, y) -> equalsShallow(x, y) } + } + } else { + first?.size == second?.size + } + + private fun equalsShallow2( + first: Map>>?, + second: Map>>? + ) = + if (first != null && second != null && first.size == second.size) { + first.entries.zip(second.entries).all { (x, y) -> + x.key == y.key && x.value.size == y.value.size && 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 0cd93251d4..7c23edd3cc 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 @@ -1,5 +1,5 @@ /* - * Copyright 2022 Google LLC + * Copyright 2022-2023 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/engine/src/main/java/com/google/android/fhir/search/BaseSearch.kt b/engine/src/main/java/com/google/android/fhir/search/BaseSearch.kt index 088942baac..d5a02d96ca 100644 --- a/engine/src/main/java/com/google/android/fhir/search/BaseSearch.kt +++ b/engine/src/main/java/com/google/android/fhir/search/BaseSearch.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 Google LLC + * Copyright 2022-2023 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. 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 6674ed4945..953f89873b 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 @@ -69,7 +69,8 @@ internal suspend fun Search.execute(database: Database): List Date: Thu, 27 Jul 2023 19:48:15 +0530 Subject: [PATCH 12/16] Review Comments: Updated kdocs and refactored function name --- .../main/java/com/google/android/fhir/FhirEngine.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) 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 4167e32386..4d4a031d9f 100644 --- a/engine/src/main/java/com/google/android/fhir/FhirEngine.kt +++ b/engine/src/main/java/com/google/android/fhir/FhirEngine.kt @@ -130,7 +130,10 @@ suspend inline fun FhirEngine.delete(id: String) { typealias SearchParamName = String -/** It contains the searched resource and referenced resources as per the search query. */ +/** + * Contains a FHIR resource that satisfies the search criteria in the query together with any + * referenced resources as specified in the query. + */ data class SearchResult( /** Matching resource as per the query. */ val resource: R, @@ -143,7 +146,7 @@ data class SearchResult( other is SearchResult<*> && equalsShallow(resource, other.resource) && equalsShallow(included, other.included) && - equalsShallow2(revIncluded, other.revIncluded) + equalsShallow(revIncluded, other.revIncluded) private fun equalsShallow(first: Resource, second: Resource) = first.resourceType == second.resourceType && first.logicalId == second.logicalId @@ -162,7 +165,8 @@ data class SearchResult( first?.size == second?.size } - private fun equalsShallow2( + @JvmName("equalsShallowRevInclude") + private fun equalsShallow( first: Map>>?, second: Map>>? ) = From cfe459f482c1324dd504000ef4659f17b3fc5f75 Mon Sep 17 00:00:00 2001 From: Aditya Khajuria Date: Tue, 1 Aug 2023 15:57:38 +0530 Subject: [PATCH 13/16] Review comments: refactored type of revInclude --- .../android/fhir/db/impl/DatabaseImplTest.kt | 145 +++++++++--------- .../com/google/android/fhir/FhirEngine.kt | 20 +-- .../com/google/android/fhir/db/Database.kt | 2 +- .../google/android/fhir/search/MoreSearch.kt | 12 +- 4 files changed, 89 insertions(+), 90 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 4782b86195..bbec0d44c8 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 @@ -72,6 +72,7 @@ import org.hl7.fhir.r4.model.Period import org.hl7.fhir.r4.model.Practitioner import org.hl7.fhir.r4.model.Quantity import org.hl7.fhir.r4.model.Reference +import org.hl7.fhir.r4.model.Resource import org.hl7.fhir.r4.model.ResourceType import org.hl7.fhir.r4.model.RiskAssessment import org.hl7.fhir.r4.model.SearchParameter @@ -3124,13 +3125,13 @@ class DatabaseImplTest { patient01, included = null, revIncluded = - mapOf(ResourceType.Condition to mapOf(Condition.SUBJECT.paramName to listOf(con1))) + mapOf((ResourceType.Condition to Condition.SUBJECT.paramName) to listOf(con1)) ), SearchResult( patient02, included = null, revIncluded = - mapOf(ResourceType.Condition to mapOf(Condition.SUBJECT.paramName to listOf(con3))) + mapOf((ResourceType.Condition to Condition.SUBJECT.paramName) to listOf(con3)) ) ) ) @@ -3177,7 +3178,7 @@ class DatabaseImplTest { id = "pa-03" addName( HumanName().apply { - addGiven("John") + addGiven("James") family = "Doe" } ) @@ -3378,29 +3379,23 @@ class DatabaseImplTest { // Each has 3 conditions, only 2 should match // Each has 3 encounters, only 2 should match + val resources: Map = + (patients + practitioners + organizations + conditions + encounters).associateBy { + it.logicalId + } // Each has 3 GP, only 2 should match - database.insertRemote( - *(patients + practitioners + organizations + conditions + encounters).toTypedArray() - ) + database.insertRemote(*resources.values.toTypedArray()) val result = Search(ResourceType.Patient) .apply { - revInclude(Condition.SUBJECT) { - filter(Condition.CODE, { value = of(diabetesCodeableConcept) }) - filter(Condition.CODE, { value = of(migraineCodeableConcept) }) - operation = Operation.OR - } - - revInclude(Encounter.SUBJECT) { - filter( - Encounter.DATE, - { - value = of(DateTimeType("2023-01-01")) - prefix = ParamPrefixEnum.GREATERTHAN_OR_EQUALS - } - ) - } + filter( + Patient.GIVEN, + { + value = "James" + modifier = StringFilterModifier.MATCHES_EXACTLY + } + ) include(Patient.GENERAL_PRACTITIONER) { filter(Practitioner.ACTIVE, { value = of(true) }) @@ -3413,7 +3408,6 @@ class DatabaseImplTest { ) operation = Operation.AND } - include(Patient.ORGANIZATION) { filter( Organization.NAME, @@ -3425,60 +3419,67 @@ class DatabaseImplTest { filter(Practitioner.ACTIVE, { value = of(true) }) operation = Operation.AND } - } - .execute(database) - assertThat(result[0].resource.logicalId).isEqualTo("pa-01") - assertThat(result[0].included!![Patient.GENERAL_PRACTITIONER.paramName]!!.map { it.logicalId }) - .containsExactly("gp-01", "gp-02") - assertThat(result[0].included!![Patient.ORGANIZATION.paramName]!!.map { it.logicalId }) - .containsExactly("org-01") - assertThat( - result[0].revIncluded!![ResourceType.Condition]!![Condition.SUBJECT.paramName]!!.map { - it.logicalId - } - ) - .containsExactly("con-01-pa-01", "con-03-pa-01") - assertThat( - result[0].revIncluded!![ResourceType.Encounter]!![Encounter.SUBJECT.paramName]!!.map { - it.logicalId - } - ) - .containsExactly("en-01-pa-01", "en-02-pa-01") - - assertThat(result[1].resource.logicalId).isEqualTo("pa-02") - assertThat(result[1].included!![Patient.GENERAL_PRACTITIONER.paramName]!!.map { it.logicalId }) - .containsExactly("gp-01", "gp-02") - assertThat(result[1].included!![Patient.ORGANIZATION.paramName]!!.map { it.logicalId }) - .containsExactly("org-02") - assertThat( - result[1].revIncluded!![ResourceType.Condition]!![Condition.SUBJECT.paramName]!!.map { - it.logicalId - } - ) - .containsExactly("con-01-pa-02", "con-03-pa-02") - assertThat( - result[1].revIncluded!![ResourceType.Encounter]!![Encounter.SUBJECT.paramName]!!.map { - it.logicalId + revInclude(Condition.SUBJECT) { + filter(Condition.CODE, { value = of(diabetesCodeableConcept) }) + filter(Condition.CODE, { value = of(migraineCodeableConcept) }) + operation = Operation.OR + } + revInclude(Encounter.SUBJECT) { + filter( + Encounter.DATE, + { + value = of(DateTimeType("2023-01-01")) + prefix = ParamPrefixEnum.GREATERTHAN_OR_EQUALS + } + ) + } } - ) - .containsExactly("en-01-pa-02", "en-02-pa-02") + .execute(database) - assertThat(result[2].resource.logicalId).isEqualTo("pa-03") - assertThat(result[2].included!![Patient.GENERAL_PRACTITIONER.paramName]!!.map { it.logicalId }) - .containsExactly("gp-01", "gp-02") - assertThat( - result[2].revIncluded!![ResourceType.Condition]!![Condition.SUBJECT.paramName]!!.map { - it.logicalId - } - ) - .containsExactly("con-01-pa-03", "con-03-pa-03") - assertThat( - result[2].revIncluded!![ResourceType.Encounter]!![Encounter.SUBJECT.paramName]!!.map { - it.logicalId - } + 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"]!!) + ) + ), + 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"]!!) + ) + ), + 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"]!!) + ) + ) + ) ) - .containsExactly("en-01-pa-03", "en-02-pa-03") } private companion object { 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 cef6f48d39..f0cc2f72f9 100644 --- a/engine/src/main/java/com/google/android/fhir/FhirEngine.kt +++ b/engine/src/main/java/com/google/android/fhir/FhirEngine.kt @@ -139,7 +139,7 @@ data class SearchResult( /** Matching referenced resources as per the [Search.include] criteria in the query. */ val included: Map>?, /** Matching referenced resources as per the [Search.revInclude] criteria in the query. */ - val revIncluded: Map>>? + val revIncluded: Map, List>? ) { override fun equals(other: Any?) = other is SearchResult<*> && @@ -150,15 +150,17 @@ data class SearchResult( 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.zip(second.entries).all { (x, y) -> - x.key == y.key && - x.value.size == y.value.size && - x.value.zip(y.value).all { (x, y) -> equalsShallow(x, y) } + first.entries.asSequence().zip(second.entries.asSequence()).all { (x, y) -> + x.key == y.key && equalsShallow(x.value, y.value) } } else { first?.size == second?.size @@ -166,12 +168,12 @@ data class SearchResult( @JvmName("equalsShallowRevInclude") private fun equalsShallow( - first: Map>>?, - second: Map>>? + first: Map, List>?, + second: Map, List>? ) = if (first != null && second != null && first.size == second.size) { - first.entries.zip(second.entries).all { (x, y) -> - x.key == y.key && x.value.size == y.value.size && equalsShallow(x.value, y.value) + 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 829b44f268..a3a3fc3386 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 @@ -16,8 +16,8 @@ package com.google.android.fhir.db -import com.google.android.fhir.db.impl.dao.IndexedIdAndResource import com.google.android.fhir.LocalChange +import com.google.android.fhir.db.impl.dao.IndexedIdAndResource import com.google.android.fhir.db.impl.dao.LocalChangeToken import com.google.android.fhir.db.impl.entities.LocalChangeEntity import com.google.android.fhir.db.impl.entities.ResourceEntity 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 953f89873b..9c4f071ffe 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 @@ -26,7 +26,6 @@ import com.google.android.fhir.SearchResult import com.google.android.fhir.UcumValue import com.google.android.fhir.UnitConverter import com.google.android.fhir.db.Database -import com.google.android.fhir.db.impl.dao.IndexedIdAndResource import com.google.android.fhir.epochDay import com.google.android.fhir.logicalId import com.google.android.fhir.ucumUrl @@ -68,16 +67,17 @@ internal suspend fun Search.execute(database: Database): List): SearchQuery { ) } -private fun List.toReferencedResources() = - groupBy { it.resource.resourceType } - .mapValues { it.value.groupBy { it.matchingIndex }.mapValues { it.value.map { it.resource } } } - private fun Search.getIncludeQuery(includeIds: List): SearchQuery { var matchQuery = "" val args = mutableListOf(type.name) From 31834c69030933e95f92d0df3884bbc87d6c15ad Mon Sep 17 00:00:00 2001 From: Aditya Khajuria Date: Tue, 1 Aug 2023 17:28:44 +0530 Subject: [PATCH 14/16] Fixed failing workflow compilation as workflow doesn't depends on engine project directly but released .aar --- .../com/google/android/fhir/workflow/FhirEngineDal.kt | 3 +-- .../android/fhir/workflow/FhirEngineRetrieveProvider.kt | 2 +- .../fhir/workflow/FhirEngineTerminologyProvider.kt | 9 +++------ .../google/android/fhir/workflow/FhirEngineDalTest.kt | 2 +- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineDal.kt b/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineDal.kt index 9980884121..9b3de3fd01 100644 --- a/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineDal.kt +++ b/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineDal.kt @@ -76,8 +76,7 @@ internal class FhirEngineDal( override fun search(resourceType: String): Iterable = runBlockingOrThrowMainThreadException { val search = Search(type = ResourceType.fromCode(resourceType)) - knowledgeManager.loadResources(resourceType = resourceType) + - fhirEngine.search(search).map { it.resource } + knowledgeManager.loadResources(resourceType = resourceType) + fhirEngine.search(search) } override fun searchByUrl(resourceType: String, url: String): Iterable = diff --git a/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineRetrieveProvider.kt b/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineRetrieveProvider.kt index c5e16096df..d24455ef3a 100644 --- a/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineRetrieveProvider.kt +++ b/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineRetrieveProvider.kt @@ -74,7 +74,7 @@ internal class FhirEngineRetrieveProvider(private val fhirEngine: FhirEngine) : filterByCode(codePath, codes, search) filterByValueSet(codePath, valueSet, search) filterByDateRange(datePath, dateLowPath, dateHighPath, dateRange, search) - fhirEngine.search(search).map { it.resource } + fhirEngine.search(search) } } diff --git a/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineTerminologyProvider.kt b/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineTerminologyProvider.kt index 52e0dfe543..5848adc777 100644 --- a/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineTerminologyProvider.kt +++ b/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineTerminologyProvider.kt @@ -80,7 +80,7 @@ internal class FhirEngineTerminologyProvider( filter(CodeSystem.SYSTEM, { value = codeSystem.id }) } .first() - .resource.concept + .concept .first { it.code == code.code } .let { Code().apply { @@ -101,15 +101,12 @@ internal class FhirEngineTerminologyProvider( if (url == null) return emptyList() return knowledgeManager .loadResources(resourceType = ResourceType.ValueSet.name, url = url) - .map { it as ValueSet } + - fhirEngine.search { filter(ValueSet.URL, { value = url }) }.map { it.resource } + .map { it as ValueSet } + fhirEngine.search { filter(ValueSet.URL, { value = url }) } } private suspend fun searchByIdentifier(identifier: String?): List { if (identifier == null) return emptyList() - return fhirEngine - .search { filter(ValueSet.IDENTIFIER, { value = of(identifier) }) } - .map { it.resource } + return fhirEngine.search { filter(ValueSet.IDENTIFIER, { value = of(identifier) }) } } private suspend fun searchById(id: String): List = diff --git a/workflow/src/test/java/com/google/android/fhir/workflow/FhirEngineDalTest.kt b/workflow/src/test/java/com/google/android/fhir/workflow/FhirEngineDalTest.kt index 64661cc96b..c5d1fd4cbd 100644 --- a/workflow/src/test/java/com/google/android/fhir/workflow/FhirEngineDalTest.kt +++ b/workflow/src/test/java/com/google/android/fhir/workflow/FhirEngineDalTest.kt @@ -91,7 +91,7 @@ class FhirEngineDalTest { testPatient.name = listOf(HumanName().addGiven("Eve")) fhirEngineDal.update(testPatient) - val result = fhirEngine.search {}.single().resource + val result = fhirEngine.search {}.single() assertThat(result.nameFirstRep.givenAsSingleString).isEqualTo("Eve") } From 9f169f321217b17e3c209edb2815f3541cd63352 Mon Sep 17 00:00:00 2001 From: Aditya Khajuria Date: Tue, 1 Aug 2023 20:01:32 +0530 Subject: [PATCH 15/16] Resolved compilation errors --- .../google/android/fhir/demo/PatientDetailsViewModel.kt | 9 ++++++--- .../com/google/android/fhir/impl/FhirEngineImplTest.kt | 6 ++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/demo/src/main/java/com/google/android/fhir/demo/PatientDetailsViewModel.kt b/demo/src/main/java/com/google/android/fhir/demo/PatientDetailsViewModel.kt index 77c1f7bdd4..e5d2ae0f6f 100644 --- a/demo/src/main/java/com/google/android/fhir/demo/PatientDetailsViewModel.kt +++ b/demo/src/main/java/com/google/android/fhir/demo/PatientDetailsViewModel.kt @@ -75,13 +75,16 @@ class PatientDetailsViewModel( searchResult.first().let { data.addPatientDetailData( it.resource, - getRiskItem(it.revIncluded?.get(ResourceType.RiskAssessment) as List?) + getRiskItem( + it.revIncluded?.get(ResourceType.RiskAssessment to RiskAssessment.SUBJECT.paramName) + as List? + ) ) - it.revIncluded?.get(ResourceType.Observation)?.let { + it.revIncluded?.get(ResourceType.Observation to Observation.SUBJECT.paramName)?.let { data.addObservationsData(it as List) } - it.revIncluded?.get(ResourceType.Condition)?.let { + it.revIncluded?.get(ResourceType.Condition to Condition.SUBJECT.paramName)?.let { data.addConditionsData(it as List) } } diff --git a/engine/src/test/java/com/google/android/fhir/impl/FhirEngineImplTest.kt b/engine/src/test/java/com/google/android/fhir/impl/FhirEngineImplTest.kt index 62d7d1bdb8..fd050bbcfc 100644 --- a/engine/src/test/java/com/google/android/fhir/impl/FhirEngineImplTest.kt +++ b/engine/src/test/java/com/google/android/fhir/impl/FhirEngineImplTest.kt @@ -580,7 +580,7 @@ class FhirEngineImplTest { } assertThat(result).isNotEmpty() - assertThat(result.map { it.logicalId }).containsExactly("patient-id-create").inOrder() + assertThat(result.map { it.resource.logicalId }).containsExactly("patient-id-create").inOrder() } @Test @@ -618,7 +618,9 @@ class FhirEngineImplTest { assertThat(DateTimeType(Date.from(localChangeTimestampWhenUpdated)).value) .isAtLeast(DateTimeType(Date.from(localChangeTimestampWhenCreated)).value) assertThat(result).isNotEmpty() - assertThat(result.map { it.logicalId }).containsExactly("patient-id-update").inOrder() + assertThat(result.map { it.resource.logicalId }) + .containsExactly("patient-id-update") + .inOrder() } companion object { From 1dbe6bea25e812f69ff1fca01531b019fd99f477 Mon Sep 17 00:00:00 2001 From: Aditya Khajuria Date: Fri, 11 Aug 2023 15:52:40 +0530 Subject: [PATCH 16/16] Reverted the engine changes --- .../main/java/com/google/android/fhir/workflow/FhirEngineDal.kt | 2 +- .../google/android/fhir/workflow/FhirEngineRetrieveProvider.kt | 2 +- .../android/fhir/workflow/FhirEngineTerminologyProvider.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineDal.kt b/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineDal.kt index 9b3de3fd01..8610be6a96 100644 --- a/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineDal.kt +++ b/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineDal.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 Google LLC + * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineRetrieveProvider.kt b/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineRetrieveProvider.kt index d24455ef3a..d7a5717952 100644 --- a/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineRetrieveProvider.kt +++ b/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineRetrieveProvider.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 Google LLC + * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineTerminologyProvider.kt b/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineTerminologyProvider.kt index 5848adc777..2093a7a415 100644 --- a/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineTerminologyProvider.kt +++ b/workflow/src/main/java/com/google/android/fhir/workflow/FhirEngineTerminologyProvider.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 Google LLC + * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License.