<lambda>null1 // Copyright 2015-present 650 Industries. All rights reserved.
2 package expo.modules.sqlite
3
4 import android.content.Context
5 import android.database.Cursor
6 import androidx.core.os.bundleOf
7 import expo.modules.kotlin.exception.Exceptions
8 import expo.modules.kotlin.modules.Module
9 import expo.modules.kotlin.modules.ModuleDefinition
10 import io.expo.android.database.sqlite.SQLiteCustomExtension
11 import io.expo.android.database.sqlite.SQLiteDatabase
12 import io.expo.android.database.sqlite.SQLiteDatabaseConfiguration
13 import java.io.File
14 import java.io.IOException
15 import java.util.*
16
17 private val EMPTY_ROWS = emptyArray<Array<Any?>>()
18 private val EMPTY_COLUMNS = emptyArray<String?>()
19 private val EMPTY_RESULT = SQLiteModule.SQLitePluginResult(EMPTY_ROWS, EMPTY_COLUMNS, 0, 0, null)
20 private val DATABASES: MutableMap<String, SQLiteDatabase?> = HashMap()
21
22 class SQLiteModule : Module() {
23 private val context: Context
24 get() = appContext.reactContext ?: throw Exceptions.ReactContextLost()
25
26 override fun definition() = ModuleDefinition {
27 Name("ExpoSQLite")
28
29 Events("onDatabaseChange")
30
31 AsyncFunction("exec") { dbName: String, queries: List<Query>, readOnly: Boolean ->
32 return@AsyncFunction execute(dbName, queries, readOnly)
33 }
34
35 AsyncFunction("execRawQuery") { dbName: String, queries: List<Query>, readOnly: Boolean ->
36 return@AsyncFunction execute(dbName, queries, readOnly, raw = true)
37 }
38
39 AsyncFunction("close") { dbName: String ->
40 DATABASES
41 .remove(dbName)
42 ?.close()
43 }
44
45 Function("closeSync") { dbName: String ->
46 DATABASES
47 .remove(dbName)
48 ?.close()
49 }
50
51 AsyncFunction("deleteAsync") { dbName: String ->
52 if (DATABASES.containsKey(dbName)) {
53 throw OpenDatabaseException(dbName)
54 }
55 val dbFile = File(pathForDatabaseName(dbName))
56 if (!dbFile.exists()) {
57 throw DatabaseNotFoundException(dbName)
58 }
59 if (!dbFile.delete()) {
60 throw DeleteDatabaseException(dbName)
61 }
62 }
63
64 OnDestroy {
65 DATABASES.values.forEach {
66 it?.rawQuery("SELECT crsql_finalize()", emptyArray())
67 }
68 }
69 }
70
71 private fun execute(dbName: String, queries: List<Query>, readOnly: Boolean, raw: Boolean = false): List<Any> {
72 val db = getDatabase(dbName)
73 val results = queries.map { sqlQuery ->
74 val sql = sqlQuery.sql
75 val bindArgs = convertParamsToStringArray(sqlQuery.args)
76 try {
77 if (isSelect(sql)) {
78 doSelectInBackgroundAndPossiblyThrow(sql, bindArgs, db)
79 } else { // update/insert/delete
80 if (readOnly) {
81 SQLitePluginResult(EMPTY_ROWS, EMPTY_COLUMNS, 0, 0, ReadOnlyException())
82 } else {
83 if (raw) {
84 doRawUpdate(sql, bindArgs, db)
85 } else {
86 doUpdateInBackgroundAndPossiblyThrow(sql, bindArgs, db)
87 }
88 }
89 }
90 } catch (e: Throwable) {
91 SQLitePluginResult(EMPTY_ROWS, EMPTY_COLUMNS, 0, 0, e)
92 }
93 }
94 return pluginResultsToPrimitiveData(results)
95 }
96
97 // do a update/delete/insert operation
98 private fun doUpdateInBackgroundAndPossiblyThrow(
99 sql: String,
100 bindArgs: Array<String?>?,
101 db: SQLiteDatabase
102 ): SQLitePluginResult {
103 return db.compileStatement(sql).use { statement ->
104 if (bindArgs != null) {
105 for (i in bindArgs.size downTo 1) {
106 val args = bindArgs[i - 1]
107 if (args != null) {
108 statement.bindString(i, args)
109 } else {
110 statement.bindNull(i)
111 }
112 }
113 }
114 if (isInsert(sql)) {
115 val insertId = statement.executeInsert()
116 val rowsAffected = if (insertId >= 0) 1 else 0
117 SQLitePluginResult(EMPTY_ROWS, EMPTY_COLUMNS, rowsAffected, insertId, null)
118 } else if (isDelete(sql) || isUpdate(sql)) {
119 val rowsAffected = statement.executeUpdateDelete()
120 SQLitePluginResult(EMPTY_ROWS, EMPTY_COLUMNS, rowsAffected, 0, null)
121 } else {
122 // in this case, we don't need rowsAffected or insertId, so we can have a slight
123 // perf boost by just executing the query
124 statement.execute()
125 EMPTY_RESULT
126 }
127 }
128 }
129
130 private fun doRawUpdate(
131 sql: String,
132 bindArgs: Array<String?>,
133 db: SQLiteDatabase
134 ): SQLitePluginResult {
135 return db.rawQuery(sql, bindArgs).use { cursor ->
136 val numRows = cursor.count
137 if (numRows == 0) {
138 return EMPTY_RESULT
139 }
140
141 val numColumns = cursor.columnCount
142 val columnNames = cursor.columnNames
143 val rows: Array<Array<Any?>> = Array(numRows) { arrayOfNulls(numColumns) }
144 var i = 0
145 while (cursor.moveToNext()) {
146 val row = rows[i]
147 for (j in 0 until numColumns) {
148 row[j] = getValueFromCursor(cursor, j, cursor.getType(j))
149 }
150 rows[i] = row
151 i++
152 }
153
154 if (isInsert(sql)) {
155 val rowsAffected = getRowsAffected(db)
156 val insertId = getInsertId(db)
157 SQLitePluginResult(rows, columnNames, rowsAffected, insertId, null)
158 } else if (isDelete(sql) || isUpdate(sql)) {
159 val rowsAffected = getRowsAffected(db)
160 SQLitePluginResult(rows, columnNames, rowsAffected, 0, null)
161 } else {
162 EMPTY_RESULT
163 }
164 }
165 }
166
167 private fun getRowsAffected(
168 db: SQLiteDatabase,
169 ): Int {
170 val cursor = db.rawQuery("SELECT changes() AS numRowsAffected", null)
171 val rowsAffected = if (cursor.moveToFirst()) {
172 val index = cursor.getColumnIndex("numRowsAffected")
173 cursor.getInt(index)
174 } else {
175 -1
176 }
177 cursor.close()
178 return rowsAffected
179 }
180
181 private fun getInsertId(
182 db: SQLiteDatabase,
183 ): Long {
184 val cursor = db.rawQuery("SELECT last_insert_rowid() AS insertId", null)
185 val insertId = if (cursor.moveToFirst()) {
186 val index = cursor.getColumnIndex("insertId")
187 cursor.getLong(index)
188 } else {
189 -1
190 }
191 cursor.close()
192 return insertId
193 }
194
195 // do a select operation
196 private fun doSelectInBackgroundAndPossiblyThrow(
197 sql: String,
198 bindArgs: Array<String?>,
199 db: SQLiteDatabase
200 ): SQLitePluginResult {
201 return db.rawQuery(sql, bindArgs).use { cursor ->
202 val numRows = cursor.count
203 if (numRows == 0) {
204 return EMPTY_RESULT
205 }
206 val numColumns = cursor.columnCount
207 val columnNames = cursor.columnNames
208 val rows: Array<Array<Any?>> = Array(numRows) { arrayOfNulls(numColumns) }
209 var i = 0
210 while (cursor.moveToNext()) {
211 val row = rows[i]
212 for (j in 0 until numColumns) {
213 row[j] = getValueFromCursor(cursor, j, cursor.getType(j))
214 }
215 rows[i] = row
216 i++
217 }
218 SQLitePluginResult(rows, columnNames, 0, 0, null)
219 }
220 }
221
222 private fun getValueFromCursor(cursor: Cursor, index: Int, columnType: Int): Any? {
223 return when (columnType) {
224 Cursor.FIELD_TYPE_FLOAT -> cursor.getDouble(index)
225 Cursor.FIELD_TYPE_INTEGER -> cursor.getLong(index)
226 Cursor.FIELD_TYPE_BLOB ->
227 // convert byte[] to binary string; it's good enough, because
228 // WebSQL doesn't support blobs anyway
229 String(cursor.getBlob(index))
230
231 Cursor.FIELD_TYPE_STRING -> cursor.getString(index)
232 else -> null
233 }
234 }
235
236 @Throws(IOException::class)
237 private fun pathForDatabaseName(name: String): String {
238 val directory = File("${context.filesDir}${File.separator}SQLite")
239 ensureDirExists(directory)
240 return "$directory${File.separator}$name"
241 }
242
243 @Throws(IOException::class)
244 private fun getDatabase(name: String): SQLiteDatabase {
245 var database: SQLiteDatabase? = null
246 val path = pathForDatabaseName(name)
247 if (File(path).exists()) {
248 database = DATABASES[name]
249 }
250 if (database == null) {
251 DATABASES.remove(name)
252 val config = createConfig(path)
253 database = SQLiteDatabase.openDatabase(config, null, null)
254 addUpdateListener(database)
255 DATABASES[name] = database
256 }
257 return database!!
258 }
259
260 private fun createConfig(path: String): SQLiteDatabaseConfiguration {
261 val crsqliteExtension = SQLiteCustomExtension("libcrsqlite", "sqlite3_crsqlite_init")
262 return SQLiteDatabaseConfiguration(path, SQLiteDatabase.CREATE_IF_NECESSARY, emptyList(), emptyList(), listOf(crsqliteExtension))
263 }
264
265 private fun addUpdateListener(database: SQLiteDatabase?) {
266 database?.addUpdateListener { tableName: String, operationType: Int, rowID: Int ->
267 sendEvent(
268 "onDatabaseChange",
269 bundleOf(
270 "tableName" to tableName,
271 "rowId" to rowID,
272 "typeId" to when (operationType) {
273 9 -> SqlAction.DELETE.value
274 18 -> SqlAction.INSERT.value
275 23 -> SqlAction.UPDATE.value
276 else -> SqlAction.UNKNOWN.value
277 }
278 )
279 )
280 }
281 }
282
283 internal class SQLitePluginResult(
284 val rows: Array<Array<Any?>>,
285 val columns: Array<String?>,
286 val rowsAffected: Int,
287 val insertId: Long,
288 val error: Throwable?
289 )
290
291 private class ReadOnlyException : Exception("could not prepare statement (23 not authorized)")
292 }
293