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