Validate indices in migrations

This CL improves the database migration to include index validation.

Even though we can extract more information about the index like the
order, since Room APIs do not allow it, we do not verify them.

The pragma was added in 2015 so if we cannot read index value,
we skip index validation.

Bug: 63132683
Test: MigrationTest, TableInfoTest
Change-Id: I01f35be08232f6a6bb22766553e48f7b177a21e4
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/ext/javapoet_ext.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/ext/javapoet_ext.kt
index daaa8e3..066bd1f 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/ext/javapoet_ext.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/ext/javapoet_ext.kt
@@ -78,6 +78,8 @@
             ClassName.get("android.arch.persistence.room.util", "TableInfo.Column")
     val TABLE_INFO_FOREIGN_KEY : ClassName =
             ClassName.get("android.arch.persistence.room.util", "TableInfo.ForeignKey")
+    val TABLE_INFO_INDEX : ClassName =
+            ClassName.get("android.arch.persistence.room.util", "TableInfo.Index")
     val LIMIT_OFFSET_DATA_SOURCE : ClassName =
             ClassName.get("android.arch.persistence.room.paging", "LimitOffsetDataSource")
 }
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/EntityProcessor.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/EntityProcessor.kt
index 72cff74..405c6b1 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/EntityProcessor.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/EntityProcessor.kt
@@ -524,7 +524,7 @@
         }
 
         private fun createIndexName(columnNames: List<String>, tableName: String): String {
-            return "index_" + tableName + "_" + columnNames.joinToString("_")
+            return Index.DEFAULT_PREFIX + tableName + "_" + columnNames.joinToString("_")
         }
 
         private fun extractForeignKeys(annotation: AnnotationMirror): List<ForeignKeyInput> {
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Index.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Index.kt
index 69f16f3..0bee2e0 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Index.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Index.kt
@@ -23,7 +23,10 @@
  * Represents a processed index.
  */
 data class Index(val name : String, val unique : Boolean, val fields : List<Field>) {
-
+    companion object {
+        // should match the value in TableInfo.Index.DEFAULT_PREFIX
+        const val DEFAULT_PREFIX = "index_"
+    }
     fun createQuery(tableName : String) : String {
         val uniqueSQL = if (unique) {
             "UNIQUE"
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/TableInfoValidationWriter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/TableInfoValidationWriter.kt
index fe9e743..e54f986 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/TableInfoValidationWriter.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/TableInfoValidationWriter.kt
@@ -75,9 +75,26 @@
                         /*parent column names*/ refColumnNames)
             }
 
-            addStatement("final $T $L = new $T($S, $L, $L)",
+            val indicesSetVar = scope.getTmpVar("_indices$suffix")
+            val indicesType = ParameterizedTypeName.get(HashSet::class.typeName(),
+                    RoomTypeNames.TABLE_INFO_INDEX)
+            addStatement("final $T $L = new $T($L)", indicesType, indicesSetVar,
+                    indicesType, entity.indices.size)
+            entity.indices.forEach { index ->
+                val columnNames = index.fields
+                        .joinToString(",") { "\"${it.columnName}\"" }
+                addStatement("$L.add(new $T($S, $L, $T.asList($L)))",
+                        indicesSetVar,
+                        RoomTypeNames.TABLE_INFO_INDEX,
+                        index.name,
+                        index.unique,
+                        Arrays::class.typeName(),
+                        columnNames)
+            }
+
+            addStatement("final $T $L = new $T($S, $L, $L, $L)",
                     RoomTypeNames.TABLE_INFO, expectedInfoVar, RoomTypeNames.TABLE_INFO,
-                    entity.tableName, columnListVar, foreignKeySetVar)
+                    entity.tableName, columnListVar, foreignKeySetVar, indicesSetVar)
 
             val existingVar = scope.getTmpVar("_existing$suffix")
             addStatement("final $T $L = $T.read($N, $S)",
diff --git a/room/compiler/src/test/data/databasewriter/output/ComplexDatabase.java b/room/compiler/src/test/data/databasewriter/output/ComplexDatabase.java
index 80d73a9..cfdc110 100644
--- a/room/compiler/src/test/data/databasewriter/output/ComplexDatabase.java
+++ b/room/compiler/src/test/data/databasewriter/output/ComplexDatabase.java
@@ -11,6 +11,7 @@
 import android.arch.persistence.room.util.TableInfo;
 import android.arch.persistence.room.util.TableInfo.Column;
 import android.arch.persistence.room.util.TableInfo.ForeignKey;
+import android.arch.persistence.room.util.TableInfo.Index;
 import java.lang.IllegalStateException;
 import java.lang.Override;
 import java.lang.String;
@@ -59,7 +60,8 @@
                 _columnsUser.put("lastName", new TableInfo.Column("lastName", "TEXT", false, 0));
                 _columnsUser.put("ageColumn", new TableInfo.Column("ageColumn", "INTEGER", true, 0));
                 final HashSet<TableInfo.ForeignKey> _foreignKeysUser = new HashSet<TableInfo.ForeignKey>(0);
-                final TableInfo _infoUser = new TableInfo("User", _columnsUser, _foreignKeysUser);
+                final HashSet<TableInfo.Index> _indicesUser = new HashSet<TableInfo.Index>(0);
+                final TableInfo _infoUser = new TableInfo("User", _columnsUser, _foreignKeysUser, _indicesUser);
                 final TableInfo _existingUser = TableInfo.read(_db, "User");
                 if (! _infoUser.equals(_existingUser)) {
                     throw new IllegalStateException("Migration didn't properly handle User(foo.bar.User).\n"
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/migration/MigrationKotlinTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/migration/MigrationKotlinTest.kt
index eb1a9b8..4265e19 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/migration/MigrationKotlinTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/migration/MigrationKotlinTest.kt
@@ -290,6 +290,8 @@
                     + " (`id` INTEGER NOT NULL, `name` TEXT, PRIMARY KEY(`id`),"
                     + " FOREIGN KEY(`name`) REFERENCES `Entity1`(`name`)"
                     + " ON UPDATE NO ACTION ON DELETE NO ACTION DEFERRABLE INITIALLY DEFERRED)")
+            database.execSQL("CREATE UNIQUE INDEX `index_entity1` ON "
+                    + MigrationDbKotlin.Entity1.TABLE_NAME + " (`name`)")
         }
     }
 
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/migration/MigrationTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/migration/MigrationTest.java
index 725d53f..7fe2bc9 100644
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/migration/MigrationTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/migration/MigrationTest.java
@@ -318,6 +318,8 @@
                     + " (`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)");
+            database.execSQL("CREATE UNIQUE INDEX `index_entity1` ON "
+                    + MigrationDb.Entity1.TABLE_NAME + " (`name`)");
         }
     };
 
