1 /*
<lambda>null2  * Copyright (C) 2017 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.vo
18 
19 import androidx.room.BuiltInTypeConverters
20 import androidx.room.compiler.codegen.CodeLanguage
21 import androidx.room.compiler.codegen.XCodeBlock
22 import androidx.room.compiler.codegen.XCodeBlock.Builder.Companion.applyTo
23 import androidx.room.compiler.codegen.XTypeName
24 import androidx.room.compiler.codegen.buildCodeBlock
25 import androidx.room.compiler.processing.XNullability
26 import androidx.room.ext.CollectionTypeNames.ARRAY_MAP
27 import androidx.room.ext.CollectionTypeNames.LONG_SPARSE_ARRAY
28 import androidx.room.ext.CommonTypeNames
29 import androidx.room.ext.CommonTypeNames.ARRAY_LIST
30 import androidx.room.ext.CommonTypeNames.HASH_MAP
31 import androidx.room.ext.CommonTypeNames.HASH_SET
32 import androidx.room.ext.KotlinCollectionMemberNames
33 import androidx.room.ext.KotlinCollectionMemberNames.MUTABLE_LIST_OF
34 import androidx.room.ext.KotlinCollectionMemberNames.MUTABLE_SET_OF
35 import androidx.room.ext.RoomTypeNames.BYTE_ARRAY_WRAPPER
36 import androidx.room.ext.capitalize
37 import androidx.room.ext.stripNonJava
38 import androidx.room.parser.ParsedQuery
39 import androidx.room.parser.SQLTypeAffinity
40 import androidx.room.parser.SqlParser
41 import androidx.room.processor.Context
42 import androidx.room.processor.ProcessorErrors
43 import androidx.room.processor.ProcessorErrors.ISSUE_TRACKER_LINK
44 import androidx.room.processor.ProcessorErrors.relationAffinityMismatch
45 import androidx.room.processor.ProcessorErrors.relationJunctionChildAffinityMismatch
46 import androidx.room.processor.ProcessorErrors.relationJunctionParentAffinityMismatch
47 import androidx.room.solver.CodeGenScope
48 import androidx.room.solver.query.parameter.QueryParameterAdapter
49 import androidx.room.solver.query.result.RowAdapter
50 import androidx.room.solver.query.result.SingleColumnRowAdapter
51 import androidx.room.solver.types.StatementValueReader
52 import androidx.room.verifier.DatabaseVerificationErrors
53 import androidx.room.writer.QueryWriter
54 import androidx.room.writer.RelationCollectorFunctionWriter
55 import androidx.room.writer.RelationCollectorFunctionWriter.Companion.PARAM_CONNECTION_VARIABLE
56 import java.util.Locale
57 
58 /** Internal class that is used to manage fetching 1/N to N relationships. */
59 data class RelationCollector(
60     val relation: Relation,
61     // affinity between relation fields
62     val affinity: SQLTypeAffinity,
63     // concrete map type name to store relationship
64     val mapTypeName: XTypeName,
65     // map key type name, not the same as the parent or entity field type
66     val keyTypeName: XTypeName,
67     // map value type name, it is assignable to the @Relation field
68     val relationTypeName: XTypeName,
69     // query writer for the relating entity query
70     val queryWriter: QueryWriter,
71     // key reader for the parent field
72     val parentKeyColumnReader: StatementValueReader,
73     // key reader for the entity field
74     val entityKeyColumnReader: StatementValueReader,
75     // adapter for the relating pojo
76     val rowAdapter: RowAdapter,
77     // parsed relating entity query
78     val loadAllQuery: ParsedQuery,
79     // true if `relationTypeName` is a Collection, when it is `relationTypeName` is always non null.
80     val relationTypeIsCollection: Boolean
81 ) {
82     // variable name of map containing keys to relation collections, set when writing the code
83     // generator in writeInitCode
84     private lateinit var varName: String
85 
86     fun writeInitCode(scope: CodeGenScope) {
87         varName =
88             scope.getTmpVar(
89                 "_collection${relation.property.getPath().stripNonJava().capitalize(Locale.US)}"
90             )
91         scope.builder.applyTo { language ->
92             if (
93                 language == CodeLanguage.JAVA ||
94                     mapTypeName.rawTypeName == ARRAY_MAP ||
95                     mapTypeName.rawTypeName == LONG_SPARSE_ARRAY
96             ) {
97                 addLocalVariable(
98                     name = varName,
99                     typeName = mapTypeName,
100                     assignExpr = XCodeBlock.ofNewInstance(mapTypeName)
101                 )
102             } else {
103                 addLocalVal(
104                     name = varName,
105                     typeName = mapTypeName,
106                     "%M()",
107                     KotlinCollectionMemberNames.MUTABLE_MAP_OF
108                 )
109             }
110         }
111     }
112 
113     // called to extract the key if it exists and adds it to the map of relations to fetch.
114     fun writeReadParentKeyCode(
115         stmtVarName: String,
116         fieldsWithIndices: List<PropertyWithIndex>,
117         scope: CodeGenScope
118     ) {
119         val indexVar =
120             fieldsWithIndices.firstOrNull { it.property === relation.parentProperty }?.indexVar
121         checkNotNull(indexVar) {
122             "Expected an index var for a column named '${relation.parentProperty.columnName}' to " +
123                 "query the '${relation.dataClassType}' @Relation but didn't. Please file a bug at " +
124                 ISSUE_TRACKER_LINK
125         }
126         scope.builder.apply {
127             readKey(stmtVarName, indexVar, parentKeyColumnReader, scope) { tmpVar ->
128                 // for relation collection put an empty collections in the map, otherwise put nulls
129                 if (relationTypeIsCollection) {
130                     beginControlFlow("if (!%L.containsKey(%L))", varName, tmpVar).apply {
131                         val newEmptyCollection = buildCodeBlock { language ->
132                             when (language) {
133                                 CodeLanguage.JAVA -> add("new %T()", relationTypeName)
134                                 CodeLanguage.KOTLIN ->
135                                     if (
136                                         relationTypeName.rawTypeName == CommonTypeNames.MUTABLE_SET
137                                     ) {
138                                         add("%M()", MUTABLE_SET_OF)
139                                     } else {
140                                         add("%M()", MUTABLE_LIST_OF)
141                                     }
142                             }
143                         }
144                         addStatement("%L.put(%L, %L)", varName, tmpVar, newEmptyCollection)
145                     }
146                     endControlFlow()
147                 } else {
148                     addStatement("%L.put(%L, null)", varName, tmpVar)
149                 }
150             }
151         }
152     }
153 
154     // called to extract key and relation collection, defaulting to empty collection if not found
155     fun writeReadCollectionIntoTmpVar(
156         stmtVarName: String,
157         propertiesWithIndices: List<PropertyWithIndex>,
158         scope: CodeGenScope
159     ): Pair<String, Property> {
160         val indexVar =
161             propertiesWithIndices.firstOrNull { it.property === relation.parentProperty }?.indexVar
162         checkNotNull(indexVar) {
163             "Expected an index var for a column named '${relation.parentProperty.columnName}' to " +
164                 "query the '${relation.dataClassType}' @Relation but didn't. Please file a bug at " +
165                 ISSUE_TRACKER_LINK
166         }
167         val tmpVarNameSuffix = if (relationTypeIsCollection) "Collection" else ""
168         val tmpRelationVar =
169             scope.getTmpVar(
170                 "_tmp${relation.property.name.stripNonJava().capitalize(Locale.US)}$tmpVarNameSuffix"
171             )
172         scope.builder.apply {
173             addLocalVariable(name = tmpRelationVar, typeName = relationTypeName)
174             readKey(
175                 stmtVarName = stmtVarName,
176                 indexVar = indexVar,
177                 keyReader = parentKeyColumnReader,
178                 scope = scope,
179                 onKeyReady = { tmpKeyVar ->
180                     if (relationTypeIsCollection) {
181                         // For Kotlin use getValue() as get() return a nullable value, when the
182                         // relation is a collection the map is pre-filled with empty collection
183                         // values for all keys, so this is safe. Special case for LongSParseArray
184                         // since it does not have a getValue() from Kotlin.
185                         val usingLongSparseArray = mapTypeName.rawTypeName == LONG_SPARSE_ARRAY
186                         applyTo { language ->
187                             when (language) {
188                                 CodeLanguage.JAVA ->
189                                     addStatement(
190                                         "%L = %L.get(%L)",
191                                         tmpRelationVar,
192                                         varName,
193                                         tmpKeyVar
194                                     )
195                                 CodeLanguage.KOTLIN ->
196                                     if (usingLongSparseArray) {
197                                         addStatement(
198                                             "%L = checkNotNull(%L.get(%L))",
199                                             tmpRelationVar,
200                                             varName,
201                                             tmpKeyVar
202                                         )
203                                     } else {
204                                         addStatement(
205                                             "%L = %L.getValue(%L)",
206                                             tmpRelationVar,
207                                             varName,
208                                             tmpKeyVar
209                                         )
210                                     }
211                             }
212                         }
213                     } else {
214                         addStatement("%L = %L.get(%L)", tmpRelationVar, varName, tmpKeyVar)
215                         if (relation.property.nonNull) {
216                             applyTo(CodeLanguage.KOTLIN) {
217                                 beginControlFlow("if (%L == null)", tmpRelationVar)
218                                 addStatement(
219                                     "error(%S)",
220                                     "Relationship item '${relation.property.name}' was expected to" +
221                                         " be NON-NULL but is NULL in @Relation involving " +
222                                         "a parent column named '${relation.parentProperty.columnName}' and " +
223                                         "entityColumn named '${relation.entityProperty.columnName}'."
224                                 )
225                                 endControlFlow()
226                             }
227                         }
228                     }
229                 },
230                 onKeyUnavailable = {
231                     if (relationTypeIsCollection) {
232                         val newEmptyCollection = buildCodeBlock { language ->
233                             when (language) {
234                                 CodeLanguage.JAVA -> add("new %T()", relationTypeName)
235                                 CodeLanguage.KOTLIN ->
236                                     if (
237                                         relationTypeName.rawTypeName == CommonTypeNames.MUTABLE_SET
238                                     ) {
239                                         add("%M()", MUTABLE_SET_OF)
240                                     } else {
241                                         add("%M()", MUTABLE_LIST_OF)
242                                     }
243                             }
244                         }
245                         addStatement("%L = %L", tmpRelationVar, newEmptyCollection)
246                     } else {
247                         addStatement("%L = null", tmpRelationVar)
248                     }
249                 }
250             )
251         }
252         return tmpRelationVar to relation.property
253     }
254 
255     // called to write the invocation to the fetch relationship function
256     fun writeFetchRelationCall(scope: CodeGenScope) {
257         val function = scope.writer.getOrCreateFunction(RelationCollectorFunctionWriter(this))
258         scope.builder.apply {
259             addStatement("%L(%L, %L)", function.name, PARAM_CONNECTION_VARIABLE, varName)
260         }
261     }
262 
263     // called to read key and call `onKeyReady` to write code once it is successfully read
264     fun readKey(
265         stmtVarName: String,
266         indexVar: String,
267         keyReader: StatementValueReader,
268         scope: CodeGenScope,
269         onKeyReady: XCodeBlock.Builder.(String) -> Unit
270     ) {
271         readKey(stmtVarName, indexVar, keyReader, scope, onKeyReady, null)
272     }
273 
274     // called to read key and call `onKeyReady` to write code once it is successfully read and
275     // `onKeyUnavailable` if the key is unavailable (missing column due to bad projection).
276     private fun readKey(
277         stmtVarName: String,
278         indexVar: String,
279         keyReader: StatementValueReader,
280         scope: CodeGenScope,
281         onKeyReady: XCodeBlock.Builder.(String) -> Unit,
282         onKeyUnavailable: (XCodeBlock.Builder.() -> Unit)?,
283     ) {
284         scope.builder.apply {
285             val tmpVar = scope.getTmpVar("_tmpKey")
286             addLocalVariable(tmpVar, keyReader.typeMirror().asTypeName())
287             keyReader.readFromStatement(tmpVar, stmtVarName, indexVar, scope)
288             if (keyReader.typeMirror().nullability == XNullability.NONNULL) {
289                 onKeyReady(tmpVar)
290             } else {
291                 beginControlFlow("if (%L != null)", tmpVar)
292                 onKeyReady(tmpVar)
293                 if (onKeyUnavailable != null) {
294                     nextControlFlow("else")
295                     onKeyUnavailable()
296                 }
297                 endControlFlow()
298             }
299         }
300     }
301 
302     /**
303      * Adapter for binding a LongSparseArray keys into query arguments. This special adapter is only
304      * used for binding the relationship query whose keys have INTEGER affinity.
305      */
306     private class LongSparseArrayKeyQueryParameterAdapter : QueryParameterAdapter(true) {
307         override fun bindToStmt(
308             inputVarName: String,
309             stmtVarName: String,
310             startIndexVarName: String,
311             scope: CodeGenScope
312         ) {
313             val itrIndexVar = "i"
314             val itrItemVar = scope.getTmpVar("_item")
315             scope.builder.applyTo { language ->
316                 when (language) {
317                     CodeLanguage.JAVA ->
318                         beginControlFlow(
319                             "for (int %L = 0; %L < %L.size(); i++)",
320                             itrIndexVar,
321                             itrIndexVar,
322                             inputVarName
323                         )
324                     CodeLanguage.KOTLIN ->
325                         beginControlFlow("for (%L in 0 until %L.size())", itrIndexVar, inputVarName)
326                 }.apply {
327                     addLocalVal(
328                         itrItemVar,
329                         XTypeName.PRIMITIVE_LONG,
330                         "%L.keyAt(%L)",
331                         inputVarName,
332                         itrIndexVar
333                     )
334                     addStatement("%L.bindLong(%L, %L)", stmtVarName, startIndexVarName, itrItemVar)
335                     addStatement("%L++", startIndexVarName)
336                 }
337                 endControlFlow()
338             }
339         }
340 
341         override fun getArgCount(inputVarName: String, outputVarName: String, scope: CodeGenScope) {
342             scope.builder.addLocalVal(
343                 outputVarName,
344                 XTypeName.PRIMITIVE_INT,
345                 "%L.size()",
346                 inputVarName
347             )
348         }
349     }
350 
351     companion object {
352 
353         private val LONG_SPARSE_ARRAY_KEY_QUERY_PARAM_ADAPTER =
354             LongSparseArrayKeyQueryParameterAdapter()
355 
356         fun createCollectors(
357             baseContext: Context,
358             relations: List<Relation>
359         ): List<RelationCollector> {
360             return relations
361                 .map { relation ->
362                     val context =
363                         baseContext.fork(
364                             element = relation.property.element,
365                             forceSuppressedWarnings = setOf(Warning.QUERY_MISMATCH),
366                             forceBuiltInConverters =
367                                 BuiltInConverterFlags.DEFAULT.copy(
368                                     byteBuffer = BuiltInTypeConverters.State.ENABLED
369                                 )
370                         )
371                     val affinity = affinityFor(context, relation)
372                     val keyTypeName = keyTypeFor(context, affinity)
373                     val (relationTypeName, isRelationCollection) =
374                         relationTypeFor(context, relation)
375                     val tmpMapTypeName =
376                         temporaryMapTypeFor(context, affinity, keyTypeName, relationTypeName)
377 
378                     val loadAllQuery = relation.createLoadAllSql()
379                     val parsedQuery = SqlParser.parse(loadAllQuery)
380                     context.checker.check(
381                         parsedQuery.errors.isEmpty(),
382                         relation.property.element,
383                         parsedQuery.errors.joinToString("\n")
384                     )
385                     if (parsedQuery.errors.isEmpty()) {
386                         val resultInfo = context.databaseVerifier?.analyze(loadAllQuery)
387                         parsedQuery.resultInfo = resultInfo
388                         if (resultInfo?.error != null) {
389                             context.logger.e(
390                                 relation.property.element,
391                                 DatabaseVerificationErrors.cannotVerifyQuery(resultInfo.error)
392                             )
393                         }
394                     }
395                     val resultInfo = parsedQuery.resultInfo
396 
397                     val usingLongSparseArray = tmpMapTypeName.rawTypeName == LONG_SPARSE_ARRAY
398                     val queryParam =
399                         if (usingLongSparseArray) {
400                             val longSparseArrayElement =
401                                 context.processingEnv.requireTypeElement(
402                                     LONG_SPARSE_ARRAY.canonicalName
403                                 )
404                             QueryParameter(
405                                 name = RelationCollectorFunctionWriter.PARAM_MAP_VARIABLE,
406                                 sqlName = RelationCollectorFunctionWriter.PARAM_MAP_VARIABLE,
407                                 type = longSparseArrayElement.type,
408                                 queryParamAdapter = LONG_SPARSE_ARRAY_KEY_QUERY_PARAM_ADAPTER
409                             )
410                         } else {
411                             val keyTypeMirror = context.processingEnv.requireType(keyTypeName)
412                             val set = context.processingEnv.requireTypeElement(CommonTypeNames.SET)
413                             val keySet = context.processingEnv.getDeclaredType(set, keyTypeMirror)
414                             QueryParameter(
415                                 name = RelationCollectorFunctionWriter.KEY_SET_VARIABLE,
416                                 sqlName = RelationCollectorFunctionWriter.KEY_SET_VARIABLE,
417                                 type = keySet,
418                                 queryParamAdapter =
419                                     context.typeAdapterStore.findQueryParameterAdapter(
420                                         typeMirror = keySet,
421                                         isMultipleParameter = true
422                                     )
423                             )
424                         }
425 
426                     val queryWriter =
427                         QueryWriter(
428                             parameters = listOf(queryParam),
429                             sectionToParamMapping =
430                                 listOf(Pair(parsedQuery.bindSections.first(), queryParam)),
431                             query = parsedQuery
432                         )
433 
434                     val parentKeyColumnReader =
435                         context.typeAdapterStore.findStatementValueReader(
436                             output =
437                                 context.processingEnv.requireType(keyTypeName).let {
438                                     if (!relation.parentProperty.nonNull) it.makeNullable() else it
439                                 },
440                             affinity = affinity
441                         )
442                     val entityKeyColumnReader =
443                         context.typeAdapterStore.findStatementValueReader(
444                             output =
445                                 context.processingEnv.requireType(keyTypeName).let { keyType ->
446                                     if (!relation.entityProperty.nonNull) keyType.makeNullable()
447                                     else keyType
448                                 },
449                             affinity = affinity
450                         )
451                     // We should always find a readers since key types all have built in converters
452                     check(parentKeyColumnReader != null && entityKeyColumnReader != null) {
453                         "Missing one of the relation key value reader for type $keyTypeName"
454                     }
455 
456                     // row adapter that matches full response
457                     fun getDefaultRowAdapter(): RowAdapter? {
458                         return context.typeAdapterStore.findRowAdapter(
459                             relation.dataClassType,
460                             parsedQuery
461                         )
462                     }
463                     val rowAdapter =
464                         if (
465                             relation.projection.size == 1 &&
466                                 resultInfo != null &&
467                                 (resultInfo.columns.size == 1 || resultInfo.columns.size == 2)
468                         ) {
469                             // check for a column adapter first
470                             val cursorReader =
471                                 context.typeAdapterStore.findStatementValueReader(
472                                     relation.dataClassType,
473                                     resultInfo.columns.first().type
474                                 )
475                             if (cursorReader == null) {
476                                 getDefaultRowAdapter()
477                             } else {
478                                 SingleColumnRowAdapter(cursorReader)
479                             }
480                         } else {
481                             getDefaultRowAdapter()
482                         }
483 
484                     if (rowAdapter == null) {
485                         context.logger.e(
486                             relation.property.element,
487                             ProcessorErrors.cannotFindQueryResultAdapter(
488                                 relation.dataClassType.asTypeName().toString(context.codeLanguage)
489                             )
490                         )
491                         null
492                     } else {
493                         RelationCollector(
494                             relation = relation,
495                             affinity = affinity,
496                             mapTypeName = tmpMapTypeName,
497                             keyTypeName = keyTypeName,
498                             relationTypeName = relationTypeName,
499                             queryWriter = queryWriter,
500                             parentKeyColumnReader = parentKeyColumnReader,
501                             entityKeyColumnReader = entityKeyColumnReader,
502                             rowAdapter = rowAdapter,
503                             loadAllQuery = parsedQuery,
504                             relationTypeIsCollection = isRelationCollection
505                         )
506                     }
507                 }
508                 .filterNotNull()
509         }
510 
511         // Gets and check the affinity of the relating columns.
512         private fun affinityFor(context: Context, relation: Relation): SQLTypeAffinity {
513             fun checkAffinity(
514                 first: SQLTypeAffinity?,
515                 second: SQLTypeAffinity?,
516                 onAffinityMismatch: () -> Unit
517             ) =
518                 if (first != null && first == second) {
519                     first
520                 } else {
521                     onAffinityMismatch()
522                     SQLTypeAffinity.TEXT
523                 }
524 
525             val parentAffinity = relation.parentProperty.statementValueReader?.affinity()
526             val childAffinity = relation.entityProperty.statementValueReader?.affinity()
527             val junctionParentAffinity =
528                 relation.junction?.parentProperty?.statementValueReader?.affinity()
529             val junctionChildAffinity =
530                 relation.junction?.entityProperty?.statementValueReader?.affinity()
531             return if (relation.junction != null) {
532                 checkAffinity(childAffinity, junctionChildAffinity) {
533                     context.logger.w(
534                         Warning.RELATION_TYPE_MISMATCH,
535                         relation.property.element,
536                         relationJunctionChildAffinityMismatch(
537                             childColumn = relation.entityProperty.columnName,
538                             junctionChildColumn = relation.junction.entityProperty.columnName,
539                             childAffinity = childAffinity,
540                             junctionChildAffinity = junctionChildAffinity
541                         )
542                     )
543                 }
544                 checkAffinity(parentAffinity, junctionParentAffinity) {
545                     context.logger.w(
546                         Warning.RELATION_TYPE_MISMATCH,
547                         relation.property.element,
548                         relationJunctionParentAffinityMismatch(
549                             parentColumn = relation.parentProperty.columnName,
550                             junctionParentColumn = relation.junction.parentProperty.columnName,
551                             parentAffinity = parentAffinity,
552                             junctionParentAffinity = junctionParentAffinity
553                         )
554                     )
555                 }
556             } else {
557                 checkAffinity(parentAffinity, childAffinity) {
558                     context.logger.w(
559                         Warning.RELATION_TYPE_MISMATCH,
560                         relation.property.element,
561                         relationAffinityMismatch(
562                             parentColumn = relation.parentProperty.columnName,
563                             childColumn = relation.entityProperty.columnName,
564                             parentAffinity = parentAffinity,
565                             childAffinity = childAffinity
566                         )
567                     )
568                 }
569             }
570         }
571 
572         // Gets the resulting relation type name. (i.e. the Pojo's @Relation field type name.)
573         private fun relationTypeFor(context: Context, relation: Relation) =
574             relation.property.type.let { fieldType ->
575                 if (fieldType.typeArguments.isNotEmpty()) {
576                     val rawType = fieldType.rawType
577                     val setType = context.processingEnv.requireType(CommonTypeNames.MUTABLE_SET)
578                     val paramTypeName =
579                         if (rawType.isAssignableFrom(setType.rawType)) {
580                             when (context.codeLanguage) {
581                                 CodeLanguage.KOTLIN ->
582                                     CommonTypeNames.MUTABLE_SET.parametrizedBy(
583                                         relation.dataClassTypeName
584                                     )
585                                 CodeLanguage.JAVA ->
586                                     HASH_SET.parametrizedBy(relation.dataClassTypeName)
587                             }
588                         } else {
589                             when (context.codeLanguage) {
590                                 CodeLanguage.KOTLIN ->
591                                     CommonTypeNames.MUTABLE_LIST.parametrizedBy(
592                                         relation.dataClassTypeName
593                                     )
594                                 CodeLanguage.JAVA ->
595                                     ARRAY_LIST.parametrizedBy(relation.dataClassTypeName)
596                             }
597                         }
598                     paramTypeName to true
599                 } else {
600                     relation.dataClassTypeName.copy(nullable = true) to false
601                 }
602             }
603 
604         // Gets the type name of the temporary key map.
605         private fun temporaryMapTypeFor(
606             context: Context,
607             affinity: SQLTypeAffinity,
608             keyTypeName: XTypeName,
609             valueTypeName: XTypeName,
610         ): XTypeName {
611             val canUseLongSparseArray =
612                 context.processingEnv.findTypeElement(LONG_SPARSE_ARRAY.canonicalName) != null
613             val canUseArrayMap =
614                 context.processingEnv.findTypeElement(ARRAY_MAP.canonicalName) != null &&
615                     context.isAndroidOnlyTarget()
616             return when {
617                 canUseLongSparseArray && affinity == SQLTypeAffinity.INTEGER ->
618                     LONG_SPARSE_ARRAY.parametrizedBy(valueTypeName)
619                 canUseArrayMap -> ARRAY_MAP.parametrizedBy(keyTypeName, valueTypeName)
620                 else ->
621                     when (context.codeLanguage) {
622                         CodeLanguage.JAVA -> HASH_MAP.parametrizedBy(keyTypeName, valueTypeName)
623                         CodeLanguage.KOTLIN ->
624                             CommonTypeNames.MUTABLE_MAP.parametrizedBy(keyTypeName, valueTypeName)
625                     }
626             }
627         }
628 
629         // Gets the type name of the relationship key.
630         private fun keyTypeFor(context: Context, affinity: SQLTypeAffinity): XTypeName {
631             val canUseLongSparseArray =
632                 context.processingEnv.findTypeElement(LONG_SPARSE_ARRAY.canonicalName) != null
633             return when (affinity) {
634                 SQLTypeAffinity.INTEGER ->
635                     if (canUseLongSparseArray) {
636                         XTypeName.PRIMITIVE_LONG
637                     } else {
638                         XTypeName.BOXED_LONG
639                     }
640                 SQLTypeAffinity.REAL -> XTypeName.BOXED_DOUBLE
641                 SQLTypeAffinity.TEXT -> CommonTypeNames.STRING
642                 SQLTypeAffinity.BLOB -> BYTE_ARRAY_WRAPPER
643                 else -> {
644                     // no affinity default to String
645                     CommonTypeNames.STRING
646                 }
647             }
648         }
649     }
650 }
651