Handle foreign key violations during Auto Migration more gracefully.
When doing a PRAGMA foreign key check after complex changes have been performed on the database, in the case of a foreign key violation, we are now outputting a clear error message notifying the user that a violation has been found, including the name of the relevant entity.
Bug: 190113935
Test: TransactionMethodProcessorTest.kt
Change-Id: I68e256074d2aa801bb8db9fced5e0099ff51b6df
diff --git a/room/integration-tests/testapp/schemas/androidx.room.integration.testapp.migration.AutoMigrationDb/1.json b/room/integration-tests/testapp/schemas/androidx.room.integration.testapp.migration.AutoMigrationDb/1.json
index 0477196..3ca5c0e 100644
--- a/room/integration-tests/testapp/schemas/androidx.room.integration.testapp.migration.AutoMigrationDb/1.json
+++ b/room/integration-tests/testapp/schemas/androidx.room.integration.testapp.migration.AutoMigrationDb/1.json
@@ -820,6 +820,33 @@
},
"indices": [],
"foreignKeys": []
+ },
+ {
+ "tableName": "Entity27",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id27` INTEGER NOT NULL, `addedInV1` INTEGER NOT NULL DEFAULT 1, PRIMARY KEY(`id27`))",
+ "fields": [
+ {
+ "fieldPath": "id27",
+ "columnName": "id27",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "addedInV1",
+ "columnName": "addedInV1",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id27"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
}
],
"views": [
diff --git a/room/integration-tests/testapp/schemas/androidx.room.integration.testapp.migration.AutoMigrationDb/2.json b/room/integration-tests/testapp/schemas/androidx.room.integration.testapp.migration.AutoMigrationDb/2.json
index f8edc20..dfc2b4c 100644
--- a/room/integration-tests/testapp/schemas/androidx.room.integration.testapp.migration.AutoMigrationDb/2.json
+++ b/room/integration-tests/testapp/schemas/androidx.room.integration.testapp.migration.AutoMigrationDb/2.json
@@ -2,7 +2,7 @@
"formatVersion": 1,
"database": {
"version": 2,
- "identityHash": "2dee70d4b5bbc3df9701e56eedb6377c",
+ "identityHash": "e6ac760b64fa91da605ebdfefc93c88e",
"entities": [
{
"tableName": "Entity1",
@@ -269,7 +269,7 @@
},
{
"tableName": "Entity9",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `addedInV1` INTEGER NOT NULL DEFAULT 1, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `Entity12`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION DEFERRABLE INITIALLY DEFERRED)",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `addedInV1` INTEGER NOT NULL DEFAULT 1, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
@@ -298,19 +298,7 @@
"autoGenerate": false
},
"indices": [],
- "foreignKeys": [
- {
- "table": "Entity12",
- "onDelete": "NO ACTION",
- "onUpdate": "NO ACTION",
- "columns": [
- "id"
- ],
- "referencedColumns": [
- "id"
- ]
- }
- ]
+ "foreignKeys": []
},
{
"tableName": "Entity10",
@@ -916,6 +904,33 @@
}
],
"foreignKeys": []
+ },
+ {
+ "tableName": "Entity27",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id27` INTEGER NOT NULL, `addedInV1` INTEGER NOT NULL DEFAULT 1, PRIMARY KEY(`id27`))",
+ "fields": [
+ {
+ "fieldPath": "id27",
+ "columnName": "id27",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "addedInV1",
+ "columnName": "addedInV1",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id27"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
}
],
"views": [
@@ -926,7 +941,7 @@
],
"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, '2dee70d4b5bbc3df9701e56eedb6377c')"
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e6ac760b64fa91da605ebdfefc93c88e')"
]
}
}
\ No newline at end of file
diff --git a/room/integration-tests/testapp/schemas/androidx.room.integration.testapp.migration.AutoMigrationDb/3.json b/room/integration-tests/testapp/schemas/androidx.room.integration.testapp.migration.AutoMigrationDb/3.json
new file mode 100644
index 0000000..d227742
--- /dev/null
+++ b/room/integration-tests/testapp/schemas/androidx.room.integration.testapp.migration.AutoMigrationDb/3.json
@@ -0,0 +1,959 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 3,
+ "identityHash": "d3f9ad7159086752b95773a87757b6f6",
+ "entities": [
+ {
+ "tableName": "Entity1",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `addedInV1` INTEGER NOT NULL DEFAULT 1, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "addedInV1",
+ "columnName": "addedInV1",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Entity2",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `addedInV1` INTEGER NOT NULL DEFAULT 1, `addedInV2` INTEGER NOT NULL DEFAULT 2, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "addedInV1",
+ "columnName": "addedInV1",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "addedInV2",
+ "columnName": "addedInV2",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "2"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Entity3",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Entity4",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `addedInV1` INTEGER NOT NULL DEFAULT 2, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "addedInV1",
+ "columnName": "addedInV1",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "2"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Entity5",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `addedInV1` TEXT DEFAULT '1', PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "addedInV1",
+ "columnName": "addedInV1",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'1'"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Entity6",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `addedInV1` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "addedInV1",
+ "columnName": "addedInV1",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Entity7",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `addedInV1` INTEGER NOT NULL DEFAULT 1, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "addedInV1",
+ "columnName": "addedInV1",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Entity8",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `addedInV1` INTEGER NOT NULL DEFAULT 1, PRIMARY KEY(`name`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "addedInV1",
+ "columnName": "addedInV1",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "name"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Entity9",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `addedInV1` INTEGER NOT NULL DEFAULT 1, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `Entity27`(`id27`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "addedInV1",
+ "columnName": "addedInV1",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": [
+ {
+ "table": "Entity27",
+ "onDelete": "NO ACTION",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "id"
+ ],
+ "referencedColumns": [
+ "id27"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "Entity10",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `addedInV1` INTEGER NOT NULL DEFAULT 1, PRIMARY KEY(`id`), FOREIGN KEY(`addedInV1`) REFERENCES `Entity13_V2`(`addedInV1`) ON UPDATE NO ACTION ON DELETE NO ACTION DEFERRABLE INITIALLY DEFERRED)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "addedInV1",
+ "columnName": "addedInV1",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_Entity10_addedInV1",
+ "unique": true,
+ "columnNames": [
+ "addedInV1"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Entity10_addedInV1` ON `${TABLE_NAME}` (`addedInV1`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "Entity13_V2",
+ "onDelete": "NO ACTION",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "addedInV1"
+ ],
+ "referencedColumns": [
+ "addedInV1"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "Entity11",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `addedInV1` INTEGER NOT NULL DEFAULT 1, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "addedInV1",
+ "columnName": "addedInV1",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Entity12",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `addedInV1` INTEGER NOT NULL DEFAULT 1, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "addedInV1",
+ "columnName": "addedInV1",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_Entity12_name",
+ "unique": true,
+ "columnNames": [
+ "name"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Entity12_name` ON `${TABLE_NAME}` (`name`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Entity13_V2",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `addedInV1` INTEGER NOT NULL DEFAULT 1, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "addedInV1",
+ "columnName": "addedInV1",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_Entity13_V2_addedInV1",
+ "unique": true,
+ "columnNames": [
+ "addedInV1"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Entity13_V2_addedInV1` ON `${TABLE_NAME}` (`addedInV1`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Entity14",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Entity15",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Entity16",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `renamedInV2` INTEGER NOT NULL DEFAULT 1, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "renamedInV2",
+ "columnName": "renamedInV2",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Entity17",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `renamedInV2` TEXT DEFAULT '1', PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "renamedInV2",
+ "columnName": "renamedInV2",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'1'"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Entity19_V2",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `addedInV1` INTEGER NOT NULL DEFAULT 1, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "addedInV1",
+ "columnName": "addedInV1",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Entity20_V2",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `renamedInV2` TEXT DEFAULT '1', `addedInV2` INTEGER NOT NULL DEFAULT 2, PRIMARY KEY(`name`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "renamedInV2",
+ "columnName": "renamedInV2",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'1'"
+ },
+ {
+ "fieldPath": "addedInV2",
+ "columnName": "addedInV2",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "2"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "name"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "ftsVersion": "FTS4",
+ "ftsOptions": {
+ "tokenizer": "simple",
+ "tokenizerArgs": [],
+ "contentTable": "Entity13_V2",
+ "languageIdColumnName": "",
+ "matchInfo": "FTS4",
+ "notIndexedColumns": [],
+ "prefixSizes": [],
+ "preferredOrder": "ASC"
+ },
+ "contentSyncTriggers": [
+ "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_Entity21_BEFORE_UPDATE BEFORE UPDATE ON `Entity13_V2` BEGIN DELETE FROM `Entity21` WHERE `docid`=OLD.`rowid`; END",
+ "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_Entity21_BEFORE_DELETE BEFORE DELETE ON `Entity13_V2` BEGIN DELETE FROM `Entity21` WHERE `docid`=OLD.`rowid`; END",
+ "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_Entity21_AFTER_UPDATE AFTER UPDATE ON `Entity13_V2` BEGIN INSERT INTO `Entity21`(`docid`, `name`, `addedInV1`) VALUES (NEW.`rowid`, NEW.`name`, NEW.`addedInV1`); END",
+ "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_Entity21_AFTER_INSERT AFTER INSERT ON `Entity13_V2` BEGIN INSERT INTO `Entity21`(`docid`, `name`, `addedInV1`) VALUES (NEW.`rowid`, NEW.`name`, NEW.`addedInV1`); END"
+ ],
+ "tableName": "Entity21",
+ "createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`name` TEXT, `addedInV1` INTEGER NOT NULL DEFAULT 1, content=`Entity13_V2`)",
+ "fields": [
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "addedInV1",
+ "columnName": "addedInV1",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "rowid"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "ftsVersion": "FTS4",
+ "ftsOptions": {
+ "tokenizer": "simple",
+ "tokenizerArgs": [],
+ "contentTable": "",
+ "languageIdColumnName": "",
+ "matchInfo": "FTS4",
+ "notIndexedColumns": [],
+ "prefixSizes": [],
+ "preferredOrder": "ASC"
+ },
+ "contentSyncTriggers": [],
+ "tableName": "Entity22",
+ "createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`name` TEXT, `addedInV1` INTEGER NOT NULL DEFAULT 1)",
+ "fields": [
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "addedInV1",
+ "columnName": "addedInV1",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "rowid"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "ftsVersion": "FTS3",
+ "ftsOptions": {
+ "tokenizer": "simple",
+ "tokenizerArgs": [],
+ "contentTable": "",
+ "languageIdColumnName": "",
+ "matchInfo": "FTS4",
+ "notIndexedColumns": [],
+ "prefixSizes": [],
+ "preferredOrder": "ASC"
+ },
+ "contentSyncTriggers": [],
+ "tableName": "Entity23",
+ "createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS3(`name` TEXT, `addedInV1` INTEGER NOT NULL DEFAULT 1, `addedInV2` INTEGER NOT NULL DEFAULT 2)",
+ "fields": [
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "addedInV1",
+ "columnName": "addedInV1",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "addedInV2",
+ "columnName": "addedInV2",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "2"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "rowid"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "ftsVersion": "FTS3",
+ "ftsOptions": {
+ "tokenizer": "simple",
+ "tokenizerArgs": [],
+ "contentTable": "",
+ "languageIdColumnName": "",
+ "matchInfo": "FTS4",
+ "notIndexedColumns": [],
+ "prefixSizes": [],
+ "preferredOrder": "ASC"
+ },
+ "contentSyncTriggers": [],
+ "tableName": "Entity24",
+ "createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS3(`name` TEXT, `addedInV1` INTEGER NOT NULL DEFAULT 1)",
+ "fields": [
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "addedInV1",
+ "columnName": "addedInV1",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "rowid"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Entity25",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `entity1Id` INTEGER NOT NULL DEFAULT 1, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "entity1Id",
+ "columnName": "entity1Id",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Entity26",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `addedInV2` INTEGER NOT NULL DEFAULT 1, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "addedInV2",
+ "columnName": "addedInV2",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_Entity26_addedInV2",
+ "unique": true,
+ "columnNames": [
+ "addedInV2"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Entity26_addedInV2` ON `${TABLE_NAME}` (`addedInV2`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Entity27",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id27` INTEGER NOT NULL, `addedInV1` INTEGER NOT NULL DEFAULT 1, PRIMARY KEY(`id27`))",
+ "fields": [
+ {
+ "fieldPath": "id27",
+ "columnName": "id27",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "addedInV1",
+ "columnName": "addedInV1",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id27"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [
+ {
+ "viewName": "Entity25Detail",
+ "createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT Entity25.id, Entity25.name, Entity25.entity1Id, Entity1.name AS userNameAndId FROM Entity25 INNER JOIN Entity1 ON Entity25.entity1Id = Entity1.id"
+ }
+ ],
+ "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, 'd3f9ad7159086752b95773a87757b6f6')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/AutoMigrationDb.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/AutoMigrationDb.java
index e4e47974..f910674 100644
--- a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/AutoMigrationDb.java
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/AutoMigrationDb.java
@@ -67,11 +67,15 @@
AutoMigrationDb.Entity23.class,
AutoMigrationDb.Entity24.class,
AutoMigrationDb.Entity25.class,
- AutoMigrationDb.Entity26.class
+ AutoMigrationDb.Entity26.class,
+ AutoMigrationDb.Entity27.class
},
autoMigrations = {
@AutoMigration(
from = 1, to = 2, spec = AutoMigrationDb.SimpleAutoMigration1.class
+ ),
+ @AutoMigration(
+ from = 2, to = 3
)
},
views = {
@@ -80,7 +84,7 @@
exportSchema = true
)
public abstract class AutoMigrationDb extends RoomDatabase {
- static final int LATEST_VERSION = 2;
+ static final int LATEST_VERSION = 3;
abstract AutoMigrationDb.AutoMigrationDao dao();
/**
@@ -192,10 +196,9 @@
* Add a foreign key to Entity 9.
*/
@Entity(foreignKeys = {
- @ForeignKey(entity = Entity12.class,
- parentColumns = "id",
- childColumns = "id",
- deferred = true)})
+ @ForeignKey(entity = Entity27.class,
+ parentColumns = "id27",
+ childColumns = "id")})
static class Entity9 {
public static final String TABLE_NAME = "Entity9";
@PrimaryKey
@@ -434,6 +437,19 @@
public int addedInV2;
}
+ /**
+ * No change between versions.
+ */
+ @Entity
+ static class Entity27 {
+ public static final String TABLE_NAME = "Entity27";
+ @PrimaryKey
+ public int id27;
+ @ColumnInfo(defaultValue = "1")
+ public int addedInV1;
+ }
+
+
@Dao
interface AutoMigrationDao {
@Query("SELECT * from Entity1 ORDER BY id ASC")
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/AutoMigrationTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/AutoMigrationTest.java
index 3206ff9..fb3cd7c 100644
--- a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/AutoMigrationTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/AutoMigrationTest.java
@@ -16,8 +16,10 @@
package androidx.room.integration.testapp.migration;
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.MatcherAssert.assertThat;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.database.sqlite.SQLiteException;
import androidx.annotation.NonNull;
import androidx.room.migration.Migration;
@@ -54,7 +56,10 @@
// Run this to create the very 1st version of the db.
public void createFirstVersion() throws IOException {
SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 1);
- db.execSQL("INSERT INTO Entity1 (id, name) VALUES (1, 'row1')");
+ db.execSQL("INSERT INTO Entity9 (id, name) VALUES (1, 'row1')");
+ db.execSQL("INSERT INTO Entity9 (id, name) VALUES (2, 'row2')");
+ db.execSQL("INSERT INTO Entity27 (id27) VALUES (3)");
+ db.execSQL("INSERT INTO Entity27 (id27) VALUES (5)");
db.close();
}
@@ -67,7 +72,25 @@
true
);
final TableInfo info = TableInfo.read(db, AutoMigrationDb.Entity1.TABLE_NAME);
- assertThat(info.columns.size(), is(3));
+ assertThat(info.columns.size()).isEqualTo(3);
+ }
+
+ @Test
+ public void goFromV1ToV3() throws IOException {
+ createFirstVersion();
+ try {
+ SupportSQLiteDatabase db = helper.runMigrationsAndValidate(
+ TEST_DB,
+ 3,
+ true
+ );
+ } catch (IllegalStateException e) {
+ assertThat(e.getMessage()).isEqualTo("Foreign key violation(s) detected in 'Entity9'."
+ + "\nNumber of different violations discovered: 1"
+ + "\nNumber of rows in violation: 2"
+ + "\nViolation(s) detected in the following constraint(s):\n"
+ + "\tParent Table = Entity27, Foreign Key Constraint Index = 0\n");
+ }
}
/**
@@ -75,12 +98,17 @@
*/
@Test
public void testAutoMigrationsNotProcessedBeforeCustomMigrations() throws IOException {
- helper.runMigrationsAndValidate(
- TEST_DB,
- 2,
- true,
- MIGRATION_1_2
- );
+ createFirstVersion();
+ try {
+ helper.runMigrationsAndValidate(
+ TEST_DB,
+ 2,
+ true,
+ MIGRATION_1_2
+ );
+ } catch (SQLiteException e) {
+ assertThat(e.getMessage()).containsMatch("no such table: Entity0");
+ }
}
private static final Migration MIGRATION_1_2 = new Migration(1, 2) {
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/writer/AutoMigrationWriter.kt b/room/room-compiler/src/main/kotlin/androidx/room/writer/AutoMigrationWriter.kt
index 8f4b05c..1279221 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/writer/AutoMigrationWriter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/writer/AutoMigrationWriter.kt
@@ -373,9 +373,10 @@
tableName: String,
migrateBuilder: MethodSpec.Builder
) {
- addDatabaseExecuteSqlStatement(
- migrateBuilder,
- "PRAGMA foreign_key_check(`$tableName`)"
+ migrateBuilder.addStatement(
+ "$T.foreignKeyCheck(database, $S)",
+ RoomTypeNames.DB_UTIL,
+ tableName
)
}
diff --git a/room/room-runtime/api/restricted_current.txt b/room/room-runtime/api/restricted_current.txt
index 51e2dac..9eaf43a 100644
--- a/room/room-runtime/api/restricted_current.txt
+++ b/room/room-runtime/api/restricted_current.txt
@@ -267,6 +267,7 @@
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class DBUtil {
method public static android.os.CancellationSignal? createCancellationSignal();
method public static void dropFtsSyncTriggers(androidx.sqlite.db.SupportSQLiteDatabase!);
+ method public static void foreignKeyCheck(androidx.sqlite.db.SupportSQLiteDatabase, String);
method @Deprecated public static android.database.Cursor query(androidx.room.RoomDatabase!, androidx.sqlite.db.SupportSQLiteQuery!, boolean);
method public static android.database.Cursor query(androidx.room.RoomDatabase, androidx.sqlite.db.SupportSQLiteQuery, boolean, android.os.CancellationSignal?);
method public static int readVersion(java.io.File) throws java.io.IOException;
diff --git a/room/room-runtime/src/main/java/androidx/room/util/DBUtil.java b/room/room-runtime/src/main/java/androidx/room/util/DBUtil.java
index c5014ae..8f876ae 100644
--- a/room/room-runtime/src/main/java/androidx/room/util/DBUtil.java
+++ b/room/room-runtime/src/main/java/androidx/room/util/DBUtil.java
@@ -35,7 +35,9 @@
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
/**
* Database utilities for Room
@@ -127,6 +129,22 @@
}
/**
+ * Checks for foreign key violations by executing a PRAGMA foreign_key_check.
+ */
+ public static void foreignKeyCheck(@NonNull SupportSQLiteDatabase db,
+ @NonNull String tableName) {
+ Cursor cursor = db.query("PRAGMA foreign_key_check(`" + tableName + "`)");
+ try {
+ if (cursor.getCount() > 0) {
+ String errorMsg = processForeignKeyCheckFailure(cursor);
+ throw new IllegalStateException(errorMsg);
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+ /**
* Reads the user version number out of the database header from the given file.
*
* @param databaseFile the database file.
@@ -171,6 +189,55 @@
return null;
}
+
+ /**
+ * Converts the {@link Cursor} returned in case of a foreign key violation into a detailed
+ * error message for debugging.
+ * <p>
+ * The foreign_key_check pragma returns one row output for each foreign key violation.
+ * <p>
+ * The cursor received has four columns for each row output. The first column is the name of
+ * the child table. The second column is the rowId of the row that contains the foreign key
+ * violation (or NULL if the child table is a WITHOUT ROWID table). The third column is the
+ * name of the parent table. The fourth column is the index of the specific foreign key
+ * constraint that failed.
+ *
+ * @param cursor Cursor containing information regarding the FK violation
+ * @return Error message generated containing debugging information
+ */
+ private static String processForeignKeyCheckFailure(Cursor cursor) {
+ int rowCount = cursor.getCount();
+ String childTableName = null;
+ Map<String, String> fkParentTables = new HashMap<>();
+
+ while (cursor.moveToNext()) {
+ if (childTableName == null) {
+ childTableName = cursor.getString(0);
+ }
+ String constraintIndex = cursor.getString(3);
+ if (!fkParentTables.containsKey(constraintIndex)) {
+ fkParentTables.put(constraintIndex, cursor.getString(2));
+ }
+ }
+
+ StringBuilder sb = new StringBuilder();
+ sb.append("Foreign key violation(s) detected in '")
+ .append(childTableName).append("'.\n");
+ sb.append("Number of different violations discovered: ")
+ .append(fkParentTables.keySet().size()).append("\n");
+ sb.append("Number of rows in violation: ")
+ .append(rowCount).append("\n");
+ sb.append("Violation(s) detected in the following constraint(s):\n");
+
+ for (Map.Entry<String, String> entry : fkParentTables.entrySet()) {
+ sb.append("\tParent Table = ")
+ .append(entry.getValue());
+ sb.append(", Foreign Key Constraint Index = ")
+ .append(entry.getKey()).append("\n");
+ }
+ return sb.toString();
+ }
+
private DBUtil() {
}
}