expo-sqlite 11.7.1 → 12.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/android/CMakeLists.txt +35 -0
  3. package/android/build.gradle +114 -29
  4. package/android/src/main/cpp/NativeDatabaseBinding.cpp +142 -0
  5. package/android/src/main/cpp/NativeDatabaseBinding.h +75 -0
  6. package/android/src/main/cpp/NativeStatementBinding.cpp +146 -0
  7. package/android/src/main/cpp/NativeStatementBinding.h +95 -0
  8. package/android/src/main/cpp/SQLite3Main.cpp +15 -0
  9. package/android/src/main/cpp/SQLite3Wrapper.cpp +223 -0
  10. package/android/src/main/cpp/SQLite3Wrapper.h +55 -0
  11. package/android/src/main/java/expo/modules/sqlite/NativeDatabase.kt +11 -0
  12. package/android/src/main/java/expo/modules/sqlite/NativeDatabaseBinding.kt +79 -0
  13. package/android/src/main/java/expo/modules/sqlite/NativeStatement.kt +11 -0
  14. package/android/src/main/java/expo/modules/sqlite/NativeStatementBinding.kt +40 -0
  15. package/android/src/main/java/expo/modules/sqlite/SQLExceptions.kt +17 -6
  16. package/android/src/main/java/expo/modules/sqlite/SQLRecords.kt +16 -3
  17. package/android/src/main/java/expo/modules/sqlite/SQLite3Wrapper.kt +51 -0
  18. package/android/src/main/java/expo/modules/sqlite/SQLiteHelpers.kt +0 -79
  19. package/android/src/main/java/expo/modules/sqlite/SQLiteModule.kt +31 -222
  20. package/android/src/main/java/expo/modules/sqlite/SQLiteModuleNext.kt +447 -0
  21. package/android/src/main/java/expo/modules/sqlite/SQLiteOptions.kt +20 -0
  22. package/build/next/Database.d.ts +254 -0
  23. package/build/next/Database.d.ts.map +1 -0
  24. package/build/next/Database.js +324 -0
  25. package/build/next/Database.js.map +1 -0
  26. package/build/next/ExpoSQLiteNext.d.ts +12 -0
  27. package/build/next/ExpoSQLiteNext.d.ts.map +1 -0
  28. package/build/next/ExpoSQLiteNext.js +26 -0
  29. package/build/next/ExpoSQLiteNext.js.map +1 -0
  30. package/build/next/ExpoSQLiteNext.native.d.ts +3 -0
  31. package/build/next/ExpoSQLiteNext.native.d.ts.map +1 -0
  32. package/build/next/ExpoSQLiteNext.native.js +3 -0
  33. package/build/next/ExpoSQLiteNext.native.js.map +1 -0
  34. package/build/next/NativeDatabase.d.ts +44 -0
  35. package/build/next/NativeDatabase.d.ts.map +1 -0
  36. package/build/next/NativeDatabase.js +2 -0
  37. package/build/next/NativeDatabase.js.map +1 -0
  38. package/build/next/NativeStatement.d.ts +68 -0
  39. package/build/next/NativeStatement.d.ts.map +1 -0
  40. package/build/next/NativeStatement.js +2 -0
  41. package/build/next/NativeStatement.js.map +1 -0
  42. package/build/next/Statement.d.ts +120 -0
  43. package/build/next/Statement.d.ts.map +1 -0
  44. package/build/next/Statement.js +135 -0
  45. package/build/next/Statement.js.map +1 -0
  46. package/build/next/hooks.d.ts +35 -0
  47. package/build/next/hooks.d.ts.map +1 -0
  48. package/build/next/hooks.js +57 -0
  49. package/build/next/hooks.js.map +1 -0
  50. package/build/next/index.d.ts +4 -0
  51. package/build/next/index.d.ts.map +1 -0
  52. package/build/next/index.js +4 -0
  53. package/build/next/index.js.map +1 -0
  54. package/expo-module.config.json +2 -2
  55. package/ios/CRSQLiteLoader.h +3 -1
  56. package/ios/CRSQLiteLoader.m +11 -6
  57. package/ios/Exceptions.swift +13 -0
  58. package/ios/ExpoSQLite.podspec +2 -2
  59. package/ios/NativeDatabase.swift +26 -0
  60. package/ios/NativeStatement.swift +13 -0
  61. package/ios/SQLAction.swift +23 -0
  62. package/ios/SQLiteModule.swift +0 -53
  63. package/ios/SQLiteModuleNext.swift +538 -0
  64. package/ios/SQLiteOptions.swift +26 -0
  65. package/next.d.ts +1 -0
  66. package/next.js +1 -0
  67. package/package.json +4 -2
  68. package/src/next/Database.ts +446 -0
  69. package/src/next/ExpoSQLiteNext.native.ts +2 -0
  70. package/src/next/ExpoSQLiteNext.ts +34 -0
  71. package/src/next/NativeDatabase.ts +58 -0
  72. package/src/next/NativeStatement.ts +85 -0
  73. package/src/next/Statement.ts +243 -0
  74. package/src/next/hooks.tsx +108 -0
  75. package/src/next/index.ts +3 -0
