Prerequisites:
For every token in a Room query that is defined as a column (column_name/AndroidSqlColumnName) in grammar, we create a AndroidSqlColumnPsiReference. AndroidSqlColumnPsiReference
can be one of two types: UnqualifiedColumnPsiReference
or QualifiedColumnPsiReference
. It depends on whether we know the table a given column belongs to. If we use column in query like this: TableName.columnName
we say that we know the table and use QualifiedColumnPsiReference
.
For tables there are two types of references: AndroidSqlSelectedTablePsiReference
and AndroidSqlDefinedTablePsiReference
.
AndroidSqlSelectedTablePsiReference
for table that is already in scope for the current query. In practice it means for table name inside column reference i.e. selected_table_name ‘.’ column_name.
AndroidSqlDefinedTablePsiReference
for table defined in the schema or using a WITH clause. It's used everywhere where we refer to table except ‘full’ columns names.
In order to resolve AndroidSqlColumnPsiReference
we need to find a SqlColumn
that corresponds to a given reference. This logic is inside resolveColumn
method.
For QualifiedColumnPsiReference
it's pretty simple. We know tableName
and try to find a SqlTable
that corresponds to it. If we found the SqlTable
we just call SqlTable.processColumn(Processor)
.
For UnqualifiedColumnPsiReference
we need to find all possible sources (see SqlTable/ Column sources) of columns in given query. And do SqlTable.processColumn(Processor)
for each of them.
We have a Room Entity kotlin @Entity class User { val age:String }
We have a RoomQuery "SELECT age FROM User"
and by clicking on age
or User
we want to find defining element for it (actual field or Class). The process of finding that element calls resolution process. All classes that help to do it are in ./resolution directory.
Let's look into example for age
field. Steps of parsing text file and creating PsiElements/Reference are described here and in Intellij SDK Documentation
Start with the step when we already have AndroidSqlColumnPsiReference
for age
field. In this example we have UnqualifiedColumnPsiReference
because in the query from language perspective we didn't specify the source table like User.age
.
UnqualifiedColumnPsiReference.resolve()
should return PsiElement that corresponds to age
field in User
class. In order to do that we try to find SqlColumn
with name age
in one of SqlTable
s in the given query. In our simple example it will be just User
table. Our next step is to obtain from the query all SqlTable
s, and call SqlTable.processColumn
on each of them. That is exactly what processSelectedSqlTables
method in Resolution.kt does.
Resolution process for UnqualifiedColumnPsiReference
:
processSelectedSqlTables
we traverse PSI tree in DFS stylePsiElement
that implements AndroidSqlTableElement
we get SqlTable
. Note that in general obtaining SqlTable
from AndroidSqlTableElement
runs another resolution process.SqlTable
we run SqlTable.processColumn
that processes every column in table. Be aware that sometimes (e.g for SubqueryTable) processColumn
starts another resolution process.SqlTable.processColumn
returns false, or we traverse all the PSI tree.SqlTable
is a class that represents the source of columns, the key functionality of SqlTable
is that it can feed its column to the processor. Also SqlTable
has a link to the defining element e.g Room Entity, SELECT statement, AndroidSqlFakePsiElement
.
AndroidSqlTableElement
is a PsiElement that is created during parsing of a SQL query. AndroidSqlTableElement.sqlTable
contains link to corresponding SqlTable
. For different subclasses of AndroidSqlTableElement
there are different implementations of AndroidSqlTableElement.sqlTable
see PsiImplUtil.getSqlTable. In most cases AndroidSqlTableElement.sqlTable
runs resolution process for the table, see resolve
method in AndroidSqlDefinedTablePsiReference
and AndroidSqlSelectedTablePsiReference
.
During the resolution process, we need to traverse all valid sources for a certain column. Everything that can be source of column implements SqlTable
interface. Examples:
SELECT * FROM (SELECT column1, column2 FROM MyTable)
inner SELECT creates SubqueryTable
for toplevel SELECT.SELECT id as aliasId, name as aliasName FROM User
part id as aliasId, name as aliasName
creates AliasColumnTable
with two columns aliasId and aliasName.SqliteSchema
see SqliteTable.convertToSqlTable in SqliteSchemaContext
.If SqlTable
is not defined in a query (e.g. WITH close, AliasColumnTable) we find it by calling processTables
on a known AndroidSqlContext. You can provide AndroidSqlContext
for PsiFile
that contains sql query through com.android.tools.idea.lang.androidSql.contextProvider extension point.
RoomSchema
is the schema of the database defined in Java/Kotlin classes. We build and store RoomSchema
per module in RoomSchemaManager
. Every module can contain 3 schemas depends on visibility scope:
For more about scopes see TestArtifactSearchScopes
.
If Room is present in a module we traverse all files in current scope and build RoomTables
(implementation of SqlTable) with RoomColumns
. In most cases RoomColumn
is defined by class field, but in order to support rowid when a user hasn't specified integer primary key we have PsiElementForFakeColumn
.
SqlTable.processColumns takes sqlTablesInProcess: MutableSet<PsiElement>
as the second argument. We need this set to avoid infinite recursion. In query Columns of A SqlTable can be defined through B SqlTable and at the same time Columns of B SqlTable can be defined through A SqlTable. When we try to process all columns of A we can end up in infinite recursion. Just like in simple DFS we store table that currently in process to avoid it.
Example of valid recursive query:
WITH recTable AS (SELECT 1 AS level UNION ALL SELECT level + 1 FROM recTable WHERE level < 10) SELECT level FROM recTable
Example of invalid recursive query:
WITH t1 AS (SELECT * FROM t2), t2 AS (select * from t1 WHERE <caret>) SELECT * FROM t1"