Skip to content

Commit

Permalink
Start saving local lastUpdated to resources for search operations. (#…
Browse files Browse the repository at this point in the history
…2030)

* Start saving local lastUpdated to resources for search operations

* Added tests

* Addressed review comments

* Reverted the db encryption flag

* spotless apply

* Review comments: Udpated tests

* Review Comments: Renamed functions to better naming conevntions
  • Loading branch information
aditya-07 committed Jul 21, 2023
1 parent d62ca13 commit 1a23a8a
Show file tree
Hide file tree
Showing 13 changed files with 1,480 additions and 271 deletions.
941 changes: 941 additions & 0 deletions engine/schemas/com.google.android.fhir.db.impl.ResourceDatabase/5.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022 Google LLC
* Copyright 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.
Expand Down Expand Up @@ -29,6 +29,7 @@ import com.google.android.fhir.db.ResourceNotFoundException
import com.google.android.fhir.db.impl.dao.toLocalChange
import com.google.android.fhir.db.impl.entities.LocalChangeEntity
import com.google.android.fhir.logicalId
import com.google.android.fhir.search.LOCAL_LAST_UPDATED_PARAM
import com.google.android.fhir.search.Operation
import com.google.android.fhir.search.Order
import com.google.android.fhir.search.Search
Expand Down Expand Up @@ -3075,6 +3076,33 @@ class DatabaseImplTest {
assertThat(result.map { it.nameFirstRep.nameAsSingleString }).contains("Darcy Smith")
}

@Test
fun search_patient_with_local_lastUpdated() = runBlocking {
database.insert(
Patient().apply { id = "patient-test-001" },
Patient().apply { id = "patient-test-002" },
Patient().apply { id = "patient-test-003" }
)

database.update(
Patient().apply {
id = "patient-test-002"
gender = Enumerations.AdministrativeGender.FEMALE
}
)

val result =
database.search<Patient>(
Search(ResourceType.Patient)
.apply { sort(LOCAL_LAST_UPDATED_PARAM, Order.DESCENDING) }
.getQuery()
)

assertThat(result.map { it.logicalId })
.containsAtLeast("patient-test-002", "patient-test-003", "patient-test-001")
.inOrder()
}

private companion object {
const val mockEpochTimeStamp = 1628516301000
const val TEST_PATIENT_1_ID = "test_patient_1"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022 Google LLC
* Copyright 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.
Expand Down Expand Up @@ -140,13 +140,45 @@ class ResourceDatabaseMigrationTest {

assertThat(retrievedTask).isEqualTo(bedNetTask)
}

@Test
fun migrate4To5_should_execute_with_no_exception(): Unit = runBlocking {
val taskId = "bed-net-001"
val bedNetTask: String =
Task()
.apply {
id = taskId
description = "Issue bed net"
meta.lastUpdated = Date()
}
.let { iParser.encodeResourceToString(it) }

helper.createDatabase(DB_NAME, 4).apply {
execSQL(
"INSERT INTO ResourceEntity (resourceUuid, resourceType, resourceId, serializedResource) VALUES ('bed-net-001', 'Task', 'bed-net-001', '$bedNetTask');"
)
close()
}

// Re-open the database with version 4 and provide MIGRATION_3_4 as the migration process.
helper.runMigrationsAndValidate(DB_NAME, 5, true, MIGRATION_4_5)

val retrievedTask: String?
getMigratedRoomDatabase().apply {
retrievedTask = this.resourceDao().getResource(taskId, ResourceType.Task)
openHelper.writableDatabase.close()
}

assertThat(retrievedTask).isEqualTo(bedNetTask)
}

private fun getMigratedRoomDatabase(): ResourceDatabase =
Room.databaseBuilder(
InstrumentationRegistry.getInstrumentation().targetContext,
ResourceDatabase::class.java,
DB_NAME
)
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4)
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5)
.build()

companion object {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022 Google LLC
* Copyright 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.
Expand Down Expand Up @@ -93,7 +93,7 @@ internal class DatabaseImpl(
}
}

addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4)
addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5)
}
.build()
}
Expand All @@ -110,22 +110,28 @@ internal class DatabaseImpl(
override suspend fun <R : Resource> insert(vararg resource: R): List<String> {
val logicalIds = mutableListOf<String>()
db.withTransaction {
logicalIds.addAll(resourceDao.insertAll(resource.toList()))
localChangeDao.addInsertAll(resource.toList())
logicalIds.addAll(
resource.map {
val timeOfLocalChange = Instant.now()
localChangeDao.addInsert(it, timeOfLocalChange)
resourceDao.insertLocalResource(it, timeOfLocalChange)
}
)
}
return logicalIds
}

override suspend fun <R : Resource> insertRemote(vararg resource: R) {
db.withTransaction { resourceDao.insertAll(resource.toList()) }
db.withTransaction { resourceDao.insertAllRemote(resource.toList()) }
}

override suspend fun update(vararg resources: Resource) {
db.withTransaction {
resources.forEach {
val timeOfLocalChange = Instant.now()
val oldResourceEntity = selectEntity(it.resourceType, it.logicalId)
resourceDao.update(it)
localChangeDao.addUpdate(oldResourceEntity, it)
resourceDao.update(it, timeOfLocalChange)
localChangeDao.addUpdate(oldResourceEntity, it, timeOfLocalChange)
}
}
}
Expand All @@ -137,7 +143,7 @@ internal class DatabaseImpl(
lastUpdated: Instant
) {
db.withTransaction {
resourceDao.updateRemoteVersionIdAndLastUpdate(
resourceDao.updateAndIndexRemoteVersionIdAndLastUpdate(
resourceId,
resourceType,
versionId,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022 Google LLC
* Copyright 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.
Expand Down Expand Up @@ -50,7 +50,7 @@ import com.google.android.fhir.db.impl.entities.UriIndexEntity
LocalChangeEntity::class,
PositionIndexEntity::class
],
version = 4,
version = 5,
exportSchema = true
)
@TypeConverters(DbTypeConverters::class)
Expand Down Expand Up @@ -99,3 +99,12 @@ val MIGRATION_3_4 =
)
}
}

val MIGRATION_4_5 =
object : Migration(/* startVersion = */ 4, /* endVersion = */ 5) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"ALTER TABLE `ResourceEntity` ADD COLUMN `lastUpdatedLocal` INTEGER DEFAULT NULL"
)
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022 Google LLC
* Copyright 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.
Expand Down Expand Up @@ -27,6 +27,7 @@ import com.google.android.fhir.db.impl.entities.ResourceEntity
import com.google.android.fhir.logicalId
import com.google.android.fhir.toTimeZoneString
import com.google.android.fhir.versionId
import java.time.Instant
import java.util.Date
import org.hl7.fhir.r4.model.Resource
import org.hl7.fhir.r4.model.ResourceType
Expand All @@ -46,14 +47,10 @@ internal abstract class LocalChangeDao {
@Insert abstract suspend fun addLocalChange(localChangeEntity: LocalChangeEntity)

@Transaction
open suspend fun addInsertAll(resources: List<Resource>) {
resources.forEach { resource -> addInsert(resource) }
}

suspend fun addInsert(resource: Resource) {
open suspend fun addInsert(resource: Resource, timeOfLocalChange: Instant) {
val resourceId = resource.logicalId
val resourceType = resource.resourceType
val timestamp = Date().toTimeZoneString()
val timestamp = Date.from(timeOfLocalChange).toTimeZoneString()
val resourceString = iParser.encodeResourceToString(resource)

addLocalChange(
Expand All @@ -69,13 +66,13 @@ internal abstract class LocalChangeDao {
)
}

suspend fun addUpdate(oldEntity: ResourceEntity, resource: Resource) {
suspend fun addUpdate(oldEntity: ResourceEntity, resource: Resource, timeOfLocalChange: Instant) {
val resourceId = resource.logicalId
val resourceType = resource.resourceType
val timestamp = Date().toTimeZoneString()
val timestamp = Date.from(timeOfLocalChange).toTimeZoneString()

if (!localChangeIsEmpty(resourceId, resourceType) &&
lastChangeType(resourceId, resourceType)!!.equals(Type.DELETE)
lastChangeType(resourceId, resourceType)!! == Type.DELETE
) {
throw InvalidLocalChangeException(
"Unexpected DELETE when updating $resourceType/$resourceId. UPDATE failed."
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022 Google LLC
* Copyright 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.
Expand Down Expand Up @@ -70,7 +70,8 @@ internal object LocalChangeUtils {
resourceType = second.resourceType,
type = type,
payload = payload,
versionId = second.versionId
versionId = second.versionId,
timestamp = second.timestamp
)
}

Expand Down Expand Up @@ -136,9 +137,11 @@ internal object LocalChangeUtils {
with(JSONArray(jsonDiff.toString())) {
val ignorePaths = setOf("/meta", "/text")
return@with JSONArray(
(0 until length()).map { optJSONObject(it) }.filterNot { jsonObject ->
ignorePaths.any { jsonObject.optString("path").startsWith(it) }
}
(0 until length())
.map { optJSONObject(it) }
.filterNot { jsonObject ->
ignorePaths.any { jsonObject.optString("path").startsWith(it) }
}
)
}
}
Expand Down
Loading

0 comments on commit 1a23a8a

Please sign in to comment.