Generate column index for queries whose column result order is known.

For queries without a top-level star projection the column result set is deterministic and therefore Room can generated the column index for a field by using the column result set of a JDBC analyzed query.

For optimizes queries via '@RewriteQueriesToDropUnusedColumns' the column index is also determined at compile-time even if the original query has a start projection, since the optimization rewrites the query with a known column order.

Bug: 160714163
Test: :room:integration-tests:room-testapp:cAT
Change-Id: I22a130ef7fa4979191c7f6cba878e34f4ebb1c17
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/parser/ParsedQuery.kt b/room/room-compiler/src/main/kotlin/androidx/room/parser/ParsedQuery.kt
index 565eed0..7aac0ef 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/parser/ParsedQuery.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/parser/ParsedQuery.kt
@@ -50,11 +50,13 @@
 }
 
 data class Table(val name: String, val alias: String)
+
 data class ParsedQuery(
     val original: String,
     val type: QueryType,
     val inputs: List<BindParameterNode>,
     val tables: Set<Table>, // pairs of table name and alias
+    val hasTopStarProjection: Boolean?, // null means unknown
     val syntaxErrors: List<String>
 ) {
     companion object {
@@ -64,6 +66,7 @@
             type = QueryType.UNKNOWN,
             inputs = emptyList(),
             tables = emptySet(),
+            hasTopStarProjection = null,
             syntaxErrors = emptyList()
         )
     }
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/parser/SqlParser.kt b/room/room-compiler/src/main/kotlin/androidx/room/parser/SqlParser.kt
index 5241975..b398f12 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/parser/SqlParser.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/parser/SqlParser.kt
@@ -20,6 +20,7 @@
 import androidx.room.compiler.processing.XProcessingEnv
 import androidx.room.compiler.processing.XType
 import androidx.room.ext.CommonTypeNames
+import androidx.room.parser.expansion.isCoreSelect
 import com.squareup.javapoet.ArrayTypeName
 import com.squareup.javapoet.TypeName
 import org.antlr.v4.runtime.tree.ParseTree
