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