Skip to content

Commit 6977691

Browse files
LZRSjingtang10
andauthored
Balance SQLite expression tree for logical operators (AND/OR) to lower the depth (#2565)
* Modify toQueryString to chunk large list of ConditionParam https://stackoverflow.com/a/17032196 * Add test for condition params chunking and wrapping in brackets * Add support for chunkSize param in SearchDsl filters * Update workflow engine dependency to use latest * Refactor remove `chunkSize` parameter * Recursively bifurcate expression tree to reduce depth * Revert touched files not relevant for the PR * Refactor toQueryString * Update tests to include base cases for toQueryString * Refactor and update related test cases * Refactor test to make filter strict --------- Co-authored-by: Jing Tang <[email protected]>
1 parent 3320691 commit 6977691

File tree

5 files changed

+327
-13
lines changed

5 files changed

+327
-13
lines changed

engine/src/androidTest/java/com/google/android/fhir/db/impl/DatabaseImplTest.kt

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import com.google.android.fhir.search.Order
3939
import com.google.android.fhir.search.Search
4040
import com.google.android.fhir.search.StringFilterModifier
4141
import com.google.android.fhir.search.execute
42+
import com.google.android.fhir.search.filter.ReferenceParamFilterCriterion
4243
import com.google.android.fhir.search.getQuery
4344
import com.google.android.fhir.search.has
4445
import com.google.android.fhir.search.include
@@ -5178,6 +5179,32 @@ class DatabaseImplTest {
51785179
assertThat(localChangeResourceReferences.size).isEqualTo(locallyCreatedPatients.size)
51795180
}
51805181

5182+
@Test
5183+
fun searchTasksForManyPatientsReturnCorrectly() = runBlocking {
5184+
val patients = (0 until 990).map { Patient().apply { id = "task-patient-index-$it" } }
5185+
database.insert(*patients.toTypedArray())
5186+
val tasks =
5187+
patients.mapIndexed { index, patient ->
5188+
Task().apply {
5189+
id = "patient-$index-task"
5190+
`for` = Reference().apply { reference = "Patient/${patient.logicalId}" }
5191+
}
5192+
}
5193+
database.insert(*tasks.toTypedArray())
5194+
5195+
val patientsSearchIdList =
5196+
patients.take(980).map<Patient, ReferenceParamFilterCriterion.() -> Unit> {
5197+
{ value = "Patient/${it.logicalId}" }
5198+
}
5199+
val searchQuery =
5200+
Search(ResourceType.Task)
5201+
.apply { filter(Task.SUBJECT, *patientsSearchIdList.toTypedArray()) }
5202+
.getQuery()
5203+
5204+
val searchResults = database.search<Task>(searchQuery)
5205+
assertThat(searchResults.size).isEqualTo(980)
5206+
}
5207+
51815208
private companion object {
51825209
const val mockEpochTimeStamp = 1628516301000
51835210
const val TEST_PATIENT_1_ID = "test_patient_1"

engine/src/main/java/com/google/android/fhir/search/MoreSearch.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,8 @@ internal val DateTimeType.rangeEpochMillis
685685

686686
data class ConditionParam<T>(val condition: String, val params: List<T>) {
687687
constructor(condition: String, vararg params: T) : this(condition, params.asList())
688+
689+
val queryString = if (params.size > 1) "($condition)" else condition
688690
}
689691

690692
private enum class SortTableInfo(val tableName: String, val columnName: String) {

engine/src/main/java/com/google/android/fhir/search/filter/FilterCriterion.kt

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2021-2023 Google LLC
2+
* Copyright 2021-2024 Google LLC
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -58,7 +58,7 @@ internal sealed class FilterCriteria(
5858
return SearchQuery(
5959
"""
6060
SELECT resourceUuid FROM $entityTableName
61-
WHERE resourceType = ? AND index_name = ? AND ${conditionParams.toQueryString(operation)}
61+
WHERE resourceType = ? AND index_name = ?${if (conditionParams.isNotEmpty()) " AND ${conditionParams.toQueryString(operation)}" else ""}
6262
""",
6363
listOf(type.name, param.paramName) + conditionParams.flatMap { it.params },
6464
)
@@ -85,16 +85,15 @@ internal sealed class FilterCriteria(
8585
* This function takes care of wrapping the conditions in brackets so that they are evaluated as
8686
* intended.
8787
*/
88-
private fun List<ConditionParam<*>>.toQueryString(operation: Operation) =
89-
this.joinToString(
90-
separator = " ${operation.logicalOperator} ",
91-
prefix = if (size > 1) "(" else "",
92-
postfix = if (size > 1) ")" else "",
93-
) {
94-
if (it.params.size > 1) {
95-
"(${it.condition})"
96-
} else {
97-
it.condition
98-
}
88+
private fun List<ConditionParam<*>>.toQueryString(operation: Operation): String {
89+
if (this.size == 1) {
90+
return first().queryString
9991
}
92+
93+
val mid = this.size / 2
94+
val left = this.subList(0, mid).toQueryString(operation)
95+
val right = this.subList(mid, this.size).toQueryString(operation)
96+
97+
return "($left ${operation.logicalOperator} $right)"
98+
}
10099
}

engine/src/test/java/com/google/android/fhir/impl/FhirEngineImplTest.kt

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.google.android.fhir.impl
1818

1919
import androidx.test.core.app.ApplicationProvider
20+
import ca.uhn.fhir.rest.gclient.TokenClientParam
2021
import ca.uhn.fhir.rest.param.ParamPrefixEnum
2122
import com.google.android.fhir.FhirServices.Companion.builder
2223
import com.google.android.fhir.LocalChange
@@ -26,6 +27,7 @@ import com.google.android.fhir.get
2627
import com.google.android.fhir.lastUpdated
2728
import com.google.android.fhir.logicalId
2829
import com.google.android.fhir.search.LOCAL_LAST_UPDATED_PARAM
30+
import com.google.android.fhir.search.filter.TokenParamFilterCriterion
2931
import com.google.android.fhir.search.search
3032
import com.google.android.fhir.sync.AcceptLocalConflictResolver
3133
import com.google.android.fhir.sync.AcceptRemoteConflictResolver
@@ -323,6 +325,36 @@ class FhirEngineImplTest {
323325
.isTrue()
324326
}
325327

328+
@Test
329+
fun `search() should return patients filtered by param _id`() = runTest {
330+
val patient1 = Patient().apply { id = "patient-1" }
331+
val patient2 = Patient().apply { id = "patient-2" }
332+
val patient3 = Patient().apply { id = "patient-45" }
333+
val patient4 = Patient().apply { id = "patient-4355" }
334+
val patient5 = Patient().apply { id = "patient-899" }
335+
val patient6 = Patient().apply { id = "patient-883376" }
336+
fhirEngine.create(patient1, patient2, patient3, patient4, patient5, patient6)
337+
338+
val filterValues =
339+
listOf(patient2, patient3, patient1, patient5, patient4, patient6).map<
340+
Patient,
341+
TokenParamFilterCriterion.() -> Unit,
342+
> {
343+
{ value = of(it.logicalId) }
344+
}
345+
val patientSearchResult =
346+
fhirEngine.search<Patient> { filter(TokenClientParam("_id"), *filterValues.toTypedArray()) }
347+
assertThat(patientSearchResult.map { it.resource.logicalId })
348+
.containsExactly(
349+
"patient-2",
350+
"patient-45",
351+
"patient-1",
352+
"patient-4355",
353+
"patient-899",
354+
"patient-883376",
355+
)
356+
}
357+
326358
@Test
327359
fun syncUpload_uploadLocalChange_success() = runTest {
328360
val localChanges = mutableListOf<LocalChange>()

0 commit comments

Comments
 (0)