Validate table names in databases

This CL adds 2 verifications:
* If a Dao query accesses a table which is not defined by the database,
we show an error.
* If 2+ entities in a Database has the same table name, we show an
error.

Bug: 32342709
Test: DatabaseProcessorTest.kt
Change-Id: Ib52d02f62f04eeca4fdbb7ac909684efb6ffa6fd
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/parser/ParsedQuery.kt b/room/compiler/src/main/kotlin/com/android/support/room/parser/ParsedQuery.kt
index 820c103..81a2574 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/parser/ParsedQuery.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/parser/ParsedQuery.kt
@@ -35,11 +35,15 @@
     }
 }
 
+data class Table(val name : String, val alias : String)
+
 data class ParsedQuery(val original: String, val inputs: List<TerminalNode>,
+                       // pairs of table name and alias,
+                       val tables: Set<Table>,
                        val syntaxErrors: List<String>) {
     companion object {
         val STARTS_WITH_NUMBER = "^\\?[0-9]".toRegex()
-        val MISSING = ParsedQuery("missing query", emptyList(), emptyList())
+        val MISSING = ParsedQuery("missing query", emptyList(), emptySet(), emptyList())
     }
 
     val sections by lazy {
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/parser/SqlParser.kt b/room/compiler/src/main/kotlin/com/android/support/room/parser/SqlParser.kt
index aff722a..03c3e1d 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/parser/SqlParser.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/parser/SqlParser.kt
@@ -26,8 +26,10 @@
 
 class BindingExtractor(val original: String) : SQLiteBaseVisitor<Void?>() {
     val bindingExpressions = arrayListOf<TerminalNode>()
-    override fun visitExpr(ctx: SQLiteParser.ExprContext?): Void? {
-        val bindParameter = ctx!!.BIND_PARAMETER()
+    // table name alias mappings
+    val tableNames = mutableSetOf<Table>()
+    override fun visitExpr(ctx: SQLiteParser.ExprContext): Void? {
+        val bindParameter = ctx.BIND_PARAMETER()
         if (bindParameter != null) {
             bindingExpressions.add(bindParameter)
         }
@@ -36,7 +38,18 @@
 
     fun createParsedQuery(syntaxErrors: ArrayList<String>): ParsedQuery {
         return ParsedQuery(original,
-                bindingExpressions.sortedBy { it.sourceInterval.a }, syntaxErrors)
+                bindingExpressions.sortedBy { it.sourceInterval.a },
+                tableNames,
+                syntaxErrors)
+    }
+
+    override fun visitTable_or_subquery(ctx: SQLiteParser.Table_or_subqueryContext): Void? {
+        val tableName = ctx.table_name()?.text
+        if (tableName != null) {
+            val tableAlias = ctx.table_alias()?.text
+            tableNames.add(Table(tableName, tableAlias ?: tableName))
+        }
+        return super.visitTable_or_subquery(ctx)
     }
 }
 
@@ -56,7 +69,8 @@
                 }
             })
             val extractor = BindingExtractor(input)
-            parser.select_stmt().accept(extractor)
+            val selectStmt = parser.select_stmt()
+            selectStmt.accept(extractor)
             return extractor.createParsedQuery(syntaxErrors)
         }
     }
@@ -68,4 +82,4 @@
     INTEGER,
     REAL,
     BLOB
-}
\ No newline at end of file
+}
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/processor/DatabaseProcessor.kt b/room/compiler/src/main/kotlin/com/android/support/room/processor/DatabaseProcessor.kt
index 829a987..6aa8f5f 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/processor/DatabaseProcessor.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/processor/DatabaseProcessor.kt
@@ -38,7 +38,7 @@
     val entityParser = EntityProcessor(context)
     val daoParser = DaoProcessor(context)
 
