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