Skip to content

Commit

Permalink
Adding resource UUID in LocalChangeEntity
Browse files Browse the repository at this point in the history
  • Loading branch information
anchita-g committed Sep 27, 2023
1 parent 996d13a commit e1c351c
Show file tree
Hide file tree
Showing 11 changed files with 1,129 additions and 34 deletions.
957 changes: 957 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 @@ -163,7 +163,7 @@ class ResourceDatabaseMigrationTest {
close()
}

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

val retrievedTask: String?
Expand Down Expand Up @@ -213,7 +213,7 @@ class ResourceDatabaseMigrationTest {
val localChangeEntityCorruptedTimeStamp: Long

getMigratedRoomDatabase().apply {
retrievedTask = this.resourceDao().getResource(taskId, ResourceType.Task)
retrievedTask = this.resourceDao().getResource(taskId, ResourceType.Task)
resourceEntityLastUpdatedLocal =
query("Select lastUpdatedLocal from ResourceEntity", null).let {
it.moveToFirst()
Expand All @@ -235,13 +235,72 @@ class ResourceDatabaseMigrationTest {
assertThat(Instant.ofEpochMilli(localChangeEntityCorruptedTimeStamp)).isEqualTo(Instant.EPOCH)
}

@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' );",
)
close()
}

helper.runMigrationsAndValidate(DB_NAME, 7, true, MIGRATION_6_7)

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

getMigratedRoomDatabase().apply {
query("SELECT resourceId, resourceUuid FROM ResourceEntity", null).let {
it.moveToFirst()
retrievedTaskResourceId = it.getString(0)
retrievedTaskResourceUuid = String(it.getBlob(1), Charsets.UTF_8)
}

query("SELECT resourceId,resourceUuid FROM LocalChangeEntity", null).let {
it.moveToFirst()
localChangeResourceId = it.getString(0)
localChangeResourceUuid = String(it.getBlob(1), Charsets.UTF_8)
}

openHelper.writableDatabase.close()
}

assertThat(retrievedTaskResourceUuid).isEqualTo(localChangeResourceUuid)
assertThat(localChangeResourceId).isEqualTo(retrievedTaskResourceId)
}

private fun getMigratedRoomDatabase(): ResourceDatabase =
Room.databaseBuilder(
InstrumentationRegistry.getInstrumentation().targetContext,
ResourceDatabase::class.java,
DB_NAME,
)
.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()

companion object {
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,10 @@ internal class DatabaseImpl(
logicalIds.addAll(
resource.map {
val timeOfLocalChange = Instant.now()
localChangeDao.addInsert(it, timeOfLocalChange)
resourceDao.insertLocalResource(it, timeOfLocalChange)
val resourceId = resourceDao.insertLocalResource(it, timeOfLocalChange)
val resourceEntity = selectEntity(it.resourceType, it.logicalId)
localChangeDao.addInsert(it, resourceEntity.resourceUuid, timeOfLocalChange)
resourceId
},
)
}
Expand Down Expand Up @@ -169,19 +178,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_resourceType_resourceUuid` ON `LocalChangeEntity` (`resourceType`, `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
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ internal abstract class ResourceDao {
lateinit var iParser: IParser
lateinit var resourceIndexer: ResourceIndexer

open suspend fun update(resource: Resource, timeOfLocalChange: Instant) {
open suspend fun update(resource: Resource, timeOfLocalChange: Instant?) {
getResourceEntity(resource.logicalId, resource.resourceType)?.let {
// In case the resource has lastUpdated meta data, use it, otherwise use the old value.
val lastUpdatedRemote: Date? = resource.meta.lastUpdated
Expand All @@ -76,12 +76,14 @@ internal abstract class ResourceDao {
val index =
ResourceIndices.Builder(resourceIndexer.index(resource))
.apply {
addDateTimeIndex(
createLocalLastUpdatedIndex(
resource.resourceType,
InstantType(Date.from(timeOfLocalChange)),
),
)
timeOfLocalChange?.let {
addDateTimeIndex(
createLocalLastUpdatedIndex(
resource.resourceType,
InstantType(Date.from(timeOfLocalChange)),
),
)
}
lastUpdatedRemote?.let { date ->
addDateTimeIndex(createLastUpdatedIndex(resource.resourceType, InstantType(date)))
}
Expand Down Expand Up @@ -183,11 +185,14 @@ internal abstract class ResourceDao {

// Since the insert removes any old indexes and lastUpdatedLocal (data not contained in resource
// itself), we extract the lastUpdatedLocal if any and then set it back again.
private suspend fun insertRemoteResource(resource: Resource) =
insertResource(
resource,
getResourceEntity(resource.logicalId, resource.resourceType)?.lastUpdatedLocal,
)
private suspend fun insertRemoteResource(resource: Resource): String {
val existingResourceEntity = getResourceEntity(resource.logicalId, resource.resourceType)
if (existingResourceEntity != null) {
update(resource, existingResourceEntity.lastUpdatedLocal)
return resource.id
}
return insertResource(resource, null)
}

private suspend fun insertResource(resource: Resource, lastUpdatedLocal: Instant?): String {
val resourceUuid = UUID.randomUUID()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import java.time.Instant
import java.util.UUID

/**
* When a local change to a resource happens, the lastUpdated timestamp in [ResourceEntity] is
Expand Down Expand Up @@ -55,11 +56,18 @@ import java.time.Instant
*
* ] For resource that is fully synced with server this table should not have any rows.
*/
@Entity(indices = [Index(value = ["resourceType", "resourceId"])])
@Entity(
indices =
[
Index(value = ["resourceType", "resourceId"]),
Index(value = ["resourceType", "resourceUuid"]),
],
)
internal data class LocalChangeEntity(
@PrimaryKey(autoGenerate = true) val id: Long,
val resourceType: String,
val resourceId: String,
val resourceUuid: UUID,
val timestamp: Instant,
val type: Type,
val payload: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import ca.uhn.fhir.parser.IParser
import com.google.android.fhir.db.impl.entities.LocalChangeEntity
import com.google.common.truth.Truth.assertThat
import java.time.Instant
import java.util.UUID
import junit.framework.TestCase
import kotlinx.coroutines.runBlocking
import org.hl7.fhir.r4.model.HumanName
Expand All @@ -42,6 +43,7 @@ class LocalChangeTest : TestCase() {
LocalChangeEntity(
id = 1,
resourceType = ResourceType.Patient.name,
resourceUuid = UUID.randomUUID(),
resourceId = "Patient-001",
type = LocalChangeEntity.Type.INSERT,
payload =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import com.google.android.fhir.toLocalChange
import com.google.common.truth.Truth.assertThat
import java.net.ConnectException
import java.time.Instant
import java.util.UUID
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.runBlocking
import org.hl7.fhir.r4.model.Bundle
Expand Down Expand Up @@ -133,6 +134,7 @@ class UploaderImplTest {
LocalChangeEntity(
id = 1,
resourceType = ResourceType.Patient.name,
resourceUuid = UUID.randomUUID(),
resourceId = "Patient-001",
type = LocalChangeEntity.Type.INSERT,
payload =
Expand Down
Loading

0 comments on commit e1c351c

Please sign in to comment.