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