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())