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)