-    val baseClassElement : TypeMirror by lazy {
+    val baseClassElement: TypeMirror by lazy {
         context.processingEnv.elementUtils.getTypeElement(
                 RoomTypeNames.ROOM_DB.packageName() + "." + RoomTypeNames.ROOM_DB.simpleName())
                 .asType()
@@ -70,13 +70,53 @@
             DaoMethod(executable, executable.simpleName.toString(), dao)
         }
 
-        return Database(element = element,
+        val database = Database(element = element,
                 type = MoreElements.asType(element).asType(),
                 entities = entities,
                 daoMethods = daoMethods)
+        validateAccessedTables(database)
+        validateUniqueTableNames(database)
+        return database
     }
 
-    private fun processEntities(dbAnnotation: AnnotationMirror?, element: TypeElement) :
+    private fun validateUniqueTableNames(database: Database) {
+        database.entities
+                .groupBy {
+                    it.tableName.toLowerCase()
+                }.filter {
+            it.value.size > 1
+        }.forEach { byTableName ->
+            val error = ProcessorErrors.duplicateTableNames(byTableName.key,
+                    byTableName.value.map { it.typeName.toString() })
+            // report it for each of them and the database to make it easier
+            // for the developer
+            byTableName.value.forEach { entity ->
+                context.logger.e(entity.element, error)
+            }
+            context.logger.e(database.element, error)
+        }
+    }
+
+    private fun validateAccessedTables(database: Database) {
+        val definedTables = database.entities.mapTo(mutableSetOf()) { it.tableName.toLowerCase() }
+        database.daoMethods.forEach { daoMethod ->
+            daoMethod.dao.queryMethods.forEach { queryMethod ->
+                queryMethod.query.tables
+                        .filterNot {
+                            definedTables.contains(it.name.toLowerCase())
+                        }.forEach { table ->
+                    context.logger.e(queryMethod.element,
+                            ProcessorErrors.missingTable(
+                                    tableName = table.name,
+                                    daoName = daoMethod.dao.typeName.toString(),
+                                    methodName = queryMethod.name,
+                                    databaseName = database.typeName.toString()))
+                }
+            }
+        }
+    }
+
+    private fun processEntities(dbAnnotation: AnnotationMirror?, element: TypeElement):
             List<Entity> {
         if (!context.checker.check(dbAnnotation != null, element,
                 ProcessorErrors.DATABASE_MUST_BE_ANNOTATED_WITH_DATABASE)) {
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/processor/EntityProcessor.kt b/room/compiler/src/main/kotlin/com/android/support/room/processor/EntityProcessor.kt
index 368778b..9c45e6d 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/processor/EntityProcessor.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/processor/EntityProcessor.kt
@@ -86,7 +86,7 @@
         }
         context.checker.notBlank(tableName, element,
                 ProcessorErrors.ENTITY_TABLE_NAME_CANNOT_BE_EMPTY)
-        val entity = Entity(tableName, declaredType, fields)
+        val entity = Entity(element, tableName, declaredType, fields)
         context.checker.check(entity.primaryKeys.isNotEmpty(), element,
                 ProcessorErrors.MISSING_PRIMARY_KEY)
         return entity
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/processor/ProcessorErrors.kt b/room/compiler/src/main/kotlin/com/android/support/room/processor/ProcessorErrors.kt
index 47253bb..718d4d4 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/processor/ProcessorErrors.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/processor/ProcessorErrors.kt
@@ -105,4 +105,16 @@
                 if (unusedParams.size > 1) "s" else "",
                 unusedParams.joinToString(","))
     }
+
+    private val MISSING_TABLE = "Table \"%s\" is accessed in %s#%s but %s does not have any" +
+            " entity that declares the table \"%s\""
+    fun missingTable(tableName: String, daoName: String, methodName: String,
+                     databaseName : String): String {
+        return MISSING_TABLE.format(tableName, daoName, methodName, databaseName, tableName)
+    }
+
+    private val DUPLICATE_TABLES = "Table name \"%s\" is used by multiple entities: %s"
+    fun  duplicateTableNames(tableName: String, entityNames: List<String>): String {
+        return DUPLICATE_TABLES.format(tableName, entityNames.joinToString(", "))
+    }
 }
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/vo/Entity.kt b/room/compiler/src/main/kotlin/com/android/support/room/vo/Entity.kt
index 3de1dbe..0fade53 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/vo/Entity.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/vo/Entity.kt
@@ -18,9 +18,12 @@
 
 import com.android.support.room.ext.typeName
 import com.squareup.javapoet.ClassName
+import javax.lang.model.element.Element
+import javax.lang.model.element.TypeElement
 import javax.lang.model.type.DeclaredType
 