@@ -22,82 +22,3 @@ internal fun ensureDirExists(dir: File): File {
22
22
  }
23
23
  return dir
24
24
  }
25
-
26
- internal fun pluginResultsToPrimitiveData(results: List<SQLiteModule.SQLitePluginResult>): List<Any> {
27
- return results.map { convertPluginResultToArray(it) }
28
- }
29
-
30
- private fun convertPluginResultToArray(result: SQLiteModule.SQLitePluginResult): List<Any?> {
31
- val rowsContent = result.rows.map { row ->
32
- row.map { value ->
33
- when (value) {
34
- null -> null
35
- is String -> value
36
- is Boolean -> value
37
- else -> (value as Number).toDouble()
38
- }
39
- }
40
- }
41
-
42
- return arrayListOf(
43
- result.error?.message,
44
- result.insertId.toInt(),
45
- result.rowsAffected,
46
- result.columns,
47
- rowsContent
48
- )
49
- }
50
-
51
- private fun isPragma(str: String): Boolean {
52
- return startsWithCaseInsensitive(str, "pragma")
53
- }
54
-
55
- private fun isSelectCTE(str: String): Boolean {
56
- return startsWithCaseInsensitive(str, "with") && containsCaseInsensitive(str, "select")
57
- }
58
-
59
- private fun isPragmaReadOnly(str: String): Boolean {
60
- return isPragma(str) && !str.contains('=')
61
- }
62
-
63
- internal fun isSelect(str: String): Boolean {
64
- return startsWithCaseInsensitive(str, "select") || isSelectCTE(str) || isPragmaReadOnly(str)
65
- }
66
-
67
- internal fun isInsert(str: String): Boolean {
68
- return startsWithCaseInsensitive(str, "insert")
69
- }
70
-
71
- internal fun isUpdate(str: String): Boolean {
72
- return startsWithCaseInsensitive(str, "update")
73
- }
74
-
75
- internal fun isDelete(str: String): Boolean {
76
- return startsWithCaseInsensitive(str, "delete")
77
- }
78
-
79
- private fun startsWithCaseInsensitive(str: String, substr: String): Boolean {
80
- return str.trimStart().startsWith(substr, true)
81
- }
82
-
83
- private fun containsCaseInsensitive(str: String, substr: String): Boolean {
84
- return str.trimStart().contains(substr, true)
85
- }
86
-
87
- internal fun convertParamsToStringArray(paramArrayArg: List<Any?>): Array<String?> {
88
- return paramArrayArg.map { param ->
89
- when (param) {
90
- is String -> unescapeBlob(param)
91
- is Boolean -> if (param) "1" else "0"
92
- is Double -> param.toString()
93
- null -> null
94
- else -> throw ClassCastException("Could not find proper SQLite data type for argument: $")
95
- }
96
- }.toTypedArray()
97
- }
98
-
99
- private fun unescapeBlob(str: String): String {
100
- return str.replace("\u0001\u0001", "\u0000")
101
- .replace("\u0001\u0002", "\u0001")
102
- .replace("\u0002\u0002", "\u0002")
103
- }
@@ -2,237 +2,63 @@
2
2
  package expo.modules.sqlite
3
3
 
4
4
  import android.content.Context
5
- import android.database.Cursor
6
- import androidx.core.os.bundleOf
5
+ import androidx.collection.ArrayMap
7
6
  import expo.modules.kotlin.exception.Exceptions
8
7
  import expo.modules.kotlin.modules.Module
9
8
  import expo.modules.kotlin.modules.ModuleDefinition
