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 Function("closeSync") { dbName: String -> 55 DATABASES 56 .remove(dbName) 57 ?.close() 58 } 59 60 AsyncFunction("deleteAsync") { dbName: String -> 61 if (DATABASES.containsKey(dbName)) { 62 throw OpenDatabaseException(dbName) 63 } 64 val dbFile = File(pathForDatabaseName(dbName)) 65 if (!dbFile.exists()) { 66 throw DatabaseNotFoundException(dbName) 67 } 68 if (!dbFile.delete()) { 69 throw DeleteDatabaseException(dbName) 70 } 71 } 72 } 73 74 // do a update/delete/insert operation 75 private fun doUpdateInBackgroundAndPossiblyThrow( 76 sql: String, 77 bindArgs: Array<String?>?, 78 db: SQLiteDatabase 79 ): SQLitePluginResult { 80 return db.compileStatement(sql).use { statement -> 81 if (bindArgs != null) { 82 for (i in bindArgs.size downTo 1) { 83 if (bindArgs[i - 1] == null) { 84 statement.bindNull(i) 85 } else { 86 statement.bindString(i, bindArgs[i - 1]) 87 } 88 } 89 } 90 if (isInsert(sql)) { 91 val insertId = statement.executeInsert() 92 val rowsAffected = if (insertId >= 0) 1 else 0 93 SQLitePluginResult(EMPTY_ROWS, EMPTY_COLUMNS, rowsAffected, insertId, null) 94 } else if (isDelete(sql) || isUpdate(sql)) { 95 val rowsAffected = statement.executeUpdateDelete() 96 SQLitePluginResult(EMPTY_ROWS, EMPTY_COLUMNS, rowsAffected, 0, null) 97 } else { 98 // in this case, we don't need rowsAffected or insertId, so we can have a slight 99 // perf boost by just executing the query 100 statement.execute() 101 EMPTY_RESULT 102 } 103 } 104 } 105 106 // do a select operation 107 private fun doSelectInBackgroundAndPossiblyThrow( 108 sql: String, 109 bindArgs: Array<String?>, 110 db: SQLiteDatabase 111 ): SQLitePluginResult { 112 return db.rawQuery(sql, bindArgs).use { cursor -> 113 val numRows = cursor.count 114 if (numRows == 0) { 115 return EMPTY_RESULT 116 } 117 val numColumns = cursor.columnCount 118 val columnNames = cursor.columnNames 119 val rows: Array<Array<Any?>> = Array(numRows) { arrayOfNulls(numColumns) } 120 var i = 0 121 while (cursor.moveToNext()) { 122 val row = rows[i] 123 for (j in 0 until numColumns) { 124 row[j] = getValueFromCursor(cursor, j, cursor.getType(j)) 125 } 126 rows[i] = row 127 i++ 128 } 129 SQLitePluginResult(rows, columnNames, 0, 0, null) 130 } 131 } 132 133 private fun getValueFromCursor(cursor: Cursor, index: Int, columnType: Int): Any? { 134 return when (columnType) { 135 Cursor.FIELD_TYPE_FLOAT -> cursor.getDouble(index) 136 Cursor.FIELD_TYPE_INTEGER -> cursor.getLong(index) 137 Cursor.FIELD_TYPE_BLOB -> 138 // convert byte[] to binary string; it's good enough, because 139 // WebSQL doesn't support blobs anyway 140 String(cursor.getBlob(index)) 141 142 Cursor.FIELD_TYPE_STRING -> cursor.getString(index) 143 else -> null 144 } 145 } 146 147 @Throws(IOException::class) 148 private fun pathForDatabaseName(name: String): String { 149 val directory = File("${context.filesDir}${File.separator}SQLite") 150 ensureDirExists(directory) 151 return "$directory${File.separator}$name" 152 } 153 154 @Throws(IOException::class) 155 private fun getDatabase(name: String): SQLiteDatabase { 156 var database: SQLiteDatabase? = null 157 val path = pathForDatabaseName(name) 158 if (File(path).exists()) { 159 database = DATABASES[name] 160 } 161 if (database == null) { 162 DATABASES.remove(name) 163 database = SQLiteDatabase.openOrCreateDatabase(path, null) 164 DATABASES[name] = database 165 } 166 return database!! 167 } 168 169 internal class SQLitePluginResult( 170 val rows: Array<Array<Any?>>, 171 val columns: Array<String?>, 172 val rowsAffected: Int, 173 val insertId: Long, 174 val error: Throwable? 175 ) 176 177 private class ReadOnlyException : Exception("could not prepare statement (23 not authorized)") 178 } 179