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