diff --git a/app/build.gradle b/app/build.gradle index 0c2afc11..ca884ea9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -38,6 +38,10 @@ android { vectorDrawables.useSupportLibrary = true } + sourceSets { + androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) + } + signingConfigs { fastlane { storeFile file(getKey("keystore", "storeFile")) @@ -143,7 +147,7 @@ dependencies { kapt "androidx.room:room-compiler:$roomVersion" implementation "androidx.room:room-ktx:$roomVersion" implementation "androidx.room:room-rxjava2:$roomVersion" - testImplementation "androidx.room:room-testing:$roomVersion" + androidTestImplementation "androidx.room:room-testing:$roomVersion" implementation 'com.sylversky.fontreplacer:fontreplacer:1.0' implementation 'com.yqritc:recyclerview-flexibledivider:1.4.0' diff --git a/app/schemas/ro.code4.monitorizarevot.data.AppDatabase/2.json b/app/schemas/ro.code4.monitorizarevot.data.AppDatabase/2.json new file mode 100644 index 00000000..a028eb74 --- /dev/null +++ b/app/schemas/ro.code4.monitorizarevot.data.AppDatabase/2.json @@ -0,0 +1,613 @@ +{ + "formatVersion": 1, + "database": { + "version": 2, + "identityHash": "285e79174bf98be5e0811f6c6adc87e0", + "entities": [ + { + "tableName": "county", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `code` TEXT NOT NULL, `name` TEXT, `branchesCount` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "code", + "columnName": "code", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "branchesCount", + "columnName": "branchesCount", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_county_code", + "unique": true, + "columnNames": [ + "code" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_county_code` ON `${TABLE_NAME}` (`code`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "branch_details", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `countyCode` TEXT NOT NULL, `branchNumber` INTEGER NOT NULL, `isUrban` INTEGER NOT NULL, `isFemale` INTEGER NOT NULL, `arrivalTime` TEXT, `departureTime` TEXT, `isSynced` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`countyCode`) REFERENCES `county`(`code`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "countyCode", + "columnName": "countyCode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "branchNumber", + "columnName": "branchNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isUrban", + "columnName": "isUrban", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFemale", + "columnName": "isFemale", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "arrivalTime", + "columnName": "arrivalTime", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "departureTime", + "columnName": "departureTime", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isSynced", + "columnName": "isSynced", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_branch_details_countyCode_branchNumber", + "unique": true, + "columnNames": [ + "countyCode", + "branchNumber" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_branch_details_countyCode_branchNumber` ON `${TABLE_NAME}` (`countyCode`, `branchNumber`)" + } + ], + "foreignKeys": [ + { + "table": "county", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "countyCode" + ], + "referencedColumns": [ + "code" + ] + } + ] + }, + { + "tableName": "form_details", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`code` TEXT NOT NULL, `description` TEXT NOT NULL, `formVersion` INTEGER NOT NULL, PRIMARY KEY(`code`))", + "fields": [ + { + "fieldPath": "code", + "columnName": "code", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "formVersion", + "columnName": "formVersion", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "code" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "section", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `code` TEXT, `description` TEXT, `formCode` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`formCode`) REFERENCES `form_details`(`code`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "code", + "columnName": "code", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "formCode", + "columnName": "formCode", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "form_details", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "formCode" + ], + "referencedColumns": [ + "code" + ] + } + ] + }, + { + "tableName": "question", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `text` TEXT NOT NULL, `code` TEXT NOT NULL, `questionType` INTEGER NOT NULL, `sectionId` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`sectionId`) REFERENCES `section`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "code", + "columnName": "code", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "questionType", + "columnName": "questionType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sectionId", + "columnName": "sectionId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "section", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "sectionId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "answer", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `text` TEXT NOT NULL, `hasManualInput` INTEGER NOT NULL, `questionId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`questionId`) REFERENCES `question`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasManualInput", + "columnName": "hasManualInput", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "questionId", + "columnName": "questionId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "question", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "questionId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "answered_question", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `formCode` TEXT NOT NULL, `questionId` INTEGER NOT NULL, `countyCode` TEXT NOT NULL, `sectionNumber` INTEGER NOT NULL, `savedLocally` INTEGER NOT NULL, `synced` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`formCode`) REFERENCES `form_details`(`code`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`questionId`) REFERENCES `question`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`countyCode`, `sectionNumber`) REFERENCES `branch_details`(`countyCode`, `branchNumber`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "formCode", + "columnName": "formCode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "questionId", + "columnName": "questionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "countyCode", + "columnName": "countyCode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sectionNumber", + "columnName": "sectionNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "savedLocally", + "columnName": "savedLocally", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "synced", + "columnName": "synced", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_answered_question_countyCode_sectionNumber_id", + "unique": true, + "columnNames": [ + "countyCode", + "sectionNumber", + "id" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_answered_question_countyCode_sectionNumber_id` ON `${TABLE_NAME}` (`countyCode`, `sectionNumber`, `id`)" + } + ], + "foreignKeys": [ + { + "table": "form_details", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "formCode" + ], + "referencedColumns": [ + "code" + ] + }, + { + "table": "question", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "questionId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "branch_details", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "countyCode", + "sectionNumber" + ], + "referencedColumns": [ + "countyCode", + "branchNumber" + ] + } + ] + }, + { + "tableName": "selected_answer", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`optionId` INTEGER NOT NULL, `value` TEXT, `countyCode` TEXT NOT NULL, `branchNumber` INTEGER NOT NULL, `questionId` TEXT NOT NULL, PRIMARY KEY(`optionId`, `countyCode`, `branchNumber`), FOREIGN KEY(`optionId`) REFERENCES `answer`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`countyCode`, `branchNumber`, `questionId`) REFERENCES `answered_question`(`countyCode`, `sectionNumber`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "optionId", + "columnName": "optionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "countyCode", + "columnName": "countyCode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "branchNumber", + "columnName": "branchNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "questionId", + "columnName": "questionId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "optionId", + "countyCode", + "branchNumber" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "answer", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "optionId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "answered_question", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "countyCode", + "branchNumber", + "questionId" + ], + "referencedColumns": [ + "countyCode", + "sectionNumber", + "id" + ] + } + ] + }, + { + "tableName": "note", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `uriPath` TEXT, `description` TEXT NOT NULL, `questionId` INTEGER, `date` INTEGER NOT NULL, `countyCode` TEXT NOT NULL, `branchNumber` INTEGER NOT NULL, `synced` INTEGER NOT NULL, `someNewField` INTEGER NOT NULL, FOREIGN KEY(`questionId`) REFERENCES `question`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`countyCode`, `branchNumber`) REFERENCES `branch_details`(`countyCode`, `branchNumber`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uriPath", + "columnName": "uriPath", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "questionId", + "columnName": "questionId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "countyCode", + "columnName": "countyCode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "branchNumber", + "columnName": "branchNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "synced", + "columnName": "synced", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "someNewField", + "columnName": "someNewField", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_note_countyCode_branchNumber_questionId", + "unique": false, + "columnNames": [ + "countyCode", + "branchNumber", + "questionId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_note_countyCode_branchNumber_questionId` ON `${TABLE_NAME}` (`countyCode`, `branchNumber`, `questionId`)" + } + ], + "foreignKeys": [ + { + "table": "question", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "questionId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "branch_details", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "countyCode", + "branchNumber" + ], + "referencedColumns": [ + "countyCode", + "branchNumber" + ] + } + ] + } + ], + "views": [], + "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, '285e79174bf98be5e0811f6c6adc87e0')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/ro.code4.monitorizarevot.data.AppDatabase/3.json b/app/schemas/ro.code4.monitorizarevot.data.AppDatabase/3.json new file mode 100644 index 00000000..35f01402 --- /dev/null +++ b/app/schemas/ro.code4.monitorizarevot.data.AppDatabase/3.json @@ -0,0 +1,607 @@ +{ + "formatVersion": 1, + "database": { + "version": 3, + "identityHash": "5af29f99efb0111fa6e04a4b6a6feed5", + "entities": [ + { + "tableName": "county", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `code` TEXT NOT NULL, `name` TEXT, `branchesCount` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "code", + "columnName": "code", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "branchesCount", + "columnName": "branchesCount", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_county_code", + "unique": true, + "columnNames": [ + "code" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_county_code` ON `${TABLE_NAME}` (`code`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "branch_details", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `countyCode` TEXT NOT NULL, `branchNumber` INTEGER NOT NULL, `isUrban` INTEGER NOT NULL, `isFemale` INTEGER NOT NULL, `arrivalTime` TEXT, `departureTime` TEXT, `isSynced` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`countyCode`) REFERENCES `county`(`code`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "countyCode", + "columnName": "countyCode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "branchNumber", + "columnName": "branchNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isUrban", + "columnName": "isUrban", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFemale", + "columnName": "isFemale", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "arrivalTime", + "columnName": "arrivalTime", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "departureTime", + "columnName": "departureTime", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isSynced", + "columnName": "isSynced", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_branch_details_countyCode_branchNumber", + "unique": true, + "columnNames": [ + "countyCode", + "branchNumber" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_branch_details_countyCode_branchNumber` ON `${TABLE_NAME}` (`countyCode`, `branchNumber`)" + } + ], + "foreignKeys": [ + { + "table": "county", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "countyCode" + ], + "referencedColumns": [ + "code" + ] + } + ] + }, + { + "tableName": "form_details", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`code` TEXT NOT NULL, `description` TEXT NOT NULL, `formVersion` INTEGER NOT NULL, PRIMARY KEY(`code`))", + "fields": [ + { + "fieldPath": "code", + "columnName": "code", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "formVersion", + "columnName": "formVersion", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "code" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "section", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `code` TEXT, `description` TEXT, `formCode` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`formCode`) REFERENCES `form_details`(`code`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "code", + "columnName": "code", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "formCode", + "columnName": "formCode", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "form_details", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "formCode" + ], + "referencedColumns": [ + "code" + ] + } + ] + }, + { + "tableName": "question", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `text` TEXT NOT NULL, `code` TEXT NOT NULL, `questionType` INTEGER NOT NULL, `sectionId` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`sectionId`) REFERENCES `section`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "code", + "columnName": "code", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "questionType", + "columnName": "questionType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sectionId", + "columnName": "sectionId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "section", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "sectionId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "answer", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `text` TEXT NOT NULL, `hasManualInput` INTEGER NOT NULL, `questionId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`questionId`) REFERENCES `question`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasManualInput", + "columnName": "hasManualInput", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "questionId", + "columnName": "questionId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "question", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "questionId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "answered_question", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `formCode` TEXT NOT NULL, `questionId` INTEGER NOT NULL, `countyCode` TEXT NOT NULL, `sectionNumber` INTEGER NOT NULL, `savedLocally` INTEGER NOT NULL, `synced` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`formCode`) REFERENCES `form_details`(`code`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`questionId`) REFERENCES `question`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`countyCode`, `sectionNumber`) REFERENCES `branch_details`(`countyCode`, `branchNumber`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "formCode", + "columnName": "formCode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "questionId", + "columnName": "questionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "countyCode", + "columnName": "countyCode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sectionNumber", + "columnName": "sectionNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "savedLocally", + "columnName": "savedLocally", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "synced", + "columnName": "synced", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_answered_question_countyCode_sectionNumber_id", + "unique": true, + "columnNames": [ + "countyCode", + "sectionNumber", + "id" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_answered_question_countyCode_sectionNumber_id` ON `${TABLE_NAME}` (`countyCode`, `sectionNumber`, `id`)" + } + ], + "foreignKeys": [ + { + "table": "form_details", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "formCode" + ], + "referencedColumns": [ + "code" + ] + }, + { + "table": "question", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "questionId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "branch_details", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "countyCode", + "sectionNumber" + ], + "referencedColumns": [ + "countyCode", + "branchNumber" + ] + } + ] + }, + { + "tableName": "selected_answer", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`optionId` INTEGER NOT NULL, `value` TEXT, `countyCode` TEXT NOT NULL, `branchNumber` INTEGER NOT NULL, `questionId` TEXT NOT NULL, PRIMARY KEY(`optionId`, `countyCode`, `branchNumber`), FOREIGN KEY(`optionId`) REFERENCES `answer`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`countyCode`, `branchNumber`, `questionId`) REFERENCES `answered_question`(`countyCode`, `sectionNumber`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "optionId", + "columnName": "optionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "countyCode", + "columnName": "countyCode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "branchNumber", + "columnName": "branchNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "questionId", + "columnName": "questionId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "optionId", + "countyCode", + "branchNumber" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "answer", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "optionId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "answered_question", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "countyCode", + "branchNumber", + "questionId" + ], + "referencedColumns": [ + "countyCode", + "sectionNumber", + "id" + ] + } + ] + }, + { + "tableName": "note", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `uriPath` TEXT, `description` TEXT NOT NULL, `questionId` INTEGER, `date` INTEGER NOT NULL, `countyCode` TEXT NOT NULL, `branchNumber` INTEGER NOT NULL, `synced` INTEGER NOT NULL, FOREIGN KEY(`questionId`) REFERENCES `question`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`countyCode`, `branchNumber`) REFERENCES `branch_details`(`countyCode`, `branchNumber`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uriPath", + "columnName": "uriPath", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "questionId", + "columnName": "questionId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "countyCode", + "columnName": "countyCode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "branchNumber", + "columnName": "branchNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "synced", + "columnName": "synced", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_note_countyCode_branchNumber_questionId", + "unique": false, + "columnNames": [ + "countyCode", + "branchNumber", + "questionId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_note_countyCode_branchNumber_questionId` ON `${TABLE_NAME}` (`countyCode`, `branchNumber`, `questionId`)" + } + ], + "foreignKeys": [ + { + "table": "question", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "questionId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "branch_details", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "countyCode", + "branchNumber" + ], + "referencedColumns": [ + "countyCode", + "branchNumber" + ] + } + ] + } + ], + "views": [], + "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, '5af29f99efb0111fa6e04a4b6a6feed5')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/ro/code4/monitorizarevot/MigrationTest.kt b/app/src/androidTest/java/ro/code4/monitorizarevot/MigrationTest.kt new file mode 100644 index 00000000..665ca2da --- /dev/null +++ b/app/src/androidTest/java/ro/code4/monitorizarevot/MigrationTest.kt @@ -0,0 +1,149 @@ +package ro.code4.monitorizarevot + +import android.content.ContentValues +import android.database.sqlite.SQLiteDatabase +import androidx.room.Room +import androidx.room.testing.MigrationTestHelper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.* +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import ro.code4.monitorizarevot.data.AppDatabase +import ro.code4.monitorizarevot.data.Migrations +import java.io.IOException +import java.util.Date + +@RunWith(AndroidJUnit4::class) +class MigrationTest { + + @get:Rule + val helper = MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + AppDatabase::class.java.canonicalName + ) + + @Test + @Throws(IOException::class) + fun migrate1To2() { + var rowId = -1L + helper.createDatabase(TEST_DB, 1).use { + rowId = it.insert( + "note", + SQLiteDatabase.CONFLICT_REPLACE, + ContentValues().apply { + put("id", 1) + put("description", "test description") + put("date", Date().time) + put("countyCode", "TEST") + put("branchNumber", 1) + put("synced", 1) + } + ) + } + + helper.runMigrationsAndValidate(TEST_DB, 2, true, Migrations.MIGRATION_1_2).use { db -> + db.query("SELECT * FROM note WHERE rowid = ?", arrayOf(rowId)).use { c -> + assertNotNull(c) + assertTrue(c.moveToFirst()) + // Check new column + assertEquals(-1, c.getInt(c.getColumnIndex("someNewField"))) + // Check some old columns + assertEquals(1, c.getInt(c.getColumnIndex("id"))) + assertEquals("test description", c.getString(c.getColumnIndex("description"))) + } + } + + } + + @Test + @Throws(IOException::class) + fun migrate2To3() { + var rowId = -1L + helper.createDatabase(TEST_DB, 2).use { + rowId = it.insert( + "note", + SQLiteDatabase.CONFLICT_REPLACE, + ContentValues().apply { + put("id", 1) + put("description", "test description") + put("date", Date().time) + put("countyCode", "TEST") + put("branchNumber", 1) + put("synced", 1) + put("someNewField", 2) + } + ) + } + + helper.runMigrationsAndValidate(TEST_DB, 3, true, Migrations.MIGRATION_2_3).use { db -> + db.query("SELECT * FROM note WHERE rowid = ?", arrayOf(rowId)).use { c -> + assertNotNull(c) + assertTrue(c.moveToFirst()) + // Check column removed + assertEquals(-1, c.getColumnIndex("someNewField")) + // Check some old columns + assertEquals(1, c.getInt(c.getColumnIndex("id"))) + assertEquals("test description", c.getString(c.getColumnIndex("description"))) + } + } + } + + @Test + @Throws(IOException::class) + fun migrate1To3() { + var rowId = -1L + helper.createDatabase(TEST_DB, 1).use { + rowId = it.insert( + "note", + SQLiteDatabase.CONFLICT_REPLACE, + ContentValues().apply { + put("id", 1) + put("description", "test description") + put("date", Date().time) + put("countyCode", "TEST") + put("branchNumber", 1) + put("synced", 1) + } + ) + } + + helper.runMigrationsAndValidate(TEST_DB, 3, true, Migrations.MIGRATION_1_3).use { db -> + db.query("SELECT * FROM note WHERE rowid = ?", arrayOf(rowId)).use { c -> + assertNotNull(c) + assertTrue(c.moveToFirst()) + // Check column removed + assertEquals(-1, c.getColumnIndex("someNewField")) + // Check some old columns + assertEquals(1, c.getInt(c.getColumnIndex("id"))) + assertEquals("test description", c.getString(c.getColumnIndex("description"))) + } + } + } + + @Test + @Throws(IOException::class) + fun migrateAll() { + // Create earliest version of the database. + helper.createDatabase(TEST_DB, 1).apply { + close() + } + + // Open latest version of the database. Room will validate the schema + // once all migrations execute. + Room.databaseBuilder( + InstrumentationRegistry.getInstrumentation().targetContext, + AppDatabase::class.java, + TEST_DB + ).addMigrations(*Migrations.ALL).build().apply { + openHelper.writableDatabase + close() + } + } + + + companion object { + private const val TEST_DB = "test-db" + } +} \ No newline at end of file diff --git a/app/src/main/java/ro/code4/monitorizarevot/data/AppDatabase.kt b/app/src/main/java/ro/code4/monitorizarevot/data/AppDatabase.kt index 230069a2..2079bbe3 100644 --- a/app/src/main/java/ro/code4/monitorizarevot/data/AppDatabase.kt +++ b/app/src/main/java/ro/code4/monitorizarevot/data/AppDatabase.kt @@ -16,7 +16,7 @@ import ro.code4.monitorizarevot.data.model.answers.SelectedAnswer @Database( entities = [County::class, BranchDetails::class, FormDetails::class, Section::class, Question::class, Answer::class, AnsweredQuestion::class, SelectedAnswer::class, Note::class], - version = 1 + version = 3 ) @TypeConverters(DateConverter::class) abstract class AppDatabase : RoomDatabase() { @@ -38,7 +38,7 @@ abstract class AppDatabase : RoomDatabase() { context.applicationContext, AppDatabase::class.java, "database" - ).build() + ).addMigrations(*Migrations.ALL).build() } } } diff --git a/app/src/main/java/ro/code4/monitorizarevot/data/Migrations.kt b/app/src/main/java/ro/code4/monitorizarevot/data/Migrations.kt new file mode 100644 index 00000000..fc72159c --- /dev/null +++ b/app/src/main/java/ro/code4/monitorizarevot/data/Migrations.kt @@ -0,0 +1,61 @@ +package ro.code4.monitorizarevot.data + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +object Migrations { + /* + According to https://developer.android.com/reference/android/database/sqlite/package-summary Android has old SQLite. + For example, you can't rename column without recreating table + */ + + // this is an example for migration + val MIGRATION_1_2 = object : Migration(1, 2) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("ALTER TABLE note ADD COLUMN `someNewField` INTEGER NOT NULL DEFAULT -1") + } + } + + val MIGRATION_2_3 = object : Migration(2, 3) { + override fun migrate(database: SupportSQLiteDatabase) { + // We want to remove column, it requires recreating table + val commands = arrayOf( + "PRAGMA foreign_keys=OFF", + "BEGIN TRANSACTION;", + // It can be found in schema json file + """CREATE TABLE note_backup ( + `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + `uriPath` TEXT, + `description` TEXT NOT NULL, + `questionId` INTEGER, + `date` INTEGER NOT NULL, + `countyCode` TEXT NOT NULL, + `branchNumber` INTEGER NOT NULL, + `synced` INTEGER NOT NULL, + FOREIGN KEY(`questionId`) REFERENCES `question`(`id`) + ON UPDATE NO ACTION ON DELETE CASCADE, + FOREIGN KEY(`countyCode`, `branchNumber`) REFERENCES `branch_details`(`countyCode`, `branchNumber`) + ON UPDATE NO ACTION ON DELETE NO ACTION ); + """.trimIndent(), + "INSERT INTO note_backup SELECT `id`, `uriPath`, `description`, `questionId`, `date`, `countyCode`, `branchNumber`, `synced` FROM note;", + "DROP TABLE note;", + "ALTER TABLE note_backup RENAME TO note;", + // Don't forget about indices + "CREATE INDEX `index_note_countyCode_branchNumber_questionId` ON `note` (`countyCode`, `branchNumber`, `questionId`);", + "COMMIT;", + "PRAGMA foreign_keys=ON" + ) + for (command in commands) + database.execSQL(command) + } + } + + // Room can handle more than one version increment: we can define a migration that makes the migration process faster. + val MIGRATION_1_3 = object : Migration(1, 3) { + override fun migrate(database: SupportSQLiteDatabase) { + // do nothing, because version 2 adds new column and version 3 removes it + } + } + + val ALL: Array = arrayOf(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_1_3) +} \ No newline at end of file