Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Start saving local lastUpdated to resources for search operations. #2030

Merged
merged 17 commits into from
Jul 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

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(
aditya-07 marked this conversation as resolved.
Show resolved Hide resolved
"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
Loading