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