1 /*
<lambda>null2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.room.processor
18 
19 import androidx.room.Query
20 import androidx.room.SkipQueryVerification
21 import androidx.room.Transaction
22 import androidx.room.compiler.processing.XAnnotation
23 import androidx.room.compiler.processing.XMethodElement
24 import androidx.room.compiler.processing.XType
25 import androidx.room.ext.isNotError
26 import androidx.room.parser.ParsedQuery
27 import androidx.room.parser.QueryType
28 import androidx.room.parser.SqlParser
29 import androidx.room.processor.ProcessorErrors.cannotMapSpecifiedColumn
30 import androidx.room.solver.TypeAdapterExtras
31 import androidx.room.solver.query.result.DataClassRowAdapter
32 import androidx.room.verifier.ColumnInfo
33 import androidx.room.verifier.DatabaseVerificationErrors
34 import androidx.room.verifier.DatabaseVerifier
35 import androidx.room.vo.MapInfo
36 import androidx.room.vo.QueryFunction
37 import androidx.room.vo.QueryParameter
38 import androidx.room.vo.ReadQueryFunction
39 import androidx.room.vo.Warning
40 import androidx.room.vo.WriteQueryFunction
41 
42 class QueryFunctionProcessor(
43     baseContext: Context,
44     val containing: XType,
45     val executableElement: XMethodElement,
46     val dbVerifier: DatabaseVerifier? = null
47 ) {
48     val context = baseContext.fork(executableElement)
49 
50     /**
51      * The processing of the function might happen in multiple steps if we decide to rewrite the
52      * query after the first processing. To allow it while respecting the Context, it is implemented
53      * as a sub procedure in [InternalQueryProcessor].
54      */
55     fun process(): QueryFunction {
56         val annotation = executableElement.getAnnotation(Query::class)
57         context.checker.check(
58             annotation != null,
59             executableElement,
60             ProcessorErrors.MISSING_QUERY_ANNOTATION
61         )
62 
63         /**
64          * Run the first process without reporting any errors / warnings as we might be able to fix
65          * them for the developer.
66          */
67         val (initialResult, logs) =
68             context.collectLogs {
69                 InternalQueryProcessor(
70                         context = it,
71                         executableElement = executableElement,
72                         dbVerifier = dbVerifier,
73                         containing = containing
74                     )
75                     .processQuery(annotation?.getAsString("value"))
76             }
77         // check if want to swap the query for a better one
78         val finalResult =
79             if (initialResult is ReadQueryFunction) {
80                 val resultAdapter = initialResult.queryResultBinder.adapter
81                 val originalQuery = initialResult.query
82                 val finalQuery =
83                     resultAdapter?.let {
84                         context.queryRewriter.rewrite(originalQuery, resultAdapter)
85                     } ?: originalQuery
86                 if (finalQuery != originalQuery) {
87                     // ok parse again
88                     return InternalQueryProcessor(
89                             context = context,
90                             executableElement = executableElement,
91                             dbVerifier = dbVerifier,
92                             containing = containing
93                         )
94                         .processQuery(finalQuery.original)
95                 } else {
96                     initialResult
97                 }
98             } else {
99                 initialResult
100             }
101         if (finalResult == initialResult) {
102             // if we didn't rewrite it, send all logs to the calling context.
103             logs.writeTo(context)
104         }
105         return finalResult
106     }
107 }
108 
109 private class InternalQueryProcessor(
110     val context: Context,
111     val executableElement: XMethodElement,
112     val containing: XType,
113     val dbVerifier: DatabaseVerifier? = null
114 ) {
processQuerynull115     fun processQuery(input: String?): QueryFunction {
116         val delegate = FunctionProcessorDelegate.createFor(context, containing, executableElement)
117         val returnType = delegate.extractReturnType()
118 
119         val returnsDeferredType = delegate.returnsDeferredType()
120         val isSuspendFunction = delegate.executableElement.isSuspendFunction()
121         context.checker.check(
122             !isSuspendFunction || !returnsDeferredType,
123             executableElement,
124             ProcessorErrors.suspendReturnsDeferredType(returnType.rawType.typeName.toString())
125         )
126 
127         val query =
128             if (!isSuspendFunction && !returnsDeferredType && !context.isAndroidOnlyTarget()) {
129                 // A blocking function that does not return a deferred return type is not allowed
130                 // if the target platforms include non-Android targets.
131                 context.logger.e(
132                     executableElement,
133                     ProcessorErrors.INVALID_BLOCKING_DAO_FUNCTION_NON_ANDROID
134                 )
135                 // Early return so we don't generate redundant code.
136                 ParsedQuery.MISSING
137             } else if (input != null) {
138                 val query = SqlParser.parse(input)
139                 context.checker.check(
140                     query.errors.isEmpty(),
141                     executableElement,
142                     query.errors.joinToString("\n")
143                 )
144                 validateQuery(query)
145                 context.checker.check(
146                     returnType.isNotError(),
147                     executableElement,
148                     ProcessorErrors.CANNOT_RESOLVE_RETURN_TYPE,
149                     executableElement
150                 )
151                 query
152             } else {
153                 ParsedQuery.MISSING
154             }
155 
156         context.checker.notUnbound(
157             returnType,
158             executableElement,
159             ProcessorErrors.CANNOT_USE_UNBOUND_GENERICS_IN_QUERY_FUNCTIONS
160         )
161 
162         val isPreparedQuery = PREPARED_TYPES.contains(query.type)
163         val queryFunction =
164             if (isPreparedQuery) {
165                 getPreparedQueryFunction(delegate, returnType, query)
166             } else {
167                 getQueryFunction(delegate, returnType, query)
168             }
169 
170         return processQueryFunction(queryFunction)
171     }
172 
processQueryFunctionnull173     private fun processQueryFunction(queryFunction: QueryFunction): QueryFunction {
174         val missing =
175             queryFunction.sectionToParamMapping.filter { it.second == null }.map { it.first.text }
176         if (missing.isNotEmpty()) {
177             context.logger.e(
178                 executableElement,
179                 ProcessorErrors.missingParameterForBindVariable(missing)
180             )
181         }
182 
183         val unused =
184             queryFunction.parameters
185                 .filterNot { param ->
186                     queryFunction.sectionToParamMapping.any { it.second == param }
187                 }
188                 .map(QueryParameter::sqlName)
189 
190         if (unused.isNotEmpty()) {
191             context.logger.e(
192                 executableElement,
193                 ProcessorErrors.unusedQueryFunctionParameter(unused)
194             )
195         }
196         return queryFunction
197     }
198 
validateQuerynull199     private fun validateQuery(query: ParsedQuery) {
200         val skipQueryVerification = executableElement.hasAnnotation(SkipQueryVerification::class)
201         if (skipQueryVerification) {
202             return
203         }
204         query.resultInfo = dbVerifier?.analyze(query.original)
205         if (query.resultInfo?.error != null) {
206             context.logger.e(
207                 executableElement,
208                 DatabaseVerificationErrors.cannotVerifyQuery(query.resultInfo!!.error!!)
209             )
210         }
211     }
212 
getPreparedQueryFunctionnull213     private fun getPreparedQueryFunction(
214         delegate: FunctionProcessorDelegate,
215         returnType: XType,
216         query: ParsedQuery
217     ): WriteQueryFunction {
218         val resultBinder = delegate.findPreparedResultBinder(returnType, query)
219         context.checker.check(
220             resultBinder.adapter != null,
221             executableElement,
222             ProcessorErrors.cannotFindPreparedQueryResultAdapter(
223                 returnType.asTypeName().toString(context.codeLanguage),
224                 query.type
225             )
226         )
227 
228         val parameters = delegate.extractQueryParams(query)
229         return WriteQueryFunction(
230             element = executableElement,
231             query = query,
232             returnType = returnType,
233             parameters = parameters,
234             preparedQueryResultBinder = resultBinder
235         )
236     }
237 
238     @Suppress("DEPRECATION") // Due to MapInfo usage
getQueryFunctionnull239     private fun getQueryFunction(
240         delegate: FunctionProcessorDelegate,
241         returnType: XType,
242         query: ParsedQuery
243     ): QueryFunction {
244         val resultBinder =
245             delegate.findResultBinder(returnType, query) {
246                 delegate.executableElement.getAnnotation(androidx.room.MapInfo::class)?.let {
247                     processMapInfo(it, query, delegate.executableElement, this)
248                 }
249             }
250         context.checker.check(
251             resultBinder.adapter != null,
252             executableElement,
253             ProcessorErrors.cannotFindQueryResultAdapter(
254                 returnType.asTypeName().toString(context.codeLanguage)
255             )
256         )
257 
258         val inTransaction = executableElement.hasAnnotation(Transaction::class)
259         if (query.type == QueryType.SELECT && !inTransaction) {
260             // put a warning if it is has relations and not annotated w/ transaction
261             val hasRelations =
262                 resultBinder.adapter?.rowAdapters?.any { adapter ->
263                     adapter is DataClassRowAdapter && adapter.relationCollectors.isNotEmpty()
264                 } == true
265             if (hasRelations) {
266                 context.logger.w(
267                     Warning.RELATION_QUERY_WITHOUT_TRANSACTION,
268                     executableElement,
269                     ProcessorErrors.TRANSACTION_MISSING_ON_RELATION
270                 )
271             }
272         }
273 
274         query.resultInfo?.let { queryResultInfo ->
275             val mappings = resultBinder.adapter?.mappings ?: return@let
276             // If there are no mapping (e.g. might be a primitive return type result), then we
277             // can't reasonable determine cursor mismatch.
278             if (
279                 mappings.isEmpty() || mappings.none { it is DataClassRowAdapter.DataClassMapping }
280             ) {
281                 return@let
282             }
283             val usedColumns = mappings.flatMap { it.usedColumns }
284             val columnNames = queryResultInfo.columns.map { it.name }
285             val unusedColumns = columnNames - usedColumns
286             val dataClassMappings =
287                 mappings.filterIsInstance<DataClassRowAdapter.DataClassMapping>()
288             val pojoUnusedFields =
289                 dataClassMappings
290                     .filter { it.unusedFields.isNotEmpty() }
291                     .associate {
292                         it.dataClass.typeName.toString(context.codeLanguage) to it.unusedFields
293                     }
294             if (unusedColumns.isNotEmpty() || pojoUnusedFields.isNotEmpty()) {
295                 val warningMsg =
296                     ProcessorErrors.queryPropertyDataClassMismatch(
297                         dataClassTypeNames =
298                             dataClassMappings.map {
299                                 it.dataClass.typeName.toString(context.codeLanguage)
300                             },
301                         unusedColumns = unusedColumns,
302                         allColumns = columnNames,
303                         dataClassUnusedProperties = pojoUnusedFields,
304                     )
305                 context.logger.w(Warning.QUERY_MISMATCH, executableElement, warningMsg)
306             }
307         }
308 
309         val parameters = delegate.extractQueryParams(query)
310 
311         return ReadQueryFunction(
312             element = executableElement,
313             query = query,
314             returnType = returnType,
315             parameters = parameters,
316             inTransaction = inTransaction,
317             queryResultBinder = resultBinder
318         )
319     }
320 
321     /**
322      * Parse @MapInfo annotation, validate its inputs and put information in the bag of extras, it
323      * will be later used by the TypeAdapterStore.
324      */
325     @Suppress("DEPRECATION") // Due to @MapInfo usage
processMapInfonull326     private fun processMapInfo(
327         mapInfoAnnotation: XAnnotation,
328         query: ParsedQuery,
329         queryExecutableElement: XMethodElement,
330         adapterExtras: TypeAdapterExtras,
331     ) {
332         val keyColumn = mapInfoAnnotation["keyColumn"]?.asString() ?: ""
333         val keyTable = mapInfoAnnotation["keyTable"]?.asString()?.ifEmpty { null }
334         val valueColumn = mapInfoAnnotation["valueColumn"]?.asString() ?: ""
335         val valueTable = mapInfoAnnotation["valueTable"]?.asString()?.ifEmpty { null }
336 
337         val resultTableAliases = query.tables.associate { it.name to it.alias }
338         // Checks if this list of columns contains one with matching name and origin table.
339         // Takes into account that projection tables names might be aliased but originTable uses
340         // sqlite3_column_origin_name which is un-aliased.
341         fun List<ColumnInfo>.contains(columnName: String, tableName: String?) =
342             any { resultColumn ->
343                 val resultTableAlias =
344                     resultColumn.originTable?.let { resultTableAliases[it] ?: it }
345                 resultColumn.name == columnName &&
346                     (if (tableName != null) {
347                         resultTableAlias == tableName || resultColumn.originTable == tableName
348                     } else true)
349             }
350 
351         context.checker.check(
352             keyColumn.isNotEmpty() || valueColumn.isNotEmpty(),
353             queryExecutableElement,
354             ProcessorErrors.MAP_INFO_MUST_HAVE_AT_LEAST_ONE_COLUMN_PROVIDED
355         )
356 
357         val resultColumns = query.resultInfo?.columns
358 
359         if (resultColumns != null) {
360             context.checker.check(
361                 keyColumn.isEmpty() || resultColumns.contains(keyColumn, keyTable),
362                 queryExecutableElement
363             ) {
364                 cannotMapSpecifiedColumn(
365                     (if (keyTable != null) "$keyTable." else "") + keyColumn,
366                     resultColumns.map { it.name },
367                     androidx.room.MapInfo::class.java.simpleName
368                 )
369             }
370             context.checker.check(
371                 valueColumn.isEmpty() || resultColumns.contains(valueColumn, valueTable),
372                 queryExecutableElement
373             ) {
374                 cannotMapSpecifiedColumn(
375                     (if (valueTable != null) "$valueTable." else "") + valueColumn,
376                     resultColumns.map { it.name },
377                     androidx.room.MapInfo::class.java.simpleName
378                 )
379             }
380         }
381 
382         adapterExtras.putData(MapInfo::class, MapInfo(keyColumn, valueColumn))
383     }
384 
385     companion object {
386         val PREPARED_TYPES = arrayOf(QueryType.INSERT, QueryType.DELETE, QueryType.UPDATE)
387     }
388 }
389