Skip to content

Commit

Permalink
Add resource UUID to LocalChangeEntity, fixing changing UUID issue fo…
Browse files Browse the repository at this point in the history
…r the same resource (#2210)

* Adding resource UUID in LocalChangeEntity

* fixing migration tests

* returning resource UUID for insert resource

* review comments
  • Loading branch information
anchita-g committed Oct 3, 2023
1 parent 18e41c0 commit dc9a921
Show file tree
Hide file tree
Showing 11 changed files with 1,158 additions and 74 deletions.
956 changes: 956 additions & 0 deletions engine/schemas/com.google.android.fhir.db.impl.ResourceDatabase/7.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,21 @@ class DatabaseImplTest {
.isTrue()
}

@Test
fun insert_existingRemoteResource_shouldNotChangeResourceEntityUuidOrId() = runBlocking {
val patient: Patient = readFromFile(Patient::class.java, "/date_test_patient.json")
database.insertRemote(patient)
val patientEntityAfterFirstRemoteSync =
database.selectEntity(ResourceType.Patient, patient.logicalId)
database.insertRemote(patient)
val patientEntityAfterSecondRemoteSync =
database.selectEntity(ResourceType.Patient, patient.logicalId)
assertThat(patientEntityAfterSecondRemoteSync.resourceUuid)
.isEqualTo(patientEntityAfterFirstRemoteSync.resourceUuid)
assertThat(patientEntityAfterSecondRemoteSync.id)
.isEqualTo(patientEntityAfterFirstRemoteSync.id)
}

@Test
fun insert_remoteResource_shouldSaveVersionIdAndLastUpdated() = runBlocking {
val patient =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

package com.google.android.fhir.db.impl

import androidx.room.Room
import androidx.room.testing.MigrationTestHelper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
Expand All @@ -30,7 +29,6 @@ import java.util.Date
import kotlinx.coroutines.runBlocking
import org.hl7.fhir.r4.model.HumanName
import org.hl7.fhir.r4.model.Patient
import org.hl7.fhir.r4.model.ResourceType
import org.hl7.fhir.r4.model.Task
import org.junit.Rule
import org.junit.Test
Expand Down Expand Up @@ -69,14 +67,16 @@ class ResourceDatabaseMigrationTest {

// Open latest version of the database. Room will validate the schema
// once all migrations execute.
helper.runMigrationsAndValidate(DB_NAME, 2, true, MIGRATION_1_2)
val migratedDatabase = helper.runMigrationsAndValidate(DB_NAME, 2, true, MIGRATION_1_2)

val readPatientJson: String?
getMigratedRoomDatabase().apply {
readPatientJson = this.resourceDao().getResource("migrate1-2-test", ResourceType.Patient)
openHelper.writableDatabase.close()
migratedDatabase.let { database ->
database.query("SELECT serializedResource FROM ResourceEntity").let {
it.moveToFirst()
readPatientJson = it.getString(0)
}
}

migratedDatabase.close()
assertThat(readPatientJson).isEqualTo(insertedPatientJson)
}

Expand All @@ -101,14 +101,16 @@ class ResourceDatabaseMigrationTest {
}

// Re-open the database with version 3 and provide MIGRATION_2_3 as the migration process.
helper.runMigrationsAndValidate(DB_NAME, 3, true, MIGRATION_2_3)
val migratedDatabase = helper.runMigrationsAndValidate(DB_NAME, 3, true, MIGRATION_2_3)

val retrievedTask: String?
getMigratedRoomDatabase().apply {
retrievedTask = this.resourceDao().getResource(taskId, ResourceType.Task)
openHelper.writableDatabase.close()
migratedDatabase.let { database ->
database.query("SELECT serializedResource FROM ResourceEntity").let {
it.moveToFirst()
retrievedTask = it.getString(0)
}
}

migratedDatabase.close()
assertThat(retrievedTask).isEqualTo(bedNetTask)
}

Expand All @@ -133,14 +135,16 @@ class ResourceDatabaseMigrationTest {
}

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

val retrievedTask: String?
getMigratedRoomDatabase().apply {
retrievedTask = this.resourceDao().getResource(taskId, ResourceType.Task)
openHelper.writableDatabase.close()
migratedDatabase.let { database ->
database.query("SELECT serializedResource FROM ResourceEntity").let {
it.moveToFirst()
retrievedTask = it.getString(0)
}
}

migratedDatabase.close()
assertThat(retrievedTask).isEqualTo(bedNetTask)
}

Expand All @@ -163,15 +167,17 @@ class ResourceDatabaseMigrationTest {
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)
// Re-open the database with version 5 and provide MIGRATION_4_5 as the migration process.
val migratedDatabase = helper.runMigrationsAndValidate(DB_NAME, 5, true, MIGRATION_4_5)

val retrievedTask: String?
getMigratedRoomDatabase().apply {
retrievedTask = this.resourceDao().getResource(taskId, ResourceType.Task)
openHelper.writableDatabase.close()
migratedDatabase.let { database ->
database.query("SELECT serializedResource FROM ResourceEntity").let {
it.moveToFirst()
retrievedTask = it.getString(0)
}
}

migratedDatabase.close()
assertThat(retrievedTask).isEqualTo(bedNetTask)
}

Expand Down Expand Up @@ -205,44 +211,87 @@ class ResourceDatabaseMigrationTest {
close()
}

helper.runMigrationsAndValidate(DB_NAME, 6, true, MIGRATION_5_6)
val migratedDatabase = helper.runMigrationsAndValidate(DB_NAME, 6, true, MIGRATION_5_6)

val retrievedTask: String?
val localChangeEntityTimeStamp: Long
val resourceEntityLastUpdatedLocal: Long
val localChangeEntityCorruptedTimeStamp: Long

getMigratedRoomDatabase().apply {
retrievedTask = this.resourceDao().getResource(taskId, ResourceType.Task)
migratedDatabase.let { database ->
database.query("SELECT serializedResource FROM ResourceEntity").let {
it.moveToFirst()
retrievedTask = it.getString(0)
}

resourceEntityLastUpdatedLocal =
query("Select lastUpdatedLocal from ResourceEntity", null).let {
database.query("Select lastUpdatedLocal from ResourceEntity").let {
it.moveToFirst()
it.getLong(0)
}

query("SELECT timestamp FROM LocalChangeEntity", null).let {
database.query("SELECT timestamp FROM LocalChangeEntity").let {
it.moveToFirst()
localChangeEntityTimeStamp = it.getLong(0)
it.moveToNext()
localChangeEntityCorruptedTimeStamp = it.getLong(0)
}

openHelper.writableDatabase.close()
}

migratedDatabase.close()
assertThat(retrievedTask).isEqualTo(bedNetTask)
assertThat(localChangeEntityTimeStamp).isEqualTo(resourceEntityLastUpdatedLocal)
assertThat(Instant.ofEpochMilli(localChangeEntityCorruptedTimeStamp)).isEqualTo(Instant.EPOCH)
}

private fun getMigratedRoomDatabase(): ResourceDatabase =
Room.databaseBuilder(
InstrumentationRegistry.getInstrumentation().targetContext,
ResourceDatabase::class.java,
DB_NAME,
@Test
fun migrate6To7_should_execute_with_no_exception(): Unit = runBlocking {
val taskId = "bed-net-001"
val taskResourceUuid = "e2c79e28-ed4d-4029-a12c-108d1eb5bedb"
val bedNetTask: String =
Task()
.apply {
id = taskId
description = "Issue bed net"
meta.lastUpdated = Date()
}
.let { iParser.encodeResourceToString(it) }

helper.createDatabase(DB_NAME, 6).apply {
val date = Date()
execSQL(
"INSERT INTO ResourceEntity (resourceUuid, resourceType, resourceId, serializedResource, lastUpdatedLocal) VALUES ('$taskResourceUuid', 'Task', '$taskId', '$bedNetTask', '${DbTypeConverters.instantToLong(date.toInstant())}' );",
)

execSQL(
"INSERT INTO LocalChangeEntity (resourceType, resourceId, timestamp, type, payload) VALUES ('Task', '$taskId', '${date.toTimeZoneString()}', '${DbTypeConverters.localChangeTypeToInt(LocalChangeEntity.Type.INSERT)}', '$bedNetTask' );",
)
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6)
.build()
close()
}

val migratedDatabase = helper.runMigrationsAndValidate(DB_NAME, 7, true, MIGRATION_6_7)

val retrievedTaskResourceId: String?
val retrievedTaskResourceUuid: String?
val localChangeResourceUuid: String?
val localChangeResourceId: String?

migratedDatabase.let { database ->
database.query("SELECT resourceId, resourceUuid FROM ResourceEntity").let {
it.moveToFirst()
retrievedTaskResourceId = it.getString(0)
retrievedTaskResourceUuid = String(it.getBlob(1), Charsets.UTF_8)
}

database.query("SELECT resourceId,resourceUuid FROM LocalChangeEntity").let {
it.moveToFirst()
localChangeResourceId = it.getString(0)
localChangeResourceUuid = String(it.getBlob(1), Charsets.UTF_8)
}
}
migratedDatabase.close()
assertThat(retrievedTaskResourceUuid).isEqualTo(localChangeResourceUuid)
assertThat(localChangeResourceId).isEqualTo(retrievedTaskResourceId)
}

companion object {
const val DB_NAME = "migration_tests.db"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,14 @@ internal class DatabaseImpl(
}
}

addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6)
addMigrations(
MIGRATION_1_2,
MIGRATION_2_3,
MIGRATION_3_4,
MIGRATION_4_5,
MIGRATION_5_6,
MIGRATION_6_7,
)
}
.build()
}
Expand All @@ -115,8 +122,9 @@ internal class DatabaseImpl(
logicalIds.addAll(
resource.map {
val timeOfLocalChange = Instant.now()
localChangeDao.addInsert(it, timeOfLocalChange)
resourceDao.insertLocalResource(it, timeOfLocalChange)
val resourceUuid = resourceDao.insertLocalResource(it, timeOfLocalChange)
localChangeDao.addInsert(it, resourceUuid, timeOfLocalChange)
it.logicalId
},
)
}
Expand Down Expand Up @@ -169,19 +177,16 @@ internal class DatabaseImpl(

override suspend fun delete(type: ResourceType, id: String) {
db.withTransaction {
val remoteVersionId: String? =
try {
selectEntity(type, id).versionId
} catch (e: ResourceNotFoundException) {
null
resourceDao.getResourceEntity(id, type)?.let {
val rowsDeleted = resourceDao.deleteResource(resourceId = id, resourceType = type)
if (rowsDeleted > 0) {
localChangeDao.addDelete(
resourceId = id,
resourceType = type,
resourceUuid = it.resourceUuid,
remoteVersionId = it.versionId,
)
}
val rowsDeleted = resourceDao.deleteResource(resourceId = id, resourceType = type)
if (rowsDeleted > 0) {
localChangeDao.addDelete(
resourceId = id,
resourceType = type,
remoteVersionId = remoteVersionId,
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ import com.google.android.fhir.db.impl.entities.UriIndexEntity
LocalChangeEntity::class,
PositionIndexEntity::class,
],
version = 6,
version = 7,
exportSchema = true,
)
@TypeConverters(DbTypeConverters::class)
Expand Down Expand Up @@ -127,3 +127,25 @@ val MIGRATION_5_6 =
)
}
}

/** Add column resourceUuid in [LocalChangeEntity] */
val MIGRATION_6_7 =
object : Migration(6, 7) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"CREATE TABLE IF NOT EXISTS `_new_LocalChangeEntity` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `resourceType` TEXT NOT NULL, `resourceId` TEXT NOT NULL, `resourceUuid` BLOB NOT NULL, `timestamp` INTEGER NOT NULL, `type` INTEGER NOT NULL, `payload` TEXT NOT NULL, `versionId` TEXT)",
)
database.execSQL(
"INSERT INTO `_new_LocalChangeEntity` (`id`,`resourceType`,`resourceId`,`resourceUuid`,`timestamp`,`type`,`payload`,`versionId`) " +
"SELECT localChange.id, localChange.resourceType, localChange.resourceId, resource.resourceUuid, localChange.timestamp, localChange.type, localChange.payload, localChange.versionId FROM `LocalChangeEntity` localChange LEFT JOIN ResourceEntity resource ON localChange.resourceId= resource.resourceId",
)
database.execSQL("DROP TABLE `LocalChangeEntity`")
database.execSQL("ALTER TABLE `_new_LocalChangeEntity` RENAME TO `LocalChangeEntity`")
database.execSQL(
"CREATE INDEX IF NOT EXISTS `index_LocalChangeEntity_resourceType_resourceId` ON `LocalChangeEntity` (`resourceType`, `resourceId`)",
)
database.execSQL(
"CREATE INDEX IF NOT EXISTS `index_LocalChangeEntity_resourceUuid` ON `LocalChangeEntity` (`resourceUuid`)",
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import com.google.android.fhir.logicalId
import com.google.android.fhir.versionId
import java.time.Instant
import java.util.Date
import java.util.UUID
import org.hl7.fhir.r4.model.Resource
import org.hl7.fhir.r4.model.ResourceType
import org.json.JSONArray
Expand All @@ -51,7 +52,7 @@ internal abstract class LocalChangeDao {
@Insert abstract suspend fun addLocalChange(localChangeEntity: LocalChangeEntity)

@Transaction
open suspend fun addInsert(resource: Resource, timeOfLocalChange: Instant) {
open suspend fun addInsert(resource: Resource, resourceUuid: UUID, timeOfLocalChange: Instant) {
val resourceId = resource.logicalId
val resourceType = resource.resourceType
val resourceString = iParser.encodeResourceToString(resource)
Expand All @@ -61,6 +62,7 @@ internal abstract class LocalChangeDao {
id = 0,
resourceType = resourceType.name,
resourceId = resourceId,
resourceUuid = resourceUuid,
timestamp = timeOfLocalChange,
type = Type.INSERT,
payload = resourceString,
Expand Down Expand Up @@ -95,6 +97,7 @@ internal abstract class LocalChangeDao {
id = 0,
resourceType = resourceType.name,
resourceId = resourceId,
resourceUuid = oldEntity.resourceUuid,
timestamp = timeOfLocalChange,
type = Type.UPDATE,
payload = jsonDiff.toString(),
Expand All @@ -103,12 +106,18 @@ internal abstract class LocalChangeDao {
)
}

suspend fun addDelete(resourceId: String, resourceType: ResourceType, remoteVersionId: String?) {
suspend fun addDelete(
resourceId: String,
resourceUuid: UUID,
resourceType: ResourceType,
remoteVersionId: String?,
) {
addLocalChange(
LocalChangeEntity(
id = 0,
resourceType = resourceType.name,
resourceId = resourceId,
resourceUuid = resourceUuid,
timestamp = Date().toInstant(),
type = Type.DELETE,
payload = "",
Expand Down
Loading

0 comments on commit dc9a921

Please sign in to comment.