diff --git a/room/runtime/src/androidTest/java/android/arch/persistence/room/migration/TableInfoTest.java b/room/runtime/src/androidTest/java/android/arch/persistence/room/migration/TableInfoTest.java
index 8b7025b..d88c02f 100644
--- a/room/runtime/src/androidTest/java/android/arch/persistence/room/migration/TableInfoTest.java
+++ b/room/runtime/src/androidTest/java/android/arch/persistence/room/migration/TableInfoTest.java
@@ -37,6 +37,7 @@
 import org.junit.runner.RunWith;
 
 import java.io.IOException;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -179,6 +180,35 @@
                 Collections.<TableInfo.ForeignKey>emptySet())));
     }
 
+    @Test
+    public void readIndices() {
+        mDb = createDatabase(
+                "CREATE TABLE foo (n INTEGER, indexed TEXT, unique_indexed TEXT,"
+                        + "a INTEGER, b INTEGER);",
+                "CREATE INDEX foo_indexed ON foo(indexed);",
+                "CREATE UNIQUE INDEX foo_unique_indexed ON foo(unique_indexed COLLATE NOCASE"
+                        + " DESC);",
+                "CREATE INDEX " + TableInfo.Index.DEFAULT_PREFIX + "foo_composite_indexed"
+                        + " ON foo(a, b);"
+        );
+        TableInfo info = TableInfo.read(mDb, "foo");
+        assertThat(info, is(new TableInfo(
+                "foo",
+                toMap(new TableInfo.Column("n", "INTEGER", false, 0),
+                        new TableInfo.Column("indexed", "TEXT", false, 0),
+                        new TableInfo.Column("unique_indexed", "TEXT", false, 0),
+                        new TableInfo.Column("a", "INTEGER", false, 0),
+                        new TableInfo.Column("b", "INTEGER", false, 0)),
+                Collections.<TableInfo.ForeignKey>emptySet(),
+                toSet(new TableInfo.Index("index_foo_blahblah", false,
+                        Arrays.asList("a", "b")),
+                        new TableInfo.Index("foo_unique_indexed", true,
+                                Arrays.asList("unique_indexed")),
+                        new TableInfo.Index("foo_indexed", false,
+                                Arrays.asList("indexed"))))
+        ));
+    }
+
     private static Map<String, TableInfo.Column> toMap(TableInfo.Column... columns) {
         Map<String, TableInfo.Column> result = new HashMap<>();
         for (TableInfo.Column column : columns) {
@@ -187,6 +217,14 @@
         return result;
     }
 
+    private static <T> Set<T> toSet(T... ts) {
+        final HashSet<T> result = new HashSet<T>();
+        for (T t : ts) {
+            result.add(t);
+        }
+        return result;
+    }
+
     @After
     public void closeDb() throws IOException {
         if (mDb != null && mDb.isOpen()) {
diff --git a/room/runtime/src/main/java/android/arch/persistence/room/util/TableInfo.java b/room/runtime/src/main/java/android/arch/persistence/room/util/TableInfo.java
index bcd2e9e..a115147 100644
--- a/room/runtime/src/main/java/android/arch/persistence/room/util/TableInfo.java
+++ b/room/runtime/src/main/java/android/arch/persistence/room/util/TableInfo.java
@@ -20,6 +20,7 @@
 import android.database.Cursor;
 import android.os.Build;
 import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
 import android.support.annotation.RestrictTo;
 
 import java.util.ArrayList;
@@ -29,6 +30,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.TreeMap;
 
 /**
  * A data class that holds the information about a table.
@@ -56,11 +58,70 @@
 
     public final Set<ForeignKey> foreignKeys;
 
+    /**
+     * Sometimes, Index information is not available (older versions). If so, we skip their
+     * verification.
+     */
+    @Nullable
+    public final Set<Index> indices;
+
     @SuppressWarnings("unused")
-    public TableInfo(String name, Map<String, Column> columns, Set<ForeignKey> foreignKeys) {
+    public TableInfo(String name, Map<String, Column> columns, Set<ForeignKey> foreignKeys,
+            Set<Index> indices) {
         this.name = name;
         this.columns = Collections.unmodifiableMap(columns);
         this.foreignKeys = Collections.unmodifiableSet(foreignKeys);
+        this.indices = indices == null ? null : Collections.unmodifiableSet(indices);
+    }
+
+    /**
+     * For backward compatibility with dbs created with older versions.
+     */
+    @SuppressWarnings("unused")
+    public TableInfo(String name, Map<String, Column> columns, Set<ForeignKey> foreignKeys) {
+        this(name, columns, foreignKeys, Collections.<Index>emptySet());
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        TableInfo tableInfo = (TableInfo) o;
+
+        if (name != null ? !name.equals(tableInfo.name) : tableInfo.name != null) return false;
+        if (columns != null ? !columns.equals(tableInfo.columns) : tableInfo.columns != null) {
+            return false;
+        }
+        if (foreignKeys != null ? !foreignKeys.equals(tableInfo.foreignKeys)
+                : tableInfo.foreignKeys != null) {
+            return false;
+        }
+        if (indices == null || tableInfo.indices == null) {
+            // if one us is missing index information, seems like we couldn't acquire the
+            // information so we better skip.
+            return true;
+        }
+        return indices.equals(tableInfo.indices);
+    }
+
+    @Override
+    public int hashCode() {
+        int result = name != null ? name.hashCode() : 0;
+        result = 31 * result + (columns != null ? columns.hashCode() : 0);
+        result = 31 * result + (foreignKeys != null ? foreignKeys.hashCode() : 0);
+        // skip index, it is not reliable for comparison.
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        return "TableInfo{"
+                + "name='" + name + '\''
+                + ", columns=" + columns
+                + ", foreignKeys=" + foreignKeys
+                + ", indices=" + indices
+                + '}';
     }
 
     /**
@@ -74,7 +135,8 @@
     public static TableInfo read(SupportSQLiteDatabase database, String tableName) {
         Map<String, Column> columns = readColumns(database, tableName);
         Set<ForeignKey> foreignKeys = readForeignKeys(database, tableName);
-        return new TableInfo(tableName, columns, foreignKeys);
+        Set<Index> indices = readIndices(database, tableName);
+        return new TableInfo(tableName, columns, foreignKeys, indices);
     }
 
     private static Set<ForeignKey> readForeignKeys(SupportSQLiteDatabase database,
@@ -167,34 +229,74 @@
         return columns;
     }
 
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-
-        TableInfo tableInfo = (TableInfo) o;
-
-        if (!name.equals(tableInfo.name)) return false;
-        //noinspection SimplifiableIfStatement
-        if (!columns.equals(tableInfo.columns)) return false;
-        return foreignKeys.equals(tableInfo.foreignKeys);
+    /**
+     * @return null if we cannot read the indices due to older sqlite implementations.
+     */
+    @Nullable
+    private static Set<Index> readIndices(SupportSQLiteDatabase database, String tableName) {
+        Cursor cursor = database.query("PRAGMA index_list(`" + tableName + "`)");
+        try {
+            final int nameColumnIndex = cursor.getColumnIndex("name");
+            final int originColumnIndex = cursor.getColumnIndex("origin");
+            final int uniqueIndex = cursor.getColumnIndex("unique");
+            if (nameColumnIndex == -1 || originColumnIndex == -1 || uniqueIndex == -1) {
+                // we cannot read them so better not validate any index.
+                return null;
+            }
+            HashSet<Index> indices = new HashSet<>();
+            while (cursor.moveToNext()) {
+                String origin = cursor.getString(originColumnIndex);
+                if (!"c".equals(origin)) {
+                    // Ignore auto-created indices
+                    continue;
+                }
+                String name = cursor.getString(nameColumnIndex);
+                boolean unique = cursor.getInt(uniqueIndex) == 1;
+                Index index = readIndex(database, name, unique);
+                if (index == null) {
+                    // we cannot read it properly so better not read it
+                    return null;
+                }
+                indices.add(index);
+            }
+            return indices;
+        } finally {
+            cursor.close();
+        }
     }
 
-    @Override
-    public int hashCode() {
-        int result = name.hashCode();
-        result = 31 * result + columns.hashCode();
-        result = 31 * result + foreignKeys.hashCode();
-        return result;
-    }
+    /**
+     * @return null if we cannot read the index due to older sqlite implementations.
+     */
+    @Nullable
+    private static Index readIndex(SupportSQLiteDatabase database, String name, boolean unique) {
+        Cursor cursor = database.query("PRAGMA index_xinfo(`" + name + "`)");
+        try {
+            final int seqnoColumnIndex = cursor.getColumnIndex("seqno");
+            final int cidColumnIndex = cursor.getColumnIndex("cid");
+            final int nameColumnIndex = cursor.getColumnIndex("name");
+            if (seqnoColumnIndex == -1 || cidColumnIndex == -1 || nameColumnIndex == -1) {
+                // we cannot read them so better not validate any index.
+                return null;
+            }
+            final TreeMap<Integer, String> results = new TreeMap<>();
 
-    @Override
-    public String toString() {
-        return "TableInfo{"
-                + "name='" + name + '\''
-                + ", columns=" + columns
-                + ", foreignKeys=" + foreignKeys
-                + '}';
+            while (cursor.moveToNext()) {
+                int cid = cursor.getInt(cidColumnIndex);
+                if (cid < 0) {
+                    // Ignore SQLite row ID
+                    continue;
+                }
+                int seq = cursor.getInt(seqnoColumnIndex);
+                String columnName = cursor.getString(nameColumnIndex);
+                results.put(seq, columnName);
+            }
+            final List<String> columns = new ArrayList<>(results.size());
+            columns.addAll(results.values());
+            return new Index(name, unique, columns);
+        } finally {
+            cursor.close();
+        }
     }
 
     /**
@@ -379,4 +481,65 @@
             }
         }
     }
+
+    /**
+     * Holds the information about an SQLite index
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public static class Index {
+        // should match the value in Index.kt
+        public static final String DEFAULT_PREFIX = "index_";
+        public final String name;
+        public final boolean unique;
+        public final List<String> columns;
+
+        public Index(String name, boolean unique, List<String> columns) {
+            this.name = name;
+            this.unique = unique;
+            this.columns = columns;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+
+            Index index = (Index) o;
+            if (unique != index.unique) {
+                return false;
+            }
+            if (!columns.equals(index.columns)) {
+                return false;
+            }
+            if (name.startsWith(Index.DEFAULT_PREFIX)) {
+                return index.name.startsWith(Index.DEFAULT_PREFIX);
+            } else {
+                return name.equals(index.name);
+            }
+        }
+
+        @Override
+        public int hashCode() {
+            int result;
+            if (name.startsWith(DEFAULT_PREFIX)) {
+                result = DEFAULT_PREFIX.hashCode();
+            } else {
+                result = name.hashCode();
+            }
+            result = 31 * result + (unique ? 1 : 0);
+            result = 31 * result + columns.hashCode();
+            return result;
+        }
+
+        @Override
+        public String toString() {
+            return "Index{"
+                    + "name='" + name + '\''
+                    + ", unique=" + unique
+                    + ", columns=" + columns
+                    + '}';
+        }
+    }
 }
diff --git a/room/testing/src/main/java/android/arch/persistence/room/testing/MigrationTestHelper.java b/room/testing/src/main/java/android/arch/persistence/room/testing/MigrationTestHelper.java
index a27fe0f..18e0a14 100644
--- a/room/testing/src/main/java/android/arch/persistence/room/testing/MigrationTestHelper.java
+++ b/room/testing/src/main/java/android/arch/persistence/room/testing/MigrationTestHelper.java
@@ -29,6 +29,7 @@
 import android.arch.persistence.room.migration.bundle.EntityBundle;
 import android.arch.persistence.room.migration.bundle.FieldBundle;
 import android.arch.persistence.room.migration.bundle.ForeignKeyBundle;
+import android.arch.persistence.room.migration.bundle.IndexBundle;
 import android.arch.persistence.room.migration.bundle.SchemaBundle;
 import android.arch.persistence.room.util.TableInfo;
 import android.content.Context;
@@ -285,7 +286,19 @@
 
     private static TableInfo toTableInfo(EntityBundle entityBundle) {
         return new TableInfo(entityBundle.getTableName(), toColumnMap(entityBundle),
-                toForeignKeys(entityBundle.getForeignKeys()));
+                toForeignKeys(entityBundle.getForeignKeys()), toIndices(entityBundle.getIndices()));
+    }
+
+    private static Set<TableInfo.Index> toIndices(List<IndexBundle> indices) {
+        if (indices == null) {
+            return Collections.emptySet();
+        }
+        Set<TableInfo.Index> result = new HashSet<>();
+        for (IndexBundle bundle : indices) {
+            result.add(new TableInfo.Index(bundle.getName(), bundle.isUnique(),
+                    bundle.getColumnNames()));
+        }
+        return result;
     }
 
     private static Set<TableInfo.ForeignKey> toForeignKeys(