10
- import io.expo.android.database.sqlite.SQLiteCustomExtension
11
- import io.expo.android.database.sqlite.SQLiteDatabase
12
- import io.expo.android.database.sqlite.SQLiteDatabaseConfiguration
13
9
  import java.io.File
14
10
  import java.io.IOException
15
11
  import java.util.*
16
12
 
17
- private val EMPTY_ROWS = emptyArray<Array<Any?>>()
18
- private val EMPTY_COLUMNS = emptyArray<String?>()
19
- private val EMPTY_RESULT = SQLiteModule.SQLitePluginResult(EMPTY_ROWS, EMPTY_COLUMNS, 0, 0, null)
20
- private val DATABASES: MutableMap<String, SQLiteDatabase?> = HashMap()
21
-
22
13
  class SQLiteModule : Module() {
14
+ private val cachedDatabase = ArrayMap<String, SQLite3Wrapper>()
15
+
23
16
  private val context: Context
24
17
  get() = appContext.reactContext ?: throw Exceptions.ReactContextLost()
25
18
 
26
19
  override fun definition() = ModuleDefinition {
27
20
  Name("ExpoSQLite")
28
21
 
29
- Events("onDatabaseChange")
30
-
31
22
  AsyncFunction("exec") { dbName: String, queries: List<Query>, readOnly: Boolean ->
32
23
  return@AsyncFunction execute(dbName, queries, readOnly)
33
24
  }
34
25
 
35
26
  AsyncFunction("execRawQuery") { dbName: String, queries: List<Query>, readOnly: Boolean ->
36
- return@AsyncFunction execute(dbName, queries, readOnly, raw = true)
27
+ return@AsyncFunction execute(dbName, queries, readOnly)
37
28
  }
38
29
 
39
30
  AsyncFunction("close") { dbName: String ->
40
- DATABASES
31
+ cachedDatabase
41
32
  .remove(dbName)
42
- ?.close()
33
+ ?.sqlite3_close()
43
34
  }
44
35
 
45
36
  Function("closeSync") { dbName: String ->
46
- DATABASES
37
+ cachedDatabase
47
38
  .remove(dbName)
48
- ?.close()
39
+ ?.sqlite3_close()
49
40
  }
50
41
 
51
42
  AsyncFunction("deleteAsync") { dbName: String ->
52
- if (DATABASES.containsKey(dbName)) {
53
- throw OpenDatabaseException(dbName)
43
+ if (cachedDatabase[dbName] != null) {
44
+ throw DeleteDatabaseException(dbName)
54
45
  }
55
46
  val dbFile = File(pathForDatabaseName(dbName))
56
47
  if (!dbFile.exists()) {
57
48
  throw DatabaseNotFoundException(dbName)
58
49
  }
59
50
  if (!dbFile.delete()) {
60
- throw DeleteDatabaseException(dbName)
51
+ throw DeleteDatabaseFileException(dbName)
61
52
  }
62
53
  }
63
54
 
64
55
  OnDestroy {
65
- DATABASES.values.forEach {
66
- it?.rawQuery("SELECT crsql_finalize()", emptyArray())
67
- }
68
- }
69
- }
70
-
71
- private fun execute(dbName: String, queries: List<Query>, readOnly: Boolean, raw: Boolean = false): List<Any> {
72
- val db = getDatabase(dbName)
73
- val results = queries.map { sqlQuery ->
74
- val sql = sqlQuery.sql
75
- val bindArgs = convertParamsToStringArray(sqlQuery.args)
76
- try {
77
- if (isSelect(sql)) {
78
- doSelectInBackgroundAndPossiblyThrow(sql, bindArgs, db)
79
- } else { // update/insert/delete
80
- if (readOnly) {
81
- SQLitePluginResult(EMPTY_ROWS, EMPTY_COLUMNS, 0, 0, ReadOnlyException())
82
- } else {
83
- if (raw) {
84
- doRawUpdate(sql, bindArgs, db)
85
- } else {
86
- doUpdateInBackgroundAndPossiblyThrow(sql, bindArgs, db)
87
- }
88
- }
89
- }
90
- } catch (e: Throwable) {
91
- SQLitePluginResult(EMPTY_ROWS, EMPTY_COLUMNS, 0, 0, e)
92
- }
93
- }
94
- return pluginResultsToPrimitiveData(results)
95
- }
96
-
97
- // do a update/delete/insert operation
98
- private fun doUpdateInBackgroundAndPossiblyThrow(
99
- sql: String,
100
- bindArgs: Array<String?>?,
101
- db: SQLiteDatabase
102
- ): SQLitePluginResult {
103
- return db.compileStatement(sql).use { statement ->
104
- if (bindArgs != null) {
105
- for (i in bindArgs.size downTo 1) {
106
- val args = bindArgs[i - 1]
107
- if (args != null) {
108
- statement.bindString(i, args)
109
- } else {
110
- statement.bindNull(i)
111
- }
112
- }
113
- }
114
- if (isInsert(sql)) {
115
- val insertId = statement.executeInsert()
116
- val rowsAffected = if (insertId >= 0) 1 else 0
117
- SQLitePluginResult(EMPTY_ROWS, EMPTY_COLUMNS, rowsAffected, insertId, null)
118
- } else if (isDelete(sql) || isUpdate(sql)) {
119
- val rowsAffected = statement.executeUpdateDelete()
120
- SQLitePluginResult(EMPTY_ROWS, EMPTY_COLUMNS, rowsAffected, 0, null)
121
- } else {
122
- // in this case, we don't need rowsAffected or insertId, so we can have a slight
123
- // perf boost by just executing the query
124
- statement.execute()
125
- EMPTY_RESULT
126
- }
127
- }
128
- }
129
-
130
- private fun doRawUpdate(
131
- sql: String,
132
- bindArgs: Array<String?>,
133
- db: SQLiteDatabase
134
- ): SQLitePluginResult {
135
- return db.rawQuery(sql, bindArgs).use { cursor ->
136
- val numRows = cursor.count
137
- if (numRows == 0) {
138
- return EMPTY_RESULT
139
- }
140
-
141
- val numColumns = cursor.columnCount
142
- val columnNames = cursor.columnNames
143
- val rows: Array<Array<Any?>> = Array(numRows) { arrayOfNulls(numColumns) }
144
- var i = 0
145
- while (cursor.moveToNext()) {
146
- val row = rows[i]
147
- for (j in 0 until numColumns) {
148
- row[j] = getValueFromCursor(cursor, j, cursor.getType(j))
149
- }
150
- rows[i] = row
151
- i++
152
- }
153
-
154
- if (isInsert(sql)) {
155
- val rowsAffected = getRowsAffected(db)
156
- val insertId = getInsertId(db)
157
- SQLitePluginResult(rows, columnNames, rowsAffected, insertId, null)
158
- } else if (isDelete(sql) || isUpdate(sql)) {
159
- val rowsAffected = getRowsAffected(db)
160
- SQLitePluginResult(rows, columnNames, rowsAffected, 0, null)
161
- } else {
162
- EMPTY_RESULT
56
+ cachedDatabase.values.forEach {
57
+ it.sqlite3_close()
163
58
  }
164
59
  }
165
60
  }
166
61
 
167
- private fun getRowsAffected(
168
- db: SQLiteDatabase,
169
- ): Int {
170
- val cursor = db.rawQuery("SELECT changes() AS numRowsAffected", null)
171
- val rowsAffected = if (cursor.moveToFirst()) {
172
- val index = cursor.getColumnIndex("numRowsAffected")
173
- cursor.getInt(index)
174
- } else {
175
- -1
176
- }
177
- cursor.close()
178
- return rowsAffected
179
- }
180
-
181
- private fun getInsertId(
182
- db: SQLiteDatabase,
183
- ): Long {
184
- val cursor = db.rawQuery("SELECT last_insert_rowid() AS insertId", null)
185
- val insertId = if (cursor.moveToFirst()) {
186
- val index = cursor.getColumnIndex("insertId")
187
- cursor.getLong(index)
188
- } else {
189
- -1
190
- }
191
- cursor.close()
192
- return insertId
193
- }
194
-
195
- // do a select operation
196
- private fun doSelectInBackgroundAndPossiblyThrow(
197
- sql: String,
198
- bindArgs: Array<String?>,
199
- db: SQLiteDatabase
200
- ): SQLitePluginResult {
201
- return db.rawQuery(sql, bindArgs).use { cursor ->
202
- val numRows = cursor.count
203
- if (numRows == 0) {
204
- return EMPTY_RESULT
205
- }
206
- val numColumns = cursor.columnCount
207
- val columnNames = cursor.columnNames
208
- val rows: Array<Array<Any?>> = Array(numRows) { arrayOfNulls(numColumns) }
209
- var i = 0
210
- while (cursor.moveToNext()) {
211
- val row = rows[i]
212
- for (j in 0 until numColumns) {
213
- row[j] = getValueFromCursor(cursor, j, cursor.getType(j))
214
- }
215
- rows[i] = row
216
- i++
217
- }
218
- SQLitePluginResult(rows, columnNames, 0, 0, null)
219
- }
220
- }
221
-
222
- private fun getValueFromCursor(cursor: Cursor, index: Int, columnType: Int): Any? {
223
- return when (columnType) {
224
- Cursor.FIELD_TYPE_FLOAT -> cursor.getDouble(index)
225
- Cursor.FIELD_TYPE_INTEGER -> cursor.getLong(index)
226
- Cursor.FIELD_TYPE_BLOB ->
227
- // convert byte[] to binary string; it's good enough, because
228
- // WebSQL doesn't support blobs anyway
229
- String(cursor.getBlob(index))
230
-
231
- Cursor.FIELD_TYPE_STRING -> cursor.getString(index)
232
- else -> null
233
- }
234
- }
235
-
236
62
  @Throws(IOException::class)
237
63
  private fun pathForDatabaseName(name: String): String {
238
64
  val directory = File("${context.filesDir}${File.separator}SQLite")
@@ -240,44 +66,29 @@ class SQLiteModule : Module() {
240
66
  return "$directory${File.separator}$name"
241
67
  }
242
68
 
243
- @Throws(IOException::class)
244
- private fun getDatabase(name: String): SQLiteDatabase {
245
- var database: SQLiteDatabase? = null
246
- val path = pathForDatabaseName(name)
247
- if (File(path).exists()) {
248
- database = DATABASES[name]
69
+ private fun openDatabase(dbName: String): SQLite3Wrapper? {
70
+ val path: String
71
+ try {
72
+ path = pathForDatabaseName(dbName)
73
+ } catch (_: IOException) {
74
+ return null
249
75
  }
250
- if (database == null) {
251
- DATABASES.remove(name)
252
- val config = createConfig(path)
253
- database = SQLiteDatabase.openDatabase(config, null, null)
254
- addUpdateListener(database)
255
- DATABASES[name] = database
76
+
77
+ if (File(path).exists()) {
78
+ cachedDatabase[dbName]?.let {
79
+ return it
80
+ }
256
81
  }
257
- return database!!
258
- }
259
82
 
260
- private fun createConfig(path: String): SQLiteDatabaseConfiguration {
261
- val crsqliteExtension = SQLiteCustomExtension("libcrsqlite", "sqlite3_crsqlite_init")
262
- return SQLiteDatabaseConfiguration(path, SQLiteDatabase.CREATE_IF_NECESSARY, emptyList(), emptyList(), listOf(crsqliteExtension))
83
+ cachedDatabase.remove(dbName)
84
+ val db = SQLite3Wrapper.open(path) ?: return null
85
+ cachedDatabase[dbName] = db
86
+ return db
263
87
  }
264
88
 
265
- private fun addUpdateListener(database: SQLiteDatabase?) {
266
- database?.addUpdateListener { tableName: String, operationType: Int, rowID: Int ->
267
- sendEvent(
268
- "onDatabaseChange",
269
- bundleOf(
270
- "tableName" to tableName,
271
- "rowId" to rowID,
272
- "typeId" to when (operationType) {
273
- 9 -> SqlAction.DELETE.value
274
- 18 -> SqlAction.INSERT.value
275
- 23 -> SqlAction.UPDATE.value
276
- else -> SqlAction.UNKNOWN.value
277
- }
278
- )
279
- )
280
- }
89
+ private fun execute(dbName: String, queries: List<Query>, readOnly: Boolean): List<Any> {
90
+ val db = openDatabase(dbName) ?: throw OpenDatabaseException(dbName)
91
+ return queries.map { db.executeSql(it.sql, it.args, readOnly) }
281
92
  }
282
93
 
283
94
  internal class SQLitePluginResult(
@@ -287,6 +98,4 @@ class SQLiteModule : Module() {
287
98
  val insertId: Long,
288
99
  val error: Throwable?
289
100
  )
290
-
291
- private class ReadOnlyException : Exception("could not prepare statement (23 not authorized)")
292
101
  }