Skip to content

Commit 1da6eb7

Browse files
authored
Split and limit variables to 999 when using 'IN(...)' (#2562)
* Split and limit variables to 999 when using 'IN(...)' * Add supporting tests for large variable numbers * Update comments to include github issue with 'too many SQL variables in "select .."'
1 parent 3cc52bd commit 1da6eb7

File tree

4 files changed

+82
-7
lines changed

4 files changed

+82
-7
lines changed

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

+28
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import com.google.android.fhir.SearchParamName
2929
import com.google.android.fhir.SearchResult
3030
import com.google.android.fhir.db.Database
3131
import com.google.android.fhir.db.ResourceNotFoundException
32+
import com.google.android.fhir.db.impl.dao.LocalChangeDao
3233
import com.google.android.fhir.logicalId
3334
import com.google.android.fhir.search.LOCAL_LAST_UPDATED_PARAM
3435
import com.google.android.fhir.search.Operation
@@ -86,6 +87,7 @@ import org.hl7.fhir.r4.model.ResourceType
8687
import org.hl7.fhir.r4.model.RiskAssessment
8788
import org.hl7.fhir.r4.model.SearchParameter
8889
import org.hl7.fhir.r4.model.StringType
90+
import org.hl7.fhir.r4.model.Task
8991
import org.json.JSONArray
9092
import org.json.JSONObject
9193
import org.junit.After
@@ -5003,6 +5005,32 @@ class DatabaseImplTest {
50035005
.inOrder()
50045006
}
50055007

5008+
// https://github.com/google/android-fhir/issues/2559
5009+
@Test
5010+
fun getLocalChangeResourceReferences_shouldSafelyReturnReferencesAboveSQLiteInOpLimit() =
5011+
runBlocking {
5012+
val patientsCount = LocalChangeDao.SQLITE_LIMIT_MAX_VARIABLE_NUMBER * 7
5013+
val locallyCreatedPatients =
5014+
(1..patientsCount).map {
5015+
Patient().apply {
5016+
id = "local-patient-id$it"
5017+
name = listOf(HumanName().setFamily("Family").setGiven(listOf(StringType("$it"))))
5018+
}
5019+
}
5020+
database.insert(*locallyCreatedPatients.toTypedArray())
5021+
val locallyCreatedPatientTasks =
5022+
locallyCreatedPatients.mapIndexed { index, patient ->
5023+
Task().apply {
5024+
`for` = Reference("Patient/${patient.logicalId}")
5025+
id = "local-observation-$index"
5026+
}
5027+
}
5028+
database.insert(*locallyCreatedPatientTasks.toTypedArray())
5029+
val localChangeIds = database.getAllLocalChanges().flatMap { it.token.ids }
5030+
val localChangeResourceReferences = database.getLocalChangeResourceReferences(localChangeIds)
5031+
assertThat(localChangeResourceReferences.size).isEqualTo(locallyCreatedPatients.size)
5032+
}
5033+
50065034
private companion object {
50075035
const val mockEpochTimeStamp = 1628516301000
50085036
const val TEST_PATIENT_1_ID = "test_patient_1"

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

+34
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import org.hl7.fhir.r4.model.Enumerations
3838
import org.hl7.fhir.r4.model.Observation
3939
import org.hl7.fhir.r4.model.Patient
4040
import org.hl7.fhir.r4.model.Reference
41+
import org.hl7.fhir.r4.model.Task
4142
import org.junit.After
4243
import org.junit.Before
4344
import org.junit.Test
@@ -358,6 +359,39 @@ class LocalChangeDaoTest {
358359
.isEqualTo(practitionerReference)
359360
}
360361

362+
// https://github.com/google/android-fhir/issues/2559
363+
@Test
364+
fun updateResourceIdAndReferences_shouldSafelyUpdateLocalChangesReferencesAboveSQLiteInOpLimit() =
365+
runBlocking {
366+
val localPatientId = "local-patient-id"
367+
val patientResourceUuid = UUID.randomUUID()
368+
val localPatient = Patient().apply { id = localPatientId }
369+
val patientCreationTime = Instant.now()
370+
localChangeDao.addInsert(localPatient, patientResourceUuid, patientCreationTime)
371+
372+
val countAboveLimit = LocalChangeDao.SQLITE_LIMIT_MAX_VARIABLE_NUMBER * 10
373+
(1..countAboveLimit).forEach {
374+
val taskResourceUuid = UUID.randomUUID()
375+
val task =
376+
Task().apply {
377+
id = "local-task-$it"
378+
`for` = Reference("Patient/$localPatientId")
379+
}
380+
val taskCreationTime = Instant.now()
381+
localChangeDao.addInsert(task, taskResourceUuid, taskCreationTime)
382+
}
383+
384+
val updatedPatientId = "synced-patient-id"
385+
val updatedLocalPatient = localPatient.copy().apply { id = updatedPatientId }
386+
val updatedReferences =
387+
localChangeDao.updateResourceIdAndReferences(
388+
patientResourceUuid,
389+
oldResource = localPatient,
390+
updatedResource = updatedLocalPatient,
391+
)
392+
assertThat(updatedReferences.size).isEqualTo(countAboveLimit)
393+
}
394+
361395
@Test
362396
fun getReferencesForLocalChanges_should_return_all_changes(): Unit = runBlocking {
363397
listOf(

engine/src/main/java/com/google/android/fhir/db/impl/DatabaseImpl.kt

+9-6
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import com.google.android.fhir.db.ResourceNotFoundException
3131
import com.google.android.fhir.db.ResourceWithUUID
3232
import com.google.android.fhir.db.impl.DatabaseImpl.Companion.UNENCRYPTED_DATABASE_NAME
3333
import com.google.android.fhir.db.impl.dao.ForwardIncludeSearchResult
34+
import com.google.android.fhir.db.impl.dao.LocalChangeDao.Companion.SQLITE_LIMIT_MAX_VARIABLE_NUMBER
3435
import com.google.android.fhir.db.impl.dao.ReverseIncludeSearchResult
3536
import com.google.android.fhir.db.impl.entities.ResourceEntity
3637
import com.google.android.fhir.index.ResourceIndexer
@@ -407,12 +408,14 @@ internal class DatabaseImpl(
407408
override suspend fun getLocalChangeResourceReferences(
408409
localChangeIds: List<Long>,
409410
): List<LocalChangeResourceReference> {
410-
return localChangeDao.getReferencesForLocalChanges(localChangeIds).map {
411-
LocalChangeResourceReference(
412-
it.localChangeId,
413-
it.resourceReferenceValue,
414-
it.resourceReferencePath,
415-
)
411+
return localChangeIds.chunked(SQLITE_LIMIT_MAX_VARIABLE_NUMBER).flatMap { chunk ->
412+
localChangeDao.getReferencesForLocalChanges(chunk).map {
413+
LocalChangeResourceReference(
414+
it.localChangeId,
415+
it.resourceReferenceValue,
416+
it.resourceReferencePath,
417+
)
418+
}
416419
}
417420
}
418421

engine/src/main/java/com/google/android/fhir/db/impl/dao/LocalChangeDao.kt

+11-1
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,10 @@ internal abstract class LocalChangeDao {
412412
val updatedReferenceValue = "${updatedResource.resourceType.name}/${updatedResource.logicalId}"
413413
val referringLocalChangeIds =
414414
getLocalChangeReferencesWithValue(oldReferenceValue).map { it.localChangeId }.distinct()
415-
val referringLocalChanges = getLocalChanges(referringLocalChangeIds)
415+
val referringLocalChanges =
416+
referringLocalChangeIds.chunked(SQLITE_LIMIT_MAX_VARIABLE_NUMBER).flatMap {
417+
getLocalChanges(it)
418+
}
416419

417420
referringLocalChanges.forEach { existingLocalChangeEntity ->
418421
val updatedLocalChangeEntity =
@@ -498,6 +501,13 @@ internal abstract class LocalChangeDao {
498501

499502
companion object {
500503
const val DEFAULT_ID_VALUE = 0L
504+
505+
/**
506+
* Represents SQLite limit on the size of parameters that can be passed in an IN(..) query See
507+
* https://issuetracker.google.com/issues/192284727 See https://www.sqlite.org/limits.html See
508+
* https://github.com/google/android-fhir/issues/2559
509+
*/
510+
const val SQLITE_LIMIT_MAX_VARIABLE_NUMBER = 999
501511
}
502512
}
503513

0 commit comments

Comments
 (0)