1 /*
<lambda>null2  * Copyright 2022 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
18 
19 import androidx.annotation.RestrictTo
20 import kotlin.jvm.JvmStatic
21 
22 /**
23  * Utility class for resolving and mapping ambiguous columns from a query result.
24  *
25  * Given an ordered list containing the result columns of a query along with a collection containing
26  * the set of columns per object, the algorithm will return a new collection containing the indices
27  * of the result columns of each object column.
28  *
29  * The algorithm works as follow:
30  * 1. The input is normalized by making all result column names and mapping column names lowercase.
31  *    SQLite is case insensitive.
32  * 2. The result columns might contain columns who are not part of any object, these are ignored by
33  *    creating a new list containing only useful columns, the original indices are preserved and
34  *    used in the solution. 3.a Next, we find the range of continuous indices where each mapping can
35  *    be found in the useful result columns list. The order in which the columns are found is not
36  *    important as long as they are continuous, this accounts for table migrations. The Rabin-Karp
37  *    algorithm is used for the search, since it has good performance. The has cumulative hash
38  *    function is simply the sum of the hash codes of each column name. 3.b It is expected to find
39  *    at least one range for each mapping, if none is found via Rabin-Karp then we fallback to a
40  *    depth first search approach, which is slower but removes the requirement that the columns must
41  *    be found continuously.
42  * 4. With various possible matches found for each mapping, the last step is to find the 'best'
43  *    solution, the algorithm is heuristic. We go through each combination of matched indices ranges
44  *    using a depth first traversal, comparing a solution made up of such combination with whatever
45  *    is the current best. The algorithms prefers a solution whose matches ranges don't overlap and
46  *    are continuous.
47  */
48 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
49 public object AmbiguousColumnResolver {
50 
51     /**
52      * Maps query result column indices to result object columns.
53      *
54      * @param resultColumns The ordered result column names.
55      * @param mappings An array containing the list of result object column names that must be
56      *   mapped to indices of `resultColumns`.
57      * @return An array with the same dimensions as `mappings` whose values correspond to the index
58      *   in `resultColumns` that match the object column at `mappings[i][j]`.
59      */
60     @JvmStatic
61     public fun resolve(
62         resultColumns: List<String>,
63         mappings: Array<Array<String>>
64     ): Array<IntArray> {
65         return resolve(resultColumns.toTypedArray(), mappings)
66     }
67 
68     /**
69      * Maps query result column indices to result object columns.
70      *
71      * @param resultColumns The ordered result column names.
72      * @param mappings An array containing the list of result object column names that must be
73      *   mapped to indices of `resultColumns`.
74      * @return An array with the same dimensions as `mappings` whose values correspond to the index
75      *   in `resultColumns` that match the object column at `mappings[i][j]`.
76      */
77     @JvmStatic
78     public fun resolve(
79         resultColumns: Array<String>,
80         mappings: Array<Array<String>>
81     ): Array<IntArray> {
82         // Step 1 - Transform all input columns to lowercase
83         for (i in resultColumns.indices) {
84             // For result columns, apply workarounds in CursorUtil.getColumnIndex(), i.e. backtick
85             // trimming.
86             val column = resultColumns[i]
87             resultColumns[i] =
88                 if (column[0] == '`' && column[column.length - 1] == '`') {
89                         column.substring(1, column.length - 1)
90                     } else {
91                         column
92                     }
93                     .lowercase()
94         }
95         for (i in mappings.indices) {
96             for (j in mappings[i].indices) {
97                 mappings[i][j] = mappings[i][j].lowercase()
98             }
99         }
100 
101         // Step 2 - Check requested columns and create a useful list that ignores unused columns.
102         val requestedColumns = buildSet { mappings.forEach { addAll(it) } }
103         val usefulResultColumns = buildList {
104             resultColumns.forEachIndexed { index, columnName ->
105                 if (requestedColumns.contains(columnName)) {
106                     add(ResultColumn(columnName, index))
107                 }
108             }
109         }
110 
111         // Step 3 - Find all sublist from results columns that match mapping columns unordered.
112         val mappingMatches = List(mappings.size) { mutableListOf<Match>() }
113         mappings.forEachIndexed { mappingIndex, mapping ->
114             // Step 3.a - Quick searching using a rolling hash
115             rabinKarpSearch(content = usefulResultColumns, pattern = mapping) {
116                 startIndex,
117                 endIndex,
118                 resultColumnsSublist ->
119                 val resultIndices =
120                     mapping.map { mappingColumnName ->
121                         val resultColumn =
122                             resultColumnsSublist.firstOrNull { (resultColumnName, _) ->
123                                 // TODO: Incorporate workarounds in CursorUtil.getColumnIndex()
124                                 mappingColumnName == resultColumnName
125                             }
126                         resultColumn?.index ?: return@rabinKarpSearch
127                     }
128                 mappingMatches[mappingIndex].add(
129                     Match(
130                         resultRange = IntRange(startIndex, endIndex - 1),
131                         resultIndices = resultIndices
132                     )
133                 )
134             }
135             // Step 3.b - Failed to quickly find a continuous match, widen the search (slow!)
136             if (mappingMatches[mappingIndex].isEmpty()) {
137                 val foundIndices =
138                     mapping.map { mappingColumnName ->
139                         buildList {
140                                 usefulResultColumns.forEach { resultColumn ->
141                                     if (mappingColumnName == resultColumn.name) {
142                                         add(resultColumn.index)
143                                     }
144                                 }
145                             }
146                             .also {
147                                 check(it.isNotEmpty()) {
148                                     "Column $mappingColumnName not found in result"
149                                 }
150                             }
151                     }
152                 dfs(foundIndices) { indices ->
153                     val first = indices.minOf { it }
154                     val last = indices.maxOf { it }
155                     mappingMatches[mappingIndex].add(
156                         Match(resultRange = IntRange(first, last), resultIndices = indices)
157                     )
158                 }
159             }
160         }
161         check(mappingMatches.all { it.isNotEmpty() }) { "Failed to find matches for all mappings" }
162 
163         // Step 4 - Depth first search through combinations finding the best solution
164         var bestSolution = Solution.NO_SOLUTION
165         dfs(mappingMatches) {
166             val currentSolution = Solution.build(it)
167             if (currentSolution < bestSolution) {
168                 bestSolution = currentSolution
169             }
170         }
171         return bestSolution.matches.map { it.resultIndices.toIntArray() }.toTypedArray()
172     }
173 
174     private fun rabinKarpSearch(
175         content: List<ResultColumn>,
176         pattern: Array<String>,
177         onHashMatch: (Int, Int, List<ResultColumn>) -> Unit
178     ) {
179         val mappingHash = pattern.sumOf { it.hashCode() } // Commutative hash
180         var startIndex = 0 // inclusive
181         var endIndex = pattern.size // exclusive
182         var rollingHash = content.subList(startIndex, endIndex).sumOf { it.name.hashCode() }
183         while (true) {
184             if (mappingHash == rollingHash) {
185                 onHashMatch(startIndex, endIndex, content.subList(startIndex, endIndex))
186             }
187             startIndex++
188             endIndex++
189             if (endIndex > content.size) {
190                 break
191             }
192             // Rolling hash adjustment
193             rollingHash -= content[startIndex - 1].name.hashCode()
194             rollingHash += content[endIndex - 1].name.hashCode()
195         }
196     }
197 
198     private fun <T> dfs(
199         content: List<List<T>>,
200         current: MutableList<T> = mutableListOf(),
201         depth: Int = 0,
202         block: (List<T>) -> Unit
203     ) {
204         if (depth == content.size) {
205             block(current.toList())
206             return
207         }
208         content[depth].forEach {
209             current.add(it)
210             dfs(content, current, depth + 1, block)
211             current.removeAt(current.lastIndex) // Avoiding removeLast() due to KT-64381
212         }
213     }
214 
215     private data class ResultColumn(val name: String, val index: Int)
216 
217     private class Match(
218         val resultRange: IntRange,
219         val resultIndices: List<Int>,
220     )
221 
222     /**
223      * A good solution is one that has no overlaps and whose coverage offset is zero, where coverage
224      * offset is the difference between a mapping size and its matching range, i.e. preferring
225      * continuous matches vs those with index gaps.
226      */
227     private class Solution(
228         val matches: List<Match>,
229         val coverageOffset: Int, // amount of indices covered by matches
230         val overlaps: Int, // amount of indices that overlap
231     ) : Comparable<Solution> {
232 
233         override fun compareTo(other: Solution): Int {
234             val overlapCmp = this.overlaps.compareTo(other.overlaps)
235             if (overlapCmp != 0) {
236                 return overlapCmp
237             }
238             return this.coverageOffset.compareTo(other.coverageOffset)
239         }
240 
241         companion object {
242             val NO_SOLUTION = Solution(emptyList(), Int.MAX_VALUE, Int.MAX_VALUE)
243 
244             fun build(matches: List<Match>): Solution {
245                 val coverageOffset =
246                     matches.sumOf {
247                         (it.resultRange.last - it.resultRange.first + 1) - it.resultIndices.size
248                     }
249                 val min = matches.minOf { it.resultRange.first }
250                 val max = matches.maxOf { it.resultRange.last }
251                 val overlaps =
252                     (min..max).count { i ->
253                         var count = 0
254                         matches.forEach {
255                             if (it.resultRange.contains(i)) {
256                                 count++
257                             }
258                             if (count > 1) {
259                                 return@count true
260                             }
261                         }
262                         return@count false
263                     }
264                 return Solution(
265                     matches = matches,
266                     coverageOffset = coverageOffset,
267                     overlaps = overlaps
268                 )
269             }
270         }
271     }
272 }
273