@@ -30,14 +31,14 @@
 class QueryVisitor(
     private val original: String,
     private val syntaxErrors: List<String>,
-    statement: ParseTree,
-    private val forRuntimeQuery: Boolean
+    statement: ParseTree
 ) : SQLiteBaseVisitor<Void?>() {
     private val bindingExpressions = arrayListOf<BindParameterNode>()
     // table name alias mappings
     private val tableNames = mutableSetOf<Table>()
     private val withClauseNames = mutableSetOf<String>()
     private val queryType: QueryType
+    private var foundTopLevelStarProjection: Boolean = false
 
     init {
         queryType = (0 until statement.childCount).map {
@@ -82,6 +83,13 @@
         return super.visitExpr(ctx)
     }
 
+    override fun visitResult_column(ctx: SQLiteParser.Result_columnContext): Void? {
+        if (ctx.parent.isCoreSelect && ctx.text == "*") {
+            foundTopLevelStarProjection = true
+        }
+        return super.visitResult_column(ctx)
+    }
+
     /**
      * Check if a comma separated expression (where multiple binding parameters are accepted) is
      * part of a function expression that receives a fixed number of parameters. This is
@@ -107,6 +115,8 @@
             type = queryType,
             inputs = bindingExpressions.sortedBy { it.sourceInterval.a },
             tables = tableNames,
+            hasTopStarProjection =
+                if (queryType == QueryType.SELECT) foundTopLevelStarProjection else null,
             syntaxErrors = syntaxErrors,
         )
     }
@@ -197,8 +207,7 @@
                 QueryVisitor(
                     original = input,
                     syntaxErrors = syntaxErrors,
-                    statement = statement,
-                    forRuntimeQuery = false
+                    statement = statement
                 ).createParsedQuery()
             },
             fallback = { syntaxErrors ->
@@ -207,6 +216,7 @@
                     type = QueryType.UNKNOWN,
                     inputs = emptyList(),
                     tables = emptySet(),
+                    hasTopStarProjection = null,
                     syntaxErrors = syntaxErrors,
                 )
             }
@@ -224,6 +234,7 @@
                 type = QueryType.UNKNOWN,
                 inputs = emptyList(),
                 tables = tableNames.map { Table(name = it, alias = it) }.toSet(),
+                hasTopStarProjection = null,
                 syntaxErrors = emptyList(),
             )
         }
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/parser/expansion/ExpandableSqlParser.kt b/room/room-compiler/src/main/kotlin/androidx/room/parser/expansion/ExpandableSqlParser.kt
index 6e5b317..25dc722 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/parser/expansion/ExpandableSqlParser.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/parser/expansion/ExpandableSqlParser.kt
@@ -195,7 +195,7 @@
 /**
  * Whether this [RuleContext] is the top SELECT statement.
  */
-private val RuleContext.isCoreSelect: Boolean
+internal val RuleContext.isCoreSelect: Boolean
     get() {
         return this is SQLiteParser.Select_or_valuesContext &&
             ancestors().none { it is SQLiteParser.Select_or_valuesContext }
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt b/room/room-compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
index 9da7794..5af3062 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
@@ -36,6 +36,8 @@
         return this.trimIndent().replace("\n", " ")
     }
 
+    val ISSUE_TRACKER_LINK = "https://issuetracker.google.com/issues/new?component=413107"
+
     val MISSING_QUERY_ANNOTATION = "Query methods must be annotated with ${Query::class.java}"
     val MISSING_INSERT_ANNOTATION = "Insertion methods must be annotated with ${Insert::class.java}"
     val MISSING_DELETE_ANNOTATION = "Deletion methods must be annotated with ${Delete::class.java}"
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
index 7af6697..937ec76 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
@@ -681,6 +681,7 @@
                     PojoRowAdapter(
                         context = subContext,
                         info = resultInfo,
+                        query = query,
                         pojo = pojo,
                         out = typeMirror
                     )
@@ -749,6 +750,7 @@
                 return PojoRowAdapter(
                     context = context,
                     info = null,
+                    query = query,
                     pojo = pojo,
                     out = typeMirror
                 )
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/PojoRowAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/PojoRowAdapter.kt
index 03bd06d..5ca6a30 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/PojoRowAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/PojoRowAdapter.kt
@@ -23,8 +23,10 @@
 import androidx.room.ext.T
 import androidx.room.ext.capitalize
 import androidx.room.ext.stripNonJava
+import androidx.room.parser.ParsedQuery
 import androidx.room.processor.Context
 import androidx.room.processor.ProcessorErrors
+import androidx.room.processor.ProcessorErrors.ISSUE_TRACKER_LINK
 import androidx.room.solver.CodeGenScope
 import androidx.room.verifier.QueryResultInfo
 import androidx.room.vo.Field
@@ -44,6 +46,7 @@
 class PojoRowAdapter(
     context: Context,
     private val info: QueryResultInfo?,
+    private val query: ParsedQuery?,
     val pojo: Pojo,
     out: XType
 ) : RowAdapter(out), QueryMappedRowAdapter {
@@ -116,16 +119,34 @@
             val indexVar = scope.getTmpVar(
                 "_cursorIndexOf${it.name.stripNonJava().capitalize(Locale.US)}"
             )
-            val indexMethod = if (info == null) {
-                "getColumnIndex"
+            if (info != null && query != null && query.hasTopStarProjection == false) {
+                // When result info is available and query does not have a top-level star
+                // projection we can generate column to field index since the column result order
+                // is deterministic.
+                val infoIndex = info.columns.indexOfFirst { columnInfo ->
+                    columnInfo.name == it.columnName
+                }
+                check(infoIndex != -1) {
+                    "Result column index not found for field '$it' with column name " +
+                        "'${it.columnName}'. Query: ${query.original}. Please file a bug at " +
+                        ISSUE_TRACKER_LINK
+                }
+                scope.builder().addStatement(
+                    "final $T $L = $L",
+                    TypeName.INT, indexVar, infoIndex
+                )
             } else {
-                "getColumnIndexOrThrow"
+                val indexMethod = if (info == null) {
+                    "getColumnIndex"
+                } else {
+                    "getColumnIndexOrThrow"
+                }
+                scope.builder().addStatement(
+                    "final $T $L = $T.$L($L, $S)",
+                    TypeName.INT, indexVar, RoomTypeNames.CURSOR_UTIL, indexMethod, cursorVarName,
+                    it.columnName
+                )
             }
-            scope.builder().addStatement(
-                "final $T $L = $T.$L($L, $S)",
-                TypeName.INT, indexVar, RoomTypeNames.CURSOR_UTIL, indexMethod, cursorVarName,
-                it.columnName
-            )
             FieldWithIndex(field = it, indexVar = indexVar, alwaysExists = info != null)
         }
         if (relationCollectors.isNotEmpty()) {
diff --git a/room/room-compiler/src/test/data/daoWriter/input/ComplexDao.java b/room/room-compiler/src/test/data/daoWriter/input/ComplexDao.java
index c3a4f55..a5e3510 100644
--- a/room/room-compiler/src/test/data/daoWriter/input/ComplexDao.java
+++ b/room/room-compiler/src/test/data/daoWriter/input/ComplexDao.java
@@ -77,4 +77,8 @@
 
     @Query("SELECT * FROM Child1")
     abstract public ListenableFuture<List<Child1>> getChild1ListListenableFuture();
+
+    @RewriteQueriesToDropUnusedColumns
+    @Query("SELECT * FROM User")
+    abstract public List<UserSummary> getUserNames();
 }
diff --git a/room/room-compiler/src/test/data/daoWriter/output/ComplexDao.java b/room/room-compiler/src/test/data/daoWriter/output/ComplexDao.java
index efbd70c..dd5fa93 100644
--- a/room/room-compiler/src/test/data/daoWriter/output/ComplexDao.java
+++ b/room/room-compiler/src/test/data/daoWriter/output/ComplexDao.java
@@ -54,8 +54,8 @@
         __db.assertNotSuspendingTransaction();
         final Cursor _cursor = DBUtil.query(__db, _statement, false, null);
         try {
-            final int _cursorIndexOfFullName = CursorUtil.getColumnIndexOrThrow(_cursor, "fullName");
-            final int _cursorIndexOfId = CursorUtil.getColumnIndexOrThrow(_cursor, "id");
+            final int _cursorIndexOfFullName = 0;
+            final int _cursorIndexOfId = 1;
             final List<ComplexDao.FullName> _result = new ArrayList<ComplexDao.FullName>(_cursor.getCount());
             while(_cursor.moveToNext()) {
                 final ComplexDao.FullName _item;
@@ -605,6 +605,34 @@
         }, _statement, true, _cancellationSignal);
     }
 
+    @Override
+    public List<UserSummary> getUserNames() {
+        final String _sql = "SELECT `uid`, `name` FROM (SELECT * FROM User)";
+        final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 0);
+        __db.assertNotSuspendingTransaction();
+        final Cursor _cursor = DBUtil.query(__db, _statement, false, null);
+        try {
+            final int _cursorIndexOfUid = 0;
+            final int _cursorIndexOfName = 1;
+            final List<UserSummary> _result = new ArrayList<UserSummary>(_cursor.getCount());
+            while(_cursor.moveToNext()) {
+                final UserSummary _item;
+                _item = new UserSummary();
+                _item.uid = _cursor.getInt(_cursorIndexOfUid);
+                if (_cursor.isNull(_cursorIndexOfName)) {
+                    _item.name = null;
+                } else {
+                    _item.name = _cursor.getString(_cursorIndexOfName);
+                }
+                _result.add(_item);
+            }
+            return _result;
+        } finally {
+            _cursor.close();
+            _statement.release();
+        }
+    }
+
     public static List<Class<?>> getRequiredConverters() {
         return Collections.emptyList();
     }
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/DatabaseProcessorTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/DatabaseProcessorTest.kt
index 8721f92..579851f 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/DatabaseProcessorTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/DatabaseProcessorTest.kt
@@ -1461,7 +1461,7 @@
                     viewName = viewName,
                     query = ParsedQuery(
                         "", QueryType.SELECT, emptyList(),
-                        names.map { Table(it, it) }.toSet(),
+                        names.map { Table(it, it) }.toSet(), null,
                         emptyList()
                     ),
                     type = mock(XType::class.java),
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoWriterTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoWriterTest.kt
index 101138c..fceff5a 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoWriterTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoWriterTest.kt
@@ -150,11 +150,13 @@
                     ?: invocation.context.processingEnv
                         .requireTypeElement(RoomTypeNames.ROOM_DB)
                 val dbType = db.type
+                val dbVerifier = createVerifierFromEntitiesAndViews(invocation)
+                invocation.context.attachDatabaseVerifier(dbVerifier)
                 val parser = DaoProcessor(
                     baseContext = invocation.context,
                     element = dao,
                     dbType = dbType,
-                    dbVerifier = createVerifierFromEntitiesAndViews(invocation)
+                    dbVerifier = dbVerifier
                 )
                 val parsedDao = parser.process()
                 DaoWriter(parsedDao, db, invocation.processingEnv)