1 // 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 android.database.sqlite.SQLiteDatabase 7 import expo.modules.kotlin.exception.Exceptions 8 import expo.modules.kotlin.modules.Module 9 import expo.modules.kotlin.modules.ModuleDefinition 10 import java.io.File 11 import java.io.IOException 12 import java.util.* 13 14 private val EMPTY_ROWS = emptyArray<Array<Any?>>() 15 private val EMPTY_COLUMNS = emptyArray<String?>() 16 private val EMPTY_RESULT = SQLiteModule.SQLitePluginResult(EMPTY_ROWS, EMPTY_COLUMNS, 0, 0, null) 17 private val DATABASES: MutableMap<String, SQLiteDatabase?> = HashMap() 18 19 class SQLiteModule : Module() { 20 private val context: Context 21 get() = appContext.reactContext ?: throw Exceptions.ReactContextLost() 22 23 override fun definition() = ModuleDefinition { 24 Name("ExpoSQLite") 25 26 AsyncFunction("exec") { dbName: String, queries: List<Query>, readOnly: Boolean -> 27 val db = getDatabase(dbName) 28 val results = queries.map { sqlQuery -> 29 val sql = sqlQuery.sql 30 val bindArgs = convertParamsToStringArray(sqlQuery.args) 31 try { 32 if (isSelect(sql)) { 33 doSelectInBackgroundAndPossiblyThrow(sql, bindArgs, db) 34 } else { // update/insert/delete 35 if (readOnly) { 36 SQLitePluginResult(EMPTY_ROWS, EMPTY_COLUMNS, 0, 0, ReadOnlyException()) 37 } else { 38 doUpdateInBackgroundAndPossiblyThrow(sql, bindArgs, db) 39 } 40 } 41 } catch (e: Throwable) { 42 SQLitePluginResult(EMPTY_ROWS, EMPTY_COLUMNS, 0, 0, e) 43 } 44 } 45 return@AsyncFunction pluginResultsToPrimitiveData(results) 46 } 47 48 AsyncFunction("close") { dbName: String -> 49 DATABASES 50 .remove(dbName) 51 ?.close() 52 } 53 54 AsyncFunction("deleteAsync") { dbName: String -> 55 if (DATABASES.containsKey(dbName)) { 56 throw OpenDatabaseException(dbName) 57 } 58 val dbFile = File(pathForDatabaseName(dbName)) 59 if (!dbFile.exists()) { 60 throw DatabaseNotFoundException(dbName) 61 } 62 if (!dbFile.delete()) { 63 throw DeleteDatabaseException(dbName) 64 } 65 } 66 } 67 68 // do a update/delete/insert operation 69 private fun doUpdateInBackgroundAndPossiblyThrow( 70 sql: String, 71 bindArgs: Array<String?>?, 72 db: SQLiteDatabase 73 ): SQLitePluginResult { 74 return db.compileStatement(sql).use { statement -> 75 if (bindArgs != null) { 76 for (i in bindArgs.size downTo 1) { 77 if (bindArgs[i - 1] == null) { 78 statement.bindNull(i) 79 } else { 80 statement.bindString(i, bindArgs[i - 1]) 81 } 82 } 83 } 84 if (isInsert(sql)) { 85 val insertId = statement.executeInsert() 86 val rowsAffected = if (insertId >= 0) 1 else 0 87 SQLitePluginResult(EMPTY_ROWS, EMPTY_COLUMNS, rowsAffected, insertId, null) 88 } else if (isDelete(sql) || isUpdate(sql)) { 89 val rowsAffected = statement.executeUpdateDelete() 90 SQLitePluginResult(EMPTY_ROWS, EMPTY_COLUMNS, rowsAffected, 0, null) 91 } else { 92 // in this case, we don't need rowsAffected or insertId, so we can have a slight 93 // perf boost by just executing the query 94 statement.execute() 95 EMPTY_RESULT 96 } 97 } 98 } 99 100 // do a select operation 101 private fun doSelectInBackgroundAndPossiblyThrow( 102 sql: String, 103 bindArgs: Array<String?>, 104 db: SQLiteDatabase 105 ): SQLitePluginResult { 106 return db.rawQuery(sql, bindArgs).use { cursor -> 107 val numRows = cursor.count 108 if (numRows == 0) { 109 return EMPTY_RESULT 110 } 111 val numColumns = cursor.columnCount 112 val columnNames = cursor.columnNames 113 val rows: Array<Array<Any?>> = Array(numRows) { arrayOfNulls(numColumns) } 114 var i = 0 115 while (cursor.moveToNext()) { 116 val row = rows[i] 117 for (j in 0 until numColumns) { 118 row[j] = getValueFromCursor(cursor, j, cursor.getType(j)) 119 } 120 rows[i] = row 121 i++ 122 } 123 SQLitePluginResult(rows, columnNames, 0, 0, null) 124 } 125 } 126 127 private fun getValueFromCursor(cursor: Cursor, index: Int, columnType: Int): Any? { 128 return when (columnType) { 129 Cursor.FIELD_TYPE_FLOAT -> cursor.getDouble(index) 130 Cursor.FIELD_TYPE_INTEGER -> cursor.getLong(index) 131 Cursor.FIELD_TYPE_BLOB -> 132 // convert byte[] to binary string; it's good enough, because 133 // WebSQL doesn't support blobs anyway 134 String(cursor.getBlob(index)) 135 136 Cursor.FIELD_TYPE_STRING -> cursor.getString(index) 137 else -> null 138 } 139 } 140 141 @Throws(IOException::class) 142 private fun pathForDatabaseName(name: String): String { 143 val directory = File("${context.filesDir}${File.separator}SQLite") 144 ensureDirExists(directory) 145 return "$directory${File.separator}$name" 146 } 147 148 @Throws(IOException::class) 149 private fun getDatabase(name: String): SQLiteDatabase { 150 var database: SQLiteDatabase? = null 151 val path = pathForDatabaseName(name) 152 if (File(path).exists()) { 153 database = DATABASES[name] 154 } 155 if (database == null) { 156 DATABASES.remove(name) 157 database = SQLiteDatabase.openOrCreateDatabase(path, null) 158 DATABASES[name] = database 159 } 160 return database!! 161 } 162 163 internal class SQLitePluginResult( 164 val rows: Array<Array<Any?>>, 165 val columns: Array<String?>, 166 val rowsAffected: Int, 167 val insertId: Long, 168 val error: Throwable? 169 ) 170 171 private class ReadOnlyException : Exception("could not prepare statement (23 not authorized)") 172 } 173