-data class Entity(val tableName : String, val type: DeclaredType, val fields : List<Field>) {
+data class Entity(val element : TypeElement, val tableName : String, val type: DeclaredType,
+                  val fields : List<Field>) {
     val converterClassName by lazy {
         val typeName = this.typeName
         if (typeName is ClassName) {
diff --git a/room/compiler/src/test/kotlin/com/android/support/room/parser/SqlParserTest.kt b/room/compiler/src/test/kotlin/com/android/support/room/parser/SqlParserTest.kt
index deb365f..c6dcd7e 100644
--- a/room/compiler/src/test/kotlin/com/android/support/room/parser/SqlParserTest.kt
+++ b/room/compiler/src/test/kotlin/com/android/support/room/parser/SqlParserTest.kt
@@ -24,6 +24,17 @@
 
 @RunWith(JUnit4::class)
 class SqlParserTest {
+    @Test
+    fun extractTableNames() {
+        assertThat(SqlParser.parse("select * from users").tables,
+                `is`(setOf(Table("users", "users"))))
+        assertThat(SqlParser.parse("select * from users as ux").tables,
+                `is`(setOf(Table("users", "ux"))))
+        assertThat(SqlParser.parse("select * from (select * from books)").tables,
+                `is`(setOf(Table("books", "books"))))
+        assertThat(SqlParser.parse("select x.id from (select * from books) as x").tables,
+                `is`(setOf(Table("books", "books"))))
+    }
 
     @Test
     fun findBindVariables() {
@@ -94,4 +105,4 @@
     fun assertSections(query: String, vararg sections: Section) {
         assertThat(SqlParser.parse(query).sections, `is`(sections.toList()))
     }
-}
\ No newline at end of file
+}
diff --git a/room/compiler/src/test/kotlin/com/android/support/room/processor/DatabaseProcessorTest.kt b/room/compiler/src/test/kotlin/com/android/support/room/processor/DatabaseProcessorTest.kt
index 89f378a..0b4d148 100644
--- a/room/compiler/src/test/kotlin/com/android/support/room/processor/DatabaseProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/com/android/support/room/processor/DatabaseProcessorTest.kt
@@ -74,7 +74,7 @@
                 import com.android.support.room.*;
                 @Dao
                 public interface BookDao {
-                    @Query("SELECT * FROM books")
+                    @Query("SELECT * FROM book")
                     public java.util.List<Book> loadAllBooks();
                 }
                 """)
@@ -126,6 +126,60 @@
         }.failsToCompile().withErrorContaining(ProcessorErrors.DB_MUST_EXTEND_ROOM_DB)
     }
 
+    @Test
+    fun detectMissingTable() {
+        singleDb(
+                """
+                @Database(entities = {Book.class})
+                public abstract class MyDb extends RoomDatabase {
+                    public MyDb(DatabaseConfiguration config) {
+                        super(config);
+                    }
+                    abstract BookDao bookDao();
+                }
+                """, BOOK, JavaFileObjects.forSourceString("foo.bar.BookDao",
+                """
+                package foo.bar;
+                import com.android.support.room.*;
+                @Dao
+                public interface BookDao {
+                    @Query("SELECT * FROM nonExistentTable")
+                    public java.util.List<Book> loadAllBooks();
+                }
+                """)){ db, invocation ->
+
+        }.failsToCompile().withErrorContaining(
+                ProcessorErrors.missingTable("nonExistentTable", "foo.bar.BookDao", "loadAllBooks",
+                        "foo.bar.MyDb")
+        )
+    }
+
+    @Test
+    fun detectDuplicateTableNames() {
+        singleDb("""
+                @Database(entities = {User.class, AnotherClass.class})
+                public abstract class MyDb extends RoomDatabase {
+                    public MyDb(DatabaseConfiguration config) {
+                        super(config);
+                    }
+                    abstract UserDao userDao();
+                }
+                """, USER, USER_DAO, JavaFileObjects.forSourceString("foo.bar.AnotherClass",
+                """
+                package foo.bar;
+                import com.android.support.room.*;
+                @Entity(tableName="user")
+                public class AnotherClass {
+                    @PrimaryKey
+                    int uid;
+                }
+                """)) { db, invocation ->
+        }.failsToCompile().withErrorContaining(
+                ProcessorErrors.duplicateTableNames("user",
+                        listOf("foo.bar.User", "foo.bar.AnotherClass"))
+        )
+    }
+
     fun singleDb(input: String, vararg otherFiles: JavaFileObject,
                  handler: (Database, TestInvocation) -> Unit): CompileTester {
         return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())