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