Merge "Resolving bug where defaultValue with parenthesis fails onValidateSchema." into androidx-main
diff --git a/room/integration-tests/testapp/schemas/androidx.room.integration.testapp.migration.MigrationDb/13.json b/room/integration-tests/testapp/schemas/androidx.room.integration.testapp.migration.MigrationDb/13.json
new file mode 100644
index 0000000..ede0c4f
--- /dev/null
+++ b/room/integration-tests/testapp/schemas/androidx.room.integration.testapp.migration.MigrationDb/13.json
@@ -0,0 +1,153 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 13,
+ "identityHash": "243330983fa2a2c935ba4427cbd2238b",
+ "entities": [
+ {
+ "tableName": "Entity1",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `addedInV10` INTEGER NOT NULL DEFAULT 0, `added1InV13` INTEGER NOT NULL DEFAULT (0), PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "addedInV10",
+ "columnName": "addedInV10",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "added1InV13",
+ "columnName": "added1InV13",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "(0)"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_Entity1_name",
+ "unique": true,
+ "columnNames": [
+ "name"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Entity1_name` ON `${TABLE_NAME}` (`name`)"
+ },
+ {
+ "name": "index_Entity1_addedInV10",
+ "unique": false,
+ "columnNames": [
+ "addedInV10"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_Entity1_addedInV10` ON `${TABLE_NAME}` (`addedInV10`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Entity2",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `addedInV3` TEXT, `name` TEXT DEFAULT 'Unknown', `addedInV9` TEXT)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "addedInV3",
+ "columnName": "addedInV3",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "'Unknown'"
+ },
+ {
+ "fieldPath": "addedInV9",
+ "columnName": "addedInV9",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Entity4",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT COLLATE NOCASE, PRIMARY KEY(`id`), FOREIGN KEY(`name`) REFERENCES `Entity1`(`name`) 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
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": [
+ {
+ "table": "Entity1",
+ "onDelete": "NO ACTION",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "name"
+ ],
+ "referencedColumns": [
+ "name"
+ ]
+ }
+ ]
+ }
+ ],
+ "views": [
+ {
+ "viewName": "View1",
+ "createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT Entity4.id, Entity4.name, Entity1.id AS entity1Id FROM Entity4 INNER JOIN Entity1 ON Entity4.name = Entity1.name"
+ }
+ ],
+ "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, '243330983fa2a2c935ba4427cbd2238b')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/MigrationDb.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/MigrationDb.java
index 45ed433..6dce4f2 100644
--- a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/MigrationDb.java
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/MigrationDb.java
@@ -42,7 +42,7 @@
MigrationDb.Entity4.class},
views = {MigrationDb.View1.class})
public abstract class MigrationDb extends RoomDatabase {
- static final int LATEST_VERSION = 12;
+ static final int LATEST_VERSION = 13;
static final int MAX_VERSION = 1000;
abstract MigrationDao dao();
@Entity(indices = {
@@ -55,6 +55,8 @@
public String name;
@ColumnInfo(defaultValue = "0")
public int addedInV10;
+ @ColumnInfo(defaultValue = "(0)")
+ public int added1InV13;
}
@Entity
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/MigrationTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/MigrationTest.java
index 9f86d1d..60b5462 100644
--- a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/MigrationTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/MigrationTest.java
@@ -296,6 +296,29 @@
}
@Test
+ public void validateDefaultValueWithSurroundingParenthesis() throws IOException {
+ SupportSQLiteDatabase database = helper.createDatabase(TEST_DB, 12);
+ database.close();
+ Context targetContext = ApplicationProvider.getApplicationContext();
+ MigrationDb db = Room.databaseBuilder(targetContext, MigrationDb.class, TEST_DB)
+ .addMigrations(MIGRATION_12_13)
+ .build();
+
+ assertThat(db.dao().loadAllEntity1s(), is(notNullValue()));
+ helper.closeWhenFinished(db);
+
+ // Confirm if this is also works with the Migration Test Helper.
+ Throwable throwable = null;
+ try {
+ helper.runMigrationsAndValidate(TEST_DB, 13, true,
+ MIGRATION_12_13);
+ } catch (Throwable t) {
+ throwable = t;
+ }
+ assertThat(throwable, is(nullValue()));
+ }
+
+ @Test
public void missingMigration_onUpgrade() throws IOException {
SupportSQLiteDatabase database = helper.createDatabase(TEST_DB, 1);
database.close();
@@ -510,7 +533,7 @@
database.close();
Context targetContext = ApplicationProvider.getApplicationContext();
MigrationDb db = Room.databaseBuilder(targetContext, MigrationDb.class, TEST_DB)
- .addMigrations(new EmptyMigration(11, 12))
+ .addMigrations(new EmptyMigration(11, MIGRATION_MAX_LATEST.endVersion))
.build();
try {
db.dao().loadAllEntity1s();
@@ -657,6 +680,14 @@
}
};
+ private static final Migration MIGRATION_12_13 = new Migration(12, 13) {
+ @Override
+ public void migrate(@NonNull SupportSQLiteDatabase database) {
+ database.execSQL("ALTER TABLE Entity1 "
+ + "ADD COLUMN added1InV13 INTEGER NOT NULL DEFAULT (0)");
+ }
+ };
+
/**
* Downgrade migration from {@link MigrationDb#MAX_VERSION} to
* {@link MigrationDb#LATEST_VERSION} that uses the schema file and re-creates the tables such
@@ -697,7 +728,8 @@
private static final Migration[] ALL_MIGRATIONS = new Migration[]{MIGRATION_1_2,
MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7,
- MIGRATION_7_8, MIGRATION_8_9, MIGRATION_9_10, MIGRATION_10_11, MIGRATION_11_12};
+ MIGRATION_7_8, MIGRATION_8_9, MIGRATION_9_10, MIGRATION_10_11, MIGRATION_11_12,
+ MIGRATION_12_13};
static final class EmptyMigration extends Migration {
EmptyMigration(int startVersion, int endVersion) {
diff --git a/room/runtime/api/restricted_current.txt b/room/runtime/api/restricted_current.txt
index 8662b57..89994c2 100644
--- a/room/runtime/api/restricted_current.txt
+++ b/room/runtime/api/restricted_current.txt
@@ -302,6 +302,7 @@
public static final class TableInfo.Column {
ctor @Deprecated public TableInfo.Column(String!, String!, boolean, int);
ctor public TableInfo.Column(String!, String!, boolean, int, String!, int);
+ method public static boolean defaultValueEquals(String, String?);
method public boolean isPrimaryKey();
field @androidx.room.ColumnInfo.SQLiteTypeAffinity public final int affinity;
field public final String! defaultValue;
diff --git a/room/runtime/src/androidTest/java/androidx/room/migration/TableInfoTest.java b/room/runtime/src/androidTest/java/androidx/room/migration/TableInfoTest.java
index b71367d..f7eb2f8 100644
--- a/room/runtime/src/androidTest/java/androidx/room/migration/TableInfoTest.java
+++ b/room/runtime/src/androidTest/java/androidx/room/migration/TableInfoTest.java
@@ -17,6 +17,8 @@
package androidx.room.migration;
+import static com.google.common.truth.Truth.assertThat;
+
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
@@ -46,6 +48,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
@SuppressWarnings("ArraysAsListWithZeroOrOneArgument")
@@ -401,4 +404,74 @@
}).build()
).getWritableDatabase();
}
+
+ @Test
+ public void testSurroundingParenthesis() {
+ mDb = createDatabase(
+ "CREATE TABLE foo (name INTEGER NOT NULL DEFAULT ((0) + (1 + 2)))");
+ TableInfo dbInfo = TableInfo.read(mDb, "foo");
+
+ assertThat("((0) + (1 + 2))")
+ .isNotEqualTo(Objects.requireNonNull(dbInfo.columns.get("name")).defaultValue);
+
+ assertThat(TableInfo.Column.defaultValueEquals(
+ "((0) + (1 + 2))",
+ Objects.requireNonNull(dbInfo.columns.get("name")).defaultValue)).isTrue();
+ }
+
+ @Test
+ public void testDoubleSurroundingParenthesis() {
+ mDb = createDatabase(
+ "CREATE TABLE foo (name INTEGER NOT NULL DEFAULT (((0) + (1 + 2))))");
+ TableInfo dbInfo = TableInfo.read(mDb, "foo");
+
+ assertThat("(((0) + (1 + 2)))")
+ .isNotEqualTo(Objects.requireNonNull(dbInfo.columns.get("name")).defaultValue);
+
+ assertThat(TableInfo.Column.defaultValueEquals(
+ "(((0) + (1 + 2)))",
+ Objects.requireNonNull(dbInfo.columns.get("name")).defaultValue));
+ }
+
+ @Test
+ public void testMultipleParenthesisWithSurrounding() {
+ mDb = createDatabase(
+ "CREATE TABLE foo (name INTEGER NOT NULL DEFAULT (((3 + 5) + (2 + 1)) + (1 + 2)))");
+ TableInfo dbInfo = TableInfo.read(mDb, "foo");
+
+ assertThat("(((3 + 5) + (2 + 1)) + (1 + 2))")
+ .isNotEqualTo(Objects.requireNonNull(dbInfo.columns.get("name")).defaultValue);
+
+ assertThat(TableInfo.Column.defaultValueEquals(
+ "(((3 + 5) + (2 + 1)) + (1 + 2))",
+ Objects.requireNonNull(dbInfo.columns.get("name")).defaultValue));
+ }
+
+ @Test
+ public void testSurroundingParenthesisWithSpacesBefore() {
+ mDb = createDatabase(
+ "CREATE TABLE foo (name INTEGER NOT NULL DEFAULT ( (0) + (1 + 2)))");
+ TableInfo dbInfo = TableInfo.read(mDb, "foo");
+
+ assertThat("( (0) + (1 + 2))")
+ .isNotEqualTo(Objects.requireNonNull(dbInfo.columns.get("name")).defaultValue);
+
+ assertThat(TableInfo.Column.defaultValueEquals(
+ "( (0) + (1 + 2))",
+ Objects.requireNonNull(dbInfo.columns.get("name")).defaultValue));
+ }
+
+ @Test
+ public void testSurroundingParenthesisWithSpacesAfter() {
+ mDb = createDatabase(
+ "CREATE TABLE foo (name INTEGER NOT NULL DEFAULT ((0) + (1 + 2) ))");
+ TableInfo dbInfo = TableInfo.read(mDb, "foo");
+
+ assertThat("((0) + (1 + 2) )")
+ .isNotEqualTo(Objects.requireNonNull(dbInfo.columns.get("name")).defaultValue);
+
+ assertThat(TableInfo.Column.defaultValueEquals(
+ "((0) + (1 + 2) )",
+ Objects.requireNonNull(dbInfo.columns.get("name")).defaultValue)).isTrue();
+ }
}
diff --git a/room/runtime/src/main/java/androidx/room/util/TableInfo.java b/room/runtime/src/main/java/androidx/room/util/TableInfo.java
index f884f3f..37b41d6 100644
--- a/room/runtime/src/main/java/androidx/room/util/TableInfo.java
+++ b/room/runtime/src/main/java/androidx/room/util/TableInfo.java
@@ -237,6 +237,7 @@
private static Map<String, Column> readColumns(SupportSQLiteDatabase database,
String tableName) {
+
Cursor cursor = database
.query("PRAGMA table_info(`" + tableName + "`)");
//noinspection TryFinallyCanBeTryWithResources
@@ -456,15 +457,18 @@
// from the compiler itself has it. b/136019383
if (mCreatedFrom == CREATED_FROM_ENTITY
&& column.mCreatedFrom == CREATED_FROM_DATABASE
- && (defaultValue != null && !defaultValue.equals(column.defaultValue))) {
+ && (defaultValue != null && !defaultValueEquals(defaultValue,
+ column.defaultValue))) {
return false;
} else if (mCreatedFrom == CREATED_FROM_DATABASE
&& column.mCreatedFrom == CREATED_FROM_ENTITY
- && (column.defaultValue != null && !column.defaultValue.equals(defaultValue))) {
+ && (column.defaultValue != null && !defaultValueEquals(
+ column.defaultValue, defaultValue))) {
return false;
} else if (mCreatedFrom != CREATED_FROM_UNKNOWN
&& mCreatedFrom == column.mCreatedFrom
- && (defaultValue != null ? !defaultValue.equals(column.defaultValue)
+ && (defaultValue != null ? !defaultValueEquals(defaultValue,
+ column.defaultValue)
: column.defaultValue != null)) {
return false;
}
@@ -473,6 +477,56 @@
}
/**
+ * Checks if the default values provided match. Handles the special case in which the
+ * default value is surrounded by parenthesis (e.g. encountered in b/182284899).
+ *
+ * Surrounding parenthesis are removed by SQLite when reading from the database, hence
+ * this function will check if they are present in the actual value, if so, it will
+ * compare the two values by ignoring the surrounding parenthesis.
+ *
+ */
+ public static boolean defaultValueEquals(@NonNull String actual, @Nullable String other) {
+ if (other == null) {
+ return false;
+ }
+
+ if (actual.equals(other)) {
+ return true;
+ } else if (containsSurroundingParenthesis(actual)) {
+ return actual.substring(1, actual.length() - 1).trim().equals(other);
+ }
+ return false;
+ }
+
+ /**
+ * Checks for potential surrounding parenthesis, if found, removes them and checks if
+ * remaining paranthesis are balanced. If so, the surrounding parenthesis are redundant,
+ * and returns true.
+ */
+ private static boolean containsSurroundingParenthesis(@NonNull String actual) {
+ if (actual.length() == 0) {
+ return false;
+ }
+ int surroundingParenthesis = 0;
+ for (int i = 0; i < actual.length(); i++) {
+ char c = actual.charAt(i);
+ if (i == 0 && c != '(') {
+ return false;
+ }
+
+ if (c == '(') {
+ surroundingParenthesis++;
+ } else if (c == ')') {
+ surroundingParenthesis--;
+ if (surroundingParenthesis == 0 && i != actual.length() - 1) {
+ return false;
+ }
+ }
+ }
+ return surroundingParenthesis == 0;
+ }
+
+ /**
* Returns whether this column is part of the primary key or not.
*
* @return True if this column is part of the primary key, false otherwise.