Skip to content
Open
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
6 changes: 3 additions & 3 deletions .github/workflows/pr-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@ jobs:
- 'infra/security/**'
infra1:
- 'infra/orchestrator-data/**'
- 'infra/enrolment-records:repository/**'
- 'infra/enrolment-records:realm-store/**'
- 'infra/enrolment-records:room-store/**'
- 'infra/enrolment-records/repository/**'
- 'infra/enrolment-records/realm-store/**'
- 'infra/enrolment-records/room-store/**'
- 'infra/recent-user-activity/**'
- 'infra/config-store/**'
- 'infra/credential-store/**'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,6 @@ class RealmConfig @Inject constructor() {
.build()

companion object {
private const val REALM_SCHEMA_VERSION: Long = 18
private const val REALM_SCHEMA_VERSION: Long = 19
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,18 @@ import com.simprints.infra.enrolment.records.realm.store.models.toRealmInstant
import io.realm.kotlin.dynamic.DynamicMutableRealmObject
import io.realm.kotlin.dynamic.getValue
import io.realm.kotlin.migration.AutomaticSchemaMigration
import io.realm.kotlin.types.RealmList
import io.realm.kotlin.types.RealmUUID
import java.util.Date
import java.util.UUID

internal class RealmMigrations : AutomaticSchemaMigration {
companion object {
private const val SUBJECT_FINGERPRINT_SAMPLES_FIELD = "fingerprintSamples"
private const val SUBJECT_FACE_SAMPLES_FIELD = "faceSamples"
private const val REFERENCE_ID_FIELD = "referenceId"
}

override fun migrate(migrationContext: AutomaticSchemaMigration.MigrationContext) {
val oldVersion = migrationContext.oldRealm.schemaVersion()
val newVersion = migrationContext.newRealm.schemaVersion()
Expand All @@ -25,6 +32,7 @@ internal class RealmMigrations : AutomaticSchemaMigration {
10 -> migrateTo11(migrationContext)
11 -> migrateTo12(migrationContext)
12 -> migrateTo13(migrationContext)
18 -> migrateTo19(migrationContext)
}
}
}
Expand Down Expand Up @@ -106,4 +114,30 @@ internal class RealmMigrations : AutomaticSchemaMigration {
newObject?.set(SubjectsSchemaV13.SUBJECT_ID_FIELD, RealmUUID.from(subjectId))
}
}

/**
* Because of a missing Realm migration when `referenceId` was introduced, samples created
* before that schema change were left with an empty referenceId.
*
* Backfill a stable, shared UUID for all samples of the same subject and modality that still
* have an empty referenceId, mirroring the grouping that historically applied before
* referenceId existed.
*/

private fun migrateTo19(migrationContext: AutomaticSchemaMigration.MigrationContext) {
migrationContext.enumerate(SubjectsSchemaV13.SUBJECT_TABLE) { _, newObject ->
newObject?.let {
backfillEmptyReferenceIds(it.getObjectList(SUBJECT_FINGERPRINT_SAMPLES_FIELD))
backfillEmptyReferenceIds(it.getObjectList(SUBJECT_FACE_SAMPLES_FIELD))
}
}
}

private fun backfillEmptyReferenceIds(samples: RealmList<DynamicMutableRealmObject>) {
val samplesWithEmptyReferenceId = samples.filter { it.getValue<String>(REFERENCE_ID_FIELD).isEmpty() }
if (samplesWithEmptyReferenceId.isEmpty()) return

val newReferenceId = UUID.randomUUID().toString()
samplesWithEmptyReferenceId.forEach { it.set(REFERENCE_ID_FIELD, newReferenceId) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,42 @@ class RealmMigrationsTest {
verify { newObject.set(eq(SubjectsSchemaV13.SUBJECT_ID_FIELD), eq(RealmUUID.from(GUID))) }
}

@Test
fun `when migrating 18 to 19 backfills empty referenceId shared per modality`() {
setVersions(18L, 19L)

val fingerprintSample1 = createSampleMock(referenceId = "")
val fingerprintSample2 = createSampleMock(referenceId = "")
val faceSample = createSampleMock(referenceId = "")
val untouchedFingerprintSample = createSampleMock(referenceId = "existing-ref")

val newObject = mockk<DynamicMutableRealmObject>()
every { newObject.getObjectList(REFERENCE_ID_FINGERPRINT_SAMPLES_FIELD) } returns
realmListOf(fingerprintSample1, fingerprintSample2, untouchedFingerprintSample)
every { newObject.getObjectList(REFERENCE_ID_FACE_SAMPLES_FIELD) } returns realmListOf(faceSample)

val enumeratorSlot = slot<(DynamicRealmObject, DynamicMutableRealmObject?) -> Unit>()
every { migrationContext.enumerate(any(), capture(enumeratorSlot)) } answers {
enumeratorSlot.captured.invoke(mockk(), newObject)
}

realmMigrations.migrate(migrationContext)

val fingerprintReferenceIdSlot = slot<String>()
verify(exactly = 1) { fingerprintSample1.set(REFERENCE_ID_FIELD, capture(fingerprintReferenceIdSlot)) }
verify(exactly = 1) { fingerprintSample2.set(REFERENCE_ID_FIELD, eq(fingerprintReferenceIdSlot.captured)) }
verify(exactly = 0) { untouchedFingerprintSample.set(REFERENCE_ID_FIELD, any<String>()) }
verify(exactly = 1) { faceSample.set(REFERENCE_ID_FIELD, any<String>()) }
assertThat(fingerprintReferenceIdSlot.captured).isNotEmpty()
Comment thread
meladRaouf marked this conversation as resolved.
}

private fun createSampleMock(referenceId: String): DynamicMutableRealmObject {
val sample = mockk<DynamicMutableRealmObject>()
every { sample.getValue<String>(REFERENCE_ID_FIELD) } returns referenceId
every { sample.set(REFERENCE_ID_FIELD, any<String>()) } returns sample
return sample
}

private fun setVersions(
old: Long,
new: Long,
Expand Down Expand Up @@ -198,5 +234,9 @@ class RealmMigrationsTest {

private const val UNIQUE_ID = "uniqueId"
private const val NOT_UNIQUE_ID = "nonUniqueId"

private const val REFERENCE_ID_FIELD = "referenceId"
private const val REFERENCE_ID_FINGERPRINT_SAMPLES_FIELD = "fingerprintSamples"
private const val REFERENCE_ID_FACE_SAMPLES_FIELD = "faceSamples"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
{
"formatVersion": 1,
"database": {
"version": 3,
"identityHash": "8dad14b775cb4eea6cb7293fbcf84b30",
"entities": [
{
"tableName": "DbSubject",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`subjectId` TEXT NOT NULL, `projectId` TEXT NOT NULL, `attendantId` TEXT NOT NULL, `moduleId` TEXT NOT NULL, `createdAt` INTEGER, `updatedAt` INTEGER, PRIMARY KEY(`subjectId`))",
"fields": [
{
"fieldPath": "subjectId",
"columnName": "subjectId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "projectId",
"columnName": "projectId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "attendantId",
"columnName": "attendantId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "moduleId",
"columnName": "moduleId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "createdAt",
"columnName": "createdAt",
"affinity": "INTEGER"
},
{
"fieldPath": "updatedAt",
"columnName": "updatedAt",
"affinity": "INTEGER"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"subjectId"
]
},
"indices": [
{
"name": "index_DbSubject_projectId_subjectId",
"unique": false,
"columnNames": [
"projectId",
"subjectId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_DbSubject_projectId_subjectId` ON `${TABLE_NAME}` (`projectId`, `subjectId`)"
},
{
"name": "index_DbSubject_projectId_moduleId_subjectId",
"unique": false,
"columnNames": [
"projectId",
"moduleId",
"subjectId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_DbSubject_projectId_moduleId_subjectId` ON `${TABLE_NAME}` (`projectId`, `moduleId`, `subjectId`)"
},
{
"name": "index_DbSubject_projectId_attendantId_subjectId",
"unique": false,
"columnNames": [
"projectId",
"attendantId",
"subjectId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_DbSubject_projectId_attendantId_subjectId` ON `${TABLE_NAME}` (`projectId`, `attendantId`, `subjectId`)"
}
]
},
{
"tableName": "DbBiometricTemplate",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `subjectId` TEXT NOT NULL, `identifier` INTEGER, `templateData` BLOB NOT NULL, `format` TEXT NOT NULL, `referenceId` TEXT NOT NULL, `modality` INTEGER NOT NULL, PRIMARY KEY(`uuid`), FOREIGN KEY(`subjectId`) REFERENCES `DbSubject`(`subjectId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "uuid",
"columnName": "uuid",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "subjectId",
"columnName": "subjectId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "identifier",
"columnName": "identifier",
"affinity": "INTEGER"
},
{
"fieldPath": "templateData",
"columnName": "templateData",
"affinity": "BLOB",
"notNull": true
},
{
"fieldPath": "format",
"columnName": "format",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "referenceId",
"columnName": "referenceId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "modality",
"columnName": "modality",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"uuid"
]
},
"indices": [
{
"name": "index_DbBiometricTemplate_format_subjectId",
"unique": false,
"columnNames": [
"format",
"subjectId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_DbBiometricTemplate_format_subjectId` ON `${TABLE_NAME}` (`format`, `subjectId`)"
},
{
"name": "index_DbBiometricTemplate_subjectId",
"unique": false,
"columnNames": [
"subjectId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_DbBiometricTemplate_subjectId` ON `${TABLE_NAME}` (`subjectId`)"
}
],
"foreignKeys": [
{
"table": "DbSubject",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"subjectId"
],
"referencedColumns": [
"subjectId"
]
}
]
},
{
"tableName": "DbExternalCredential",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `value` TEXT NOT NULL, `subjectId` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`value`, `subjectId`), FOREIGN KEY(`subjectId`) REFERENCES `DbSubject`(`subjectId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "value",
"columnName": "value",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "subjectId",
"columnName": "subjectId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"value",
"subjectId"
]
},
"indices": [
{
"name": "index_DbExternalCredential_subjectId",
"unique": false,
"columnNames": [
"subjectId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_DbExternalCredential_subjectId` ON `${TABLE_NAME}` (`subjectId`)"
}
],
"foreignKeys": [
{
"table": "DbSubject",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"subjectId"
],
"referencedColumns": [
"subjectId"
]
}
]
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '8dad14b775cb4eea6cb7293fbcf84b30')"
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import androidx.room.Room
import androidx.room.RoomDatabase
import com.simprints.infra.enrolment.records.room.store.SubjectsDatabase.Companion.SUBJECT_DB_VERSION
import com.simprints.infra.enrolment.records.room.store.migration.SubjectMigration1to2
import com.simprints.infra.enrolment.records.room.store.migration.SubjectMigration2to3
import com.simprints.infra.enrolment.records.room.store.models.DbBiometricTemplate
import com.simprints.infra.enrolment.records.room.store.models.DbExternalCredential
import com.simprints.infra.enrolment.records.room.store.models.DbSubject
Expand Down Expand Up @@ -35,12 +36,12 @@ abstract class SubjectsDatabase : RoomDatabase() {
): SubjectsDatabase {
val builder = Room
.databaseBuilder(context, SubjectsDatabase::class.java, dbName)
.addMigrations(SubjectMigration1to2())
.addMigrations(SubjectMigration1to2(), SubjectMigration2to3())
if (BuildConfig.DB_ENCRYPTION) {
builder.openHelperFactory(factory)
}
return builder.build()
}
const val SUBJECT_DB_VERSION = 2
const val SUBJECT_DB_VERSION = 3
}
}
Loading