1 /*
<lambda>null2 * Copyright 2024 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.util
18
19 import androidx.room.ColumnInfo
20 import androidx.sqlite.SQLiteConnection
21 import androidx.sqlite.SQLiteStatement
22 import kotlin.collections.removeLast as removeLastKt
23
24 /**
25 * Implements https://www.sqlite.org/datatype3.html section 3.1
26 *
27 * @param type The type that was given to the sqlite
28 * @return The normalized type which is one of the 5 known affinities
29 */
30 @ColumnInfo.SQLiteTypeAffinity
31 internal fun findAffinity(type: String?): Int {
32 if (type == null) {
33 return ColumnInfo.BLOB
34 }
35 val uppercaseType = type.uppercase()
36 if (uppercaseType.contains("INT")) {
37 return ColumnInfo.INTEGER
38 }
39 if (
40 uppercaseType.contains("CHAR") ||
41 uppercaseType.contains("CLOB") ||
42 uppercaseType.contains("TEXT")
43 ) {
44 return ColumnInfo.TEXT
45 }
46 if (uppercaseType.contains("BLOB")) {
47 return ColumnInfo.BLOB
48 }
49 if (
50 uppercaseType.contains("REAL") ||
51 uppercaseType.contains("FLOA") ||
52 uppercaseType.contains("DOUB")
53 ) {
54 return ColumnInfo.REAL
55 }
56 // SQLite returns NUMERIC here but it is like a catch all. We already
57 // have UNDEFINED so it is better to use UNDEFINED for consistency.
58 return ColumnInfo.UNDEFINED
59 }
60
readTableInfonull61 internal fun readTableInfo(connection: SQLiteConnection, tableName: String): TableInfo {
62 val columns = readColumns(connection, tableName)
63 val foreignKeys = readForeignKeys(connection, tableName)
64 val indices = readIndices(connection, tableName)
65 return TableInfo(tableName, columns, foreignKeys, indices)
66 }
67
readForeignKeysnull68 private fun readForeignKeys(
69 connection: SQLiteConnection,
70 tableName: String
71 ): Set<TableInfo.ForeignKey> {
72 // this seems to return everything in order but it is not documented so better be safe
73 connection.prepare("PRAGMA foreign_key_list(`$tableName`)").use { stmt ->
74 val idColumnIndex = stmt.columnIndexOf("id")
75 val seqColumnIndex = stmt.columnIndexOf("seq")
76 val tableColumnIndex = stmt.columnIndexOf("table")
77 val onDeleteColumnIndex = stmt.columnIndexOf("on_delete")
78 val onUpdateColumnIndex = stmt.columnIndexOf("on_update")
79 val ordered = readForeignKeyFieldMappings(stmt)
80
81 // Reset cursor as readForeignKeyFieldMappings has moved it
82 stmt.reset()
83 return buildSet {
84 while (stmt.step()) {
85 val seq = stmt.getLong(seqColumnIndex)
86 if (seq != 0L) {
87 continue
88 }
89 val id = stmt.getLong(idColumnIndex).toInt()
90 val myColumns = mutableListOf<String>()
91 val refColumns = mutableListOf<String>()
92
93 ordered
94 .filter { it.id == id }
95 .forEach { key ->
96 myColumns.add(key.from)
97 refColumns.add(key.to)
98 }
99
100 add(
101 TableInfo.ForeignKey(
102 referenceTable = stmt.getText(tableColumnIndex),
103 onDelete = stmt.getText(onDeleteColumnIndex),
104 onUpdate = stmt.getText(onUpdateColumnIndex),
105 columnNames = myColumns,
106 referenceColumnNames = refColumns
107 )
108 )
109 }
110 }
111 }
112 }
113
114 /**
115 * Temporary data holder for a foreign key row in the pragma result. We need this to ensure sorting
116 * in the generated foreign key object.
117 */
118 private class ForeignKeyWithSequence(
119 val id: Int,
120 val sequence: Int,
121 val from: String,
122 val to: String
123 ) : Comparable<ForeignKeyWithSequence> {
compareTonull124 override fun compareTo(other: ForeignKeyWithSequence): Int {
125 val idCmp = id - other.id
126 return if (idCmp == 0) {
127 sequence - other.sequence
128 } else {
129 idCmp
130 }
131 }
132 }
133
readForeignKeyFieldMappingsnull134 private fun readForeignKeyFieldMappings(stmt: SQLiteStatement): List<ForeignKeyWithSequence> {
135 val idColumnIndex = stmt.columnIndexOf("id")
136 val seqColumnIndex = stmt.columnIndexOf("seq")
137 val fromColumnIndex = stmt.columnIndexOf("from")
138 val toColumnIndex = stmt.columnIndexOf("to")
139
140 return buildList {
141 while (stmt.step()) {
142 add(
143 ForeignKeyWithSequence(
144 id = stmt.getLong(idColumnIndex).toInt(),
145 sequence = stmt.getLong(seqColumnIndex).toInt(),
146 from = stmt.getText(fromColumnIndex),
147 to = stmt.getText(toColumnIndex)
148 )
149 )
150 }
151 }
152 .sorted()
153 }
154
readColumnsnull155 private fun readColumns(
156 connection: SQLiteConnection,
157 tableName: String
158 ): Map<String, TableInfo.Column> {
159 connection.prepare("PRAGMA table_info(`$tableName`)").use { stmt ->
160 if (!stmt.step()) {
161 return emptyMap()
162 }
163
164 val nameIndex = stmt.columnIndexOf("name")
165 val typeIndex = stmt.columnIndexOf("type")
166 val notNullIndex = stmt.columnIndexOf("notnull")
167 val pkIndex = stmt.columnIndexOf("pk")
168 val defaultValueIndex = stmt.columnIndexOf("dflt_value")
169
170 return buildMap {
171 do {
172 val name = stmt.getText(nameIndex)
173 val type = stmt.getText(typeIndex)
174 val notNull = stmt.getLong(notNullIndex) != 0L
175 val primaryKeyPosition = stmt.getLong(pkIndex).toInt()
176 val defaultValue =
177 if (stmt.isNull(defaultValueIndex)) null else stmt.getText(defaultValueIndex)
178 put(
179 key = name,
180 value =
181 TableInfo.Column(
182 name = name,
183 type = type,
184 notNull = notNull,
185 primaryKeyPosition = primaryKeyPosition,
186 defaultValue = defaultValue,
187 createdFrom = TableInfo.CREATED_FROM_DATABASE
188 )
189 )
190 } while (stmt.step())
191 }
192 }
193 }
194
195 /** @return null if we cannot read the indices due to older sqlite implementations. */
readIndicesnull196 private fun readIndices(connection: SQLiteConnection, tableName: String): Set<TableInfo.Index>? {
197 connection.prepare("PRAGMA index_list(`$tableName`)").use { stmt ->
198 val nameColumnIndex = stmt.columnIndexOf("name")
199 val originColumnIndex = stmt.columnIndexOf("origin")
200 val uniqueIndex = stmt.columnIndexOf("unique")
201 if (nameColumnIndex == -1 || originColumnIndex == -1 || uniqueIndex == -1) {
202 // we cannot read them so better not validate any index.
203 return null
204 }
205 return buildSet {
206 while (stmt.step()) {
207 val origin = stmt.getText(originColumnIndex)
208 if ("c" != origin) {
209 // Ignore auto-created indices
210 continue
211 }
212 val name = stmt.getText(nameColumnIndex)
213 val unique = stmt.getLong(uniqueIndex) == 1L
214 // Read index but if we cannot read it properly so better not read it
215 val index = readIndex(connection, name, unique) ?: return null
216 add(index)
217 }
218 }
219 }
220 }
221
222 /** @return null if we cannot read the index due to older sqlite implementations. */
readIndexnull223 private fun readIndex(
224 connection: SQLiteConnection,
225 name: String,
226 unique: Boolean
227 ): TableInfo.Index? {
228 return connection.prepare("PRAGMA index_xinfo(`$name`)").use { stmt ->
229 val seqnoColumnIndex = stmt.columnIndexOf("seqno")
230 val cidColumnIndex = stmt.columnIndexOf("cid")
231 val nameColumnIndex = stmt.columnIndexOf("name")
232 val descColumnIndex = stmt.columnIndexOf("desc")
233 if (
234 seqnoColumnIndex == -1 ||
235 cidColumnIndex == -1 ||
236 nameColumnIndex == -1 ||
237 descColumnIndex == -1
238 ) {
239 // we cannot read them so better not validate any index.
240 return null
241 }
242 val columnsMap = mutableMapOf<Int, String>()
243 val ordersMap = mutableMapOf<Int, String>()
244 while (stmt.step()) {
245 val cid = stmt.getLong(cidColumnIndex).toInt()
246 if (cid < 0) {
247 // Ignore SQLite row ID
248 continue
249 }
250 val seq = stmt.getLong(seqnoColumnIndex).toInt()
251 val columnName = stmt.getText(nameColumnIndex)
252 val order = if (stmt.getLong(descColumnIndex) > 0) "DESC" else "ASC"
253 columnsMap[seq] = columnName
254 ordersMap[seq] = order
255 }
256 val columns = columnsMap.entries.sortedBy { it.key }.map { it.value }.toList()
257 val orders = ordersMap.entries.sortedBy { it.key }.map { it.value }.toList()
258 TableInfo.Index(name, unique, columns, orders)
259 }
260 }
261
readFtsColumnsnull262 internal fun readFtsColumns(connection: SQLiteConnection, tableName: String): Set<String> {
263 return buildSet {
264 connection.prepare("PRAGMA table_info(`$tableName`)").use { stmt ->
265 if (!stmt.step()) return@use
266 val nameIndex = stmt.columnIndexOf("name")
267 do {
268 add(stmt.getText(nameIndex))
269 } while (stmt.step())
270 }
271 }
272 }
273
readFtsOptionsnull274 internal fun readFtsOptions(connection: SQLiteConnection, tableName: String): Set<String> {
275 val sql =
276 connection.prepare("SELECT * FROM sqlite_master WHERE `name` = '$tableName'").use { stmt ->
277 if (stmt.step()) {
278 stmt.getText(stmt.columnIndexOf("sql"))
279 } else {
280 ""
281 }
282 }
283 return parseFtsOptions(sql)
284 }
285
286 // A set of valid FTS Options
287 private val FTS_OPTIONS =
288 arrayOf(
289 "tokenize=",
290 "compress=",
291 "content=",
292 "languageid=",
293 "matchinfo=",
294 "notindexed=",
295 "order=",
296 "prefix=",
297 "uncompress="
298 )
299
300 /**
301 * Parses FTS options from the create statement of an FTS table.
302 *
303 * This method assumes the given create statement is a valid well-formed SQLite statement as defined
304 * in the [CREATE VIRTUAL TABLE syntax diagram](https://www.sqlite.org/lang_createvtab.html).
305 *
306 * @param createStatement the "CREATE VIRTUAL TABLE" statement.
307 * @return the set of FTS option key and values in the create statement.
308 */
parseFtsOptionsnull309 internal fun parseFtsOptions(createStatement: String): Set<String> {
310 if (createStatement.isEmpty()) {
311 return emptySet()
312 }
313
314 // Module arguments are within the parenthesis followed by the module name.
315 val argsString =
316 createStatement.substring(
317 createStatement.indexOf('(') + 1,
318 createStatement.lastIndexOf(')')
319 )
320
321 // Split the module argument string by the comma delimiter, keeping track of quotation
322 // so that if the delimiter is found within a string literal we don't substring at the
323 // wrong index. SQLite supports four ways of quoting keywords, see:
324 // https://www.sqlite.org/lang_keywords.html
325 val args = mutableListOf<String>()
326 val quoteStack = ArrayDeque<Char>()
327 var lastDelimiterIndex = -1
328 argsString.forEachIndexed { i, value ->
329 when (value) {
330 '\'',
331 '"',
332 '`' ->
333 if (quoteStack.isEmpty()) {
334 quoteStack.addFirst(value)
335 } else if (quoteStack.firstOrNull() == value) {
336 quoteStack.removeLastKt()
337 }
338 '[' ->
339 if (quoteStack.isEmpty()) {
340 quoteStack.addFirst(value)
341 }
342 ']' ->
343 if (!quoteStack.isEmpty() && quoteStack.firstOrNull() == '[') {
344 quoteStack.removeLastKt()
345 }
346 ',' ->
347 if (quoteStack.isEmpty()) {
348 args.add(argsString.substring(lastDelimiterIndex + 1, i).trim { it <= ' ' })
349 lastDelimiterIndex = i
350 }
351 }
352 }
353
354 // Add final argument.
355 args.add(argsString.substring(lastDelimiterIndex + 1).trim())
356
357 // Match args against valid options, otherwise they are column definitions.
358 val options =
359 args
360 .filter { arg -> FTS_OPTIONS.any { validOption -> arg.startsWith(validOption) } }
361 .toSet()
362 return options
363 }
364
readViewInfonull365 internal fun readViewInfo(connection: SQLiteConnection, viewName: String): ViewInfo {
366 return connection
367 .prepare(
368 "SELECT name, sql FROM sqlite_master " + "WHERE type = 'view' AND name = '$viewName'"
369 )
370 .use { stmt ->
371 if (stmt.step()) {
372 ViewInfo(stmt.getText(0), stmt.getText(1))
373 } else {
374 ViewInfo(viewName, null)
375 }
376 }
377 }
378