<lambda>null14c04ad3dSBartosz Sofiński // Copyright 2015-present 650 Industries. All rights reserved.
24c04ad3dSBartosz Sofiński package expo.modules.sqlite
34c04ad3dSBartosz Sofiński 
44c04ad3dSBartosz Sofiński import android.content.Context
54c04ad3dSBartosz Sofiński import android.database.Cursor
6*4b361cb0SAlan Hughes import androidx.core.os.bundleOf
709ee9580SAlan Hughes import expo.modules.kotlin.exception.Exceptions
809ee9580SAlan Hughes import expo.modules.kotlin.modules.Module
909ee9580SAlan Hughes import expo.modules.kotlin.modules.ModuleDefinition
10*4b361cb0SAlan Hughes import io.expo.android.database.sqlite.SQLiteCustomExtension
11*4b361cb0SAlan Hughes import io.expo.android.database.sqlite.SQLiteDatabase
12*4b361cb0SAlan Hughes import io.expo.android.database.sqlite.SQLiteDatabaseConfiguration
134c04ad3dSBartosz Sofiński import java.io.File
144c04ad3dSBartosz Sofiński import java.io.IOException
154c04ad3dSBartosz Sofiński import java.util.*
164c04ad3dSBartosz Sofiński 
1709ee9580SAlan Hughes private val EMPTY_ROWS = emptyArray<Array<Any?>>()
1809ee9580SAlan Hughes private val EMPTY_COLUMNS = emptyArray<String?>()
194c04ad3dSBartosz Sofiński private val EMPTY_RESULT = SQLiteModule.SQLitePluginResult(EMPTY_ROWS, EMPTY_COLUMNS, 0, 0, null)
204c04ad3dSBartosz Sofiński private val DATABASES: MutableMap<String, SQLiteDatabase?> = HashMap()
214c04ad3dSBartosz Sofiński 
2209ee9580SAlan Hughes class SQLiteModule : Module() {
2309ee9580SAlan Hughes   private val context: Context
2409ee9580SAlan Hughes     get() = appContext.reactContext ?: throw Exceptions.ReactContextLost()
254c04ad3dSBartosz Sofiński 
2609ee9580SAlan Hughes   override fun definition() = ModuleDefinition {
2709ee9580SAlan Hughes     Name("ExpoSQLite")
2809ee9580SAlan Hughes 
29*4b361cb0SAlan Hughes     Events("onDatabaseChange")
30*4b361cb0SAlan Hughes 
3109ee9580SAlan Hughes     AsyncFunction("exec") { dbName: String, queries: List<Query>, readOnly: Boolean ->
3250e16f8cSAlan Hughes       return@AsyncFunction execute(dbName, queries, readOnly)
334c04ad3dSBartosz Sofiński     }
3450e16f8cSAlan Hughes 
3550e16f8cSAlan Hughes     AsyncFunction("execRawQuery") { dbName: String, queries: List<Query>, readOnly: Boolean ->
3650e16f8cSAlan Hughes       return@AsyncFunction execute(dbName, queries, readOnly, raw = true)
374c04ad3dSBartosz Sofiński     }
384c04ad3dSBartosz Sofiński 
3909ee9580SAlan Hughes     AsyncFunction("close") { dbName: String ->
404c04ad3dSBartosz Sofiński       DATABASES
414c04ad3dSBartosz Sofiński         .remove(dbName)
424c04ad3dSBartosz Sofiński         ?.close()
434c04ad3dSBartosz Sofiński     }
444c04ad3dSBartosz Sofiński 
456c4baee8SAlan Hughes     Function("closeSync") { dbName: String ->
466c4baee8SAlan Hughes       DATABASES
476c4baee8SAlan Hughes         .remove(dbName)
486c4baee8SAlan Hughes         ?.close()
496c4baee8SAlan Hughes     }
506c4baee8SAlan Hughes 
5109ee9580SAlan Hughes     AsyncFunction("deleteAsync") { dbName: String ->
523f4e5fe5SKudo Chien       if (DATABASES.containsKey(dbName)) {
5309ee9580SAlan Hughes         throw OpenDatabaseException(dbName)
543f4e5fe5SKudo Chien       }
553f4e5fe5SKudo Chien       val dbFile = File(pathForDatabaseName(dbName))
563f4e5fe5SKudo Chien       if (!dbFile.exists()) {
5709ee9580SAlan Hughes         throw DatabaseNotFoundException(dbName)
583f4e5fe5SKudo Chien       }
593f4e5fe5SKudo Chien       if (!dbFile.delete()) {
6009ee9580SAlan Hughes         throw DeleteDatabaseException(dbName)
613f4e5fe5SKudo Chien       }
6209ee9580SAlan Hughes     }
63*4b361cb0SAlan Hughes 
64*4b361cb0SAlan Hughes     OnDestroy {
65*4b361cb0SAlan Hughes       DATABASES.values.forEach {
66*4b361cb0SAlan Hughes         it?.rawQuery("SELECT crsql_finalize()", emptyArray())
67*4b361cb0SAlan Hughes       }
68*4b361cb0SAlan Hughes     }
693f4e5fe5SKudo Chien   }
703f4e5fe5SKudo Chien 
7150e16f8cSAlan Hughes   private fun execute(dbName: String, queries: List<Query>, readOnly: Boolean, raw: Boolean = false): List<Any> {
7250e16f8cSAlan Hughes     val db = getDatabase(dbName)
7350e16f8cSAlan Hughes     val results = queries.map { sqlQuery ->
7450e16f8cSAlan Hughes       val sql = sqlQuery.sql
7550e16f8cSAlan Hughes       val bindArgs = convertParamsToStringArray(sqlQuery.args)
7650e16f8cSAlan Hughes       try {
7750e16f8cSAlan Hughes         if (isSelect(sql)) {
7850e16f8cSAlan Hughes           doSelectInBackgroundAndPossiblyThrow(sql, bindArgs, db)
7950e16f8cSAlan Hughes         } else { // update/insert/delete
8050e16f8cSAlan Hughes           if (readOnly) {
8150e16f8cSAlan Hughes             SQLitePluginResult(EMPTY_ROWS, EMPTY_COLUMNS, 0, 0, ReadOnlyException())
8250e16f8cSAlan Hughes           } else {
8350e16f8cSAlan Hughes             if (raw) {
8450e16f8cSAlan Hughes               doRawUpdate(sql, bindArgs, db)
8550e16f8cSAlan Hughes             } else {
8650e16f8cSAlan Hughes               doUpdateInBackgroundAndPossiblyThrow(sql, bindArgs, db)
8750e16f8cSAlan Hughes             }
8850e16f8cSAlan Hughes           }
8950e16f8cSAlan Hughes         }
9050e16f8cSAlan Hughes       } catch (e: Throwable) {
9150e16f8cSAlan Hughes         SQLitePluginResult(EMPTY_ROWS, EMPTY_COLUMNS, 0, 0, e)
9250e16f8cSAlan Hughes       }
9350e16f8cSAlan Hughes     }
9450e16f8cSAlan Hughes     return pluginResultsToPrimitiveData(results)
9550e16f8cSAlan Hughes   }
9650e16f8cSAlan Hughes 
974c04ad3dSBartosz Sofiński   // do a update/delete/insert operation
984c04ad3dSBartosz Sofiński   private fun doUpdateInBackgroundAndPossiblyThrow(
994c04ad3dSBartosz Sofiński     sql: String,
1004c04ad3dSBartosz Sofiński     bindArgs: Array<String?>?,
1014c04ad3dSBartosz Sofiński     db: SQLiteDatabase
1024c04ad3dSBartosz Sofiński   ): SQLitePluginResult {
1034c04ad3dSBartosz Sofiński     return db.compileStatement(sql).use { statement ->
1044c04ad3dSBartosz Sofiński       if (bindArgs != null) {
1054c04ad3dSBartosz Sofiński         for (i in bindArgs.size downTo 1) {
1064e419217SAlan Hughes           val args = bindArgs[i - 1]
1074e419217SAlan Hughes           if (args != null) {
1084e419217SAlan Hughes             statement.bindString(i, args)
1094c04ad3dSBartosz Sofiński           } else {
1104e419217SAlan Hughes             statement.bindNull(i)
1114c04ad3dSBartosz Sofiński           }
1124c04ad3dSBartosz Sofiński         }
1134c04ad3dSBartosz Sofiński       }
1144c04ad3dSBartosz Sofiński       if (isInsert(sql)) {
1154c04ad3dSBartosz Sofiński         val insertId = statement.executeInsert()
1164c04ad3dSBartosz Sofiński         val rowsAffected = if (insertId >= 0) 1 else 0
1174c04ad3dSBartosz Sofiński         SQLitePluginResult(EMPTY_ROWS, EMPTY_COLUMNS, rowsAffected, insertId, null)
1184c04ad3dSBartosz Sofiński       } else if (isDelete(sql) || isUpdate(sql)) {
1194c04ad3dSBartosz Sofiński         val rowsAffected = statement.executeUpdateDelete()
1204c04ad3dSBartosz Sofiński         SQLitePluginResult(EMPTY_ROWS, EMPTY_COLUMNS, rowsAffected, 0, null)
1214c04ad3dSBartosz Sofiński       } else {
1224c04ad3dSBartosz Sofiński         // in this case, we don't need rowsAffected or insertId, so we can have a slight
1234c04ad3dSBartosz Sofiński         // perf boost by just executing the query
1244c04ad3dSBartosz Sofiński         statement.execute()
1254c04ad3dSBartosz Sofiński         EMPTY_RESULT
1264c04ad3dSBartosz Sofiński       }
1274c04ad3dSBartosz Sofiński     }
1284c04ad3dSBartosz Sofiński   }
1294c04ad3dSBartosz Sofiński 
13050e16f8cSAlan Hughes   private fun doRawUpdate(
13150e16f8cSAlan Hughes     sql: String,
13250e16f8cSAlan Hughes     bindArgs: Array<String?>,
13350e16f8cSAlan Hughes     db: SQLiteDatabase
13450e16f8cSAlan Hughes   ): SQLitePluginResult {
13550e16f8cSAlan Hughes     return db.rawQuery(sql, bindArgs).use { cursor ->
13650e16f8cSAlan Hughes       val numRows = cursor.count
13750e16f8cSAlan Hughes       if (numRows == 0) {
13850e16f8cSAlan Hughes         return EMPTY_RESULT
13950e16f8cSAlan Hughes       }
14050e16f8cSAlan Hughes 
14150e16f8cSAlan Hughes       val numColumns = cursor.columnCount
14250e16f8cSAlan Hughes       val columnNames = cursor.columnNames
14350e16f8cSAlan Hughes       val rows: Array<Array<Any?>> = Array(numRows) { arrayOfNulls(numColumns) }
14450e16f8cSAlan Hughes       var i = 0
14550e16f8cSAlan Hughes       while (cursor.moveToNext()) {
14650e16f8cSAlan Hughes         val row = rows[i]
14750e16f8cSAlan Hughes         for (j in 0 until numColumns) {
14850e16f8cSAlan Hughes           row[j] = getValueFromCursor(cursor, j, cursor.getType(j))
14950e16f8cSAlan Hughes         }
15050e16f8cSAlan Hughes         rows[i] = row
15150e16f8cSAlan Hughes         i++
15250e16f8cSAlan Hughes       }
15350e16f8cSAlan Hughes 
15450e16f8cSAlan Hughes       if (isInsert(sql)) {
155*4b361cb0SAlan Hughes         val rowsAffected = getRowsAffected(db)
156*4b361cb0SAlan Hughes         val insertId = getInsertId(db)
15750e16f8cSAlan Hughes         SQLitePluginResult(rows, columnNames, rowsAffected, insertId, null)
15850e16f8cSAlan Hughes       } else if (isDelete(sql) || isUpdate(sql)) {
159*4b361cb0SAlan Hughes         val rowsAffected = getRowsAffected(db)
16050e16f8cSAlan Hughes         SQLitePluginResult(rows, columnNames, rowsAffected, 0, null)
16150e16f8cSAlan Hughes       } else {
16250e16f8cSAlan Hughes         EMPTY_RESULT
16350e16f8cSAlan Hughes       }
16450e16f8cSAlan Hughes     }
16550e16f8cSAlan Hughes   }
16650e16f8cSAlan Hughes 
16750e16f8cSAlan Hughes   private fun getRowsAffected(
16850e16f8cSAlan Hughes     db: SQLiteDatabase,
169*4b361cb0SAlan Hughes   ): Int {
17050e16f8cSAlan Hughes     val cursor = db.rawQuery("SELECT changes() AS numRowsAffected", null)
17150e16f8cSAlan Hughes     val rowsAffected = if (cursor.moveToFirst()) {
17250e16f8cSAlan Hughes       val index = cursor.getColumnIndex("numRowsAffected")
17350e16f8cSAlan Hughes       cursor.getInt(index)
17450e16f8cSAlan Hughes     } else {
17550e16f8cSAlan Hughes       -1
17650e16f8cSAlan Hughes     }
177*4b361cb0SAlan Hughes     cursor.close()
178*4b361cb0SAlan Hughes     return rowsAffected
17950e16f8cSAlan Hughes   }
18050e16f8cSAlan Hughes 
18150e16f8cSAlan Hughes   private fun getInsertId(
18250e16f8cSAlan Hughes     db: SQLiteDatabase,
183*4b361cb0SAlan Hughes   ): Long {
18450e16f8cSAlan Hughes     val cursor = db.rawQuery("SELECT last_insert_rowid() AS insertId", null)
18550e16f8cSAlan Hughes     val insertId = if (cursor.moveToFirst()) {
18650e16f8cSAlan Hughes       val index = cursor.getColumnIndex("insertId")
18750e16f8cSAlan Hughes       cursor.getLong(index)
18850e16f8cSAlan Hughes     } else {
18950e16f8cSAlan Hughes       -1
19050e16f8cSAlan Hughes     }
191*4b361cb0SAlan Hughes     cursor.close()
192*4b361cb0SAlan Hughes     return insertId
19350e16f8cSAlan Hughes   }
19450e16f8cSAlan Hughes 
1954c04ad3dSBartosz Sofiński   // do a select operation
1964c04ad3dSBartosz Sofiński   private fun doSelectInBackgroundAndPossiblyThrow(
1974c04ad3dSBartosz Sofiński     sql: String,
1984c04ad3dSBartosz Sofiński     bindArgs: Array<String?>,
1994c04ad3dSBartosz Sofiński     db: SQLiteDatabase
2004c04ad3dSBartosz Sofiński   ): SQLitePluginResult {
2014c04ad3dSBartosz Sofiński     return db.rawQuery(sql, bindArgs).use { cursor ->
2024c04ad3dSBartosz Sofiński       val numRows = cursor.count
2034c04ad3dSBartosz Sofiński       if (numRows == 0) {
2044c04ad3dSBartosz Sofiński         return EMPTY_RESULT
2054c04ad3dSBartosz Sofiński       }
2064c04ad3dSBartosz Sofiński       val numColumns = cursor.columnCount
2074c04ad3dSBartosz Sofiński       val columnNames = cursor.columnNames
2084c04ad3dSBartosz Sofiński       val rows: Array<Array<Any?>> = Array(numRows) { arrayOfNulls(numColumns) }
2094c04ad3dSBartosz Sofiński       var i = 0
2104c04ad3dSBartosz Sofiński       while (cursor.moveToNext()) {
2114c04ad3dSBartosz Sofiński         val row = rows[i]
2124c04ad3dSBartosz Sofiński         for (j in 0 until numColumns) {
2134c04ad3dSBartosz Sofiński           row[j] = getValueFromCursor(cursor, j, cursor.getType(j))
2144c04ad3dSBartosz Sofiński         }
2154c04ad3dSBartosz Sofiński         rows[i] = row
2164c04ad3dSBartosz Sofiński         i++
2174c04ad3dSBartosz Sofiński       }
2184c04ad3dSBartosz Sofiński       SQLitePluginResult(rows, columnNames, 0, 0, null)
2194c04ad3dSBartosz Sofiński     }
2204c04ad3dSBartosz Sofiński   }
2214c04ad3dSBartosz Sofiński 
2224c04ad3dSBartosz Sofiński   private fun getValueFromCursor(cursor: Cursor, index: Int, columnType: Int): Any? {
2234c04ad3dSBartosz Sofiński     return when (columnType) {
2244c04ad3dSBartosz Sofiński       Cursor.FIELD_TYPE_FLOAT -> cursor.getDouble(index)
2254c04ad3dSBartosz Sofiński       Cursor.FIELD_TYPE_INTEGER -> cursor.getLong(index)
2264c04ad3dSBartosz Sofiński       Cursor.FIELD_TYPE_BLOB ->
2274c04ad3dSBartosz Sofiński         // convert byte[] to binary string; it's good enough, because
2284c04ad3dSBartosz Sofiński         // WebSQL doesn't support blobs anyway
2294c04ad3dSBartosz Sofiński         String(cursor.getBlob(index))
23009ee9580SAlan Hughes 
2314c04ad3dSBartosz Sofiński       Cursor.FIELD_TYPE_STRING -> cursor.getString(index)
2324c04ad3dSBartosz Sofiński       else -> null
2334c04ad3dSBartosz Sofiński     }
2344c04ad3dSBartosz Sofiński   }
2354c04ad3dSBartosz Sofiński 
2364c04ad3dSBartosz Sofiński   @Throws(IOException::class)
2374c04ad3dSBartosz Sofiński   private fun pathForDatabaseName(name: String): String {
23809ee9580SAlan Hughes     val directory = File("${context.filesDir}${File.separator}SQLite")
2394c04ad3dSBartosz Sofiński     ensureDirExists(directory)
2404c04ad3dSBartosz Sofiński     return "$directory${File.separator}$name"
2414c04ad3dSBartosz Sofiński   }
2424c04ad3dSBartosz Sofiński 
2434c04ad3dSBartosz Sofiński   @Throws(IOException::class)
2444c04ad3dSBartosz Sofiński   private fun getDatabase(name: String): SQLiteDatabase {
2454c04ad3dSBartosz Sofiński     var database: SQLiteDatabase? = null
2464c04ad3dSBartosz Sofiński     val path = pathForDatabaseName(name)
2474c04ad3dSBartosz Sofiński     if (File(path).exists()) {
2484c04ad3dSBartosz Sofiński       database = DATABASES[name]
2494c04ad3dSBartosz Sofiński     }
2504c04ad3dSBartosz Sofiński     if (database == null) {
2514c04ad3dSBartosz Sofiński       DATABASES.remove(name)
252*4b361cb0SAlan Hughes       val config = createConfig(path)
253*4b361cb0SAlan Hughes       database = SQLiteDatabase.openDatabase(config, null, null)
254*4b361cb0SAlan Hughes       addUpdateListener(database)
2554c04ad3dSBartosz Sofiński       DATABASES[name] = database
2564c04ad3dSBartosz Sofiński     }
2574c04ad3dSBartosz Sofiński     return database!!
2584c04ad3dSBartosz Sofiński   }
2594c04ad3dSBartosz Sofiński 
260*4b361cb0SAlan Hughes   private fun createConfig(path: String): SQLiteDatabaseConfiguration {
261*4b361cb0SAlan Hughes     val crsqliteExtension = SQLiteCustomExtension("libcrsqlite", "sqlite3_crsqlite_init")
262*4b361cb0SAlan Hughes     return SQLiteDatabaseConfiguration(path, SQLiteDatabase.CREATE_IF_NECESSARY, emptyList(), emptyList(), listOf(crsqliteExtension))
263*4b361cb0SAlan Hughes   }
264*4b361cb0SAlan Hughes 
265*4b361cb0SAlan Hughes   private fun addUpdateListener(database: SQLiteDatabase?) {
266*4b361cb0SAlan Hughes     database?.addUpdateListener { tableName: String, operationType: Int, rowID: Int ->
267*4b361cb0SAlan Hughes       sendEvent(
268*4b361cb0SAlan Hughes         "onDatabaseChange",
269*4b361cb0SAlan Hughes         bundleOf(
270*4b361cb0SAlan Hughes           "tableName" to tableName,
271*4b361cb0SAlan Hughes           "rowId" to rowID,
272*4b361cb0SAlan Hughes           "typeId" to when (operationType) {
273*4b361cb0SAlan Hughes             9 -> SqlAction.DELETE.value
274*4b361cb0SAlan Hughes             18 -> SqlAction.INSERT.value
275*4b361cb0SAlan Hughes             23 -> SqlAction.UPDATE.value
276*4b361cb0SAlan Hughes             else -> SqlAction.UNKNOWN.value
277*4b361cb0SAlan Hughes           }
278*4b361cb0SAlan Hughes         )
279*4b361cb0SAlan Hughes       )
280*4b361cb0SAlan Hughes     }
281*4b361cb0SAlan Hughes   }
282*4b361cb0SAlan Hughes 
2834c04ad3dSBartosz Sofiński   internal class SQLitePluginResult(
2844c04ad3dSBartosz Sofiński     val rows: Array<Array<Any?>>,
2854c04ad3dSBartosz Sofiński     val columns: Array<String?>,
2864c04ad3dSBartosz Sofiński     val rowsAffected: Int,
2874c04ad3dSBartosz Sofiński     val insertId: Long,
2884c04ad3dSBartosz Sofiński     val error: Throwable?
2894c04ad3dSBartosz Sofiński   )
2904c04ad3dSBartosz Sofiński 
2914c04ad3dSBartosz Sofiński   private class ReadOnlyException : Exception("could not prepare statement (23 not authorized)")
2924c04ad3dSBartosz Sofiński }
293