expo-sqlite 58.0.8 → 58.0.10
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.
- package/CHANGELOG.md +18 -0
- package/android/build.gradle +2 -2
- package/android/src/main/cpp/NativeDatabaseBinding.cpp +0 -22
- package/android/src/main/cpp/NativeDatabaseBinding.h +0 -1
- package/android/src/main/cpp/NativeStatementBinding.cpp +3 -1
- package/android/src/main/cpp/NativeStatementBinding.h +1 -1
- package/android/src/main/java/expo/modules/sqlite/NativeDatabase.kt +5 -0
- package/android/src/main/java/expo/modules/sqlite/NativeDatabaseBinding.kt +0 -1
- package/android/src/main/java/expo/modules/sqlite/NativeStatement.kt +9 -1
- package/android/src/main/java/expo/modules/sqlite/SQLiteModule.kt +78 -48
- package/dev-plugin-dist/+not-found.html +1 -1
- package/dev-plugin-dist/_expo/static/js/web/{__common-b5c30377ac28ec1a3c7472c7feddcab4.js → __common-9b94dd2fbdbb825a662f42cf8d2077ee.js} +36 -36
- package/dev-plugin-dist/_expo/static/js/web/{_layout-bdc15c267ba9298fc3df49fa915a8895.js → _layout-b2894030c1dfbd95b717a85901b6c271.js} +21 -21
- package/dev-plugin-dist/_expo/static/js/web/{entry-182fd72f164b90bb353bfd4ca3257c12.js → entry-04fdb7d857a65c0ed638ef2d774a4b6e.js} +547 -550
- package/dev-plugin-dist/_expo/static/js/web/{index-5d44464b52f351ee1a8d579d6ec31de6.js → index-4bf2da199f699eb47a42074d5e183c07.js} +1740 -1740
- package/dev-plugin-dist/_expo/static/js/web/{worker-634941622d077e92764ccbb4960edffc.js → worker-d51aa6be89e271e571111274ab8e7f5c.js} +17 -17
- package/dev-plugin-dist/_sitemap.html +1 -1
- package/dev-plugin-dist/index.html +1 -1
- package/ios/Benchmarks/JavaScriptBenchmarks.swift +65 -0
- package/ios/DatabaseChangeEvent.swift +4 -3
- package/ios/NativeDatabase.swift +14 -5
- package/ios/NativeStatement.swift +179 -15
- package/ios/SQLiteBindValue.swift +39 -0
- package/ios/SQLiteColumnValue.swift +54 -0
- package/ios/SQLiteModule.swift +40 -227
- package/ios/Tests/NativeStatementJavaScriptTests.swift +124 -0
- package/ios/Tests/NativeStatementTests.swift +122 -0
- package/ios/Tests/SQLiteModuleTests.swift +18 -1
- package/ios/Tests/test_StatementCleanup.swift +153 -0
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.10/expo.modules.sqlite-58.0.10-sources.jar +0 -0
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.10/expo.modules.sqlite-58.0.10-sources.jar.md5 +1 -0
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.10/expo.modules.sqlite-58.0.10-sources.jar.sha1 +1 -0
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.10/expo.modules.sqlite-58.0.10-sources.jar.sha256 +1 -0
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.10/expo.modules.sqlite-58.0.10-sources.jar.sha512 +1 -0
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.10/expo.modules.sqlite-58.0.10.aar +0 -0
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.10/expo.modules.sqlite-58.0.10.aar.md5 +1 -0
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.10/expo.modules.sqlite-58.0.10.aar.sha1 +1 -0
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.10/expo.modules.sqlite-58.0.10.aar.sha256 +1 -0
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.10/expo.modules.sqlite-58.0.10.aar.sha512 +1 -0
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/{58.0.8/expo.modules.sqlite-58.0.8.module → 58.0.10/expo.modules.sqlite-58.0.10.module} +22 -22
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.10/expo.modules.sqlite-58.0.10.module.md5 +1 -0
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.10/expo.modules.sqlite-58.0.10.module.sha1 +1 -0
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.10/expo.modules.sqlite-58.0.10.module.sha256 +1 -0
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.10/expo.modules.sqlite-58.0.10.module.sha512 +1 -0
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/{58.0.8/expo.modules.sqlite-58.0.8.pom → 58.0.10/expo.modules.sqlite-58.0.10.pom} +1 -1
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.10/expo.modules.sqlite-58.0.10.pom.md5 +1 -0
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.10/expo.modules.sqlite-58.0.10.pom.sha1 +1 -0
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.10/expo.modules.sqlite-58.0.10.pom.sha256 +1 -0
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.10/expo.modules.sqlite-58.0.10.pom.sha512 +1 -0
- package/package.json +4 -4
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.8/expo.modules.sqlite-58.0.8-sources.jar +0 -0
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.8/expo.modules.sqlite-58.0.8-sources.jar.md5 +0 -1
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.8/expo.modules.sqlite-58.0.8-sources.jar.sha1 +0 -1
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.8/expo.modules.sqlite-58.0.8-sources.jar.sha256 +0 -1
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.8/expo.modules.sqlite-58.0.8-sources.jar.sha512 +0 -1
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.8/expo.modules.sqlite-58.0.8.aar +0 -0
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.8/expo.modules.sqlite-58.0.8.aar.md5 +0 -1
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.8/expo.modules.sqlite-58.0.8.aar.sha1 +0 -1
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.8/expo.modules.sqlite-58.0.8.aar.sha256 +0 -1
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.8/expo.modules.sqlite-58.0.8.aar.sha512 +0 -1
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.8/expo.modules.sqlite-58.0.8.module.md5 +0 -1
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.8/expo.modules.sqlite-58.0.8.module.sha1 +0 -1
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.8/expo.modules.sqlite-58.0.8.module.sha256 +0 -1
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.8/expo.modules.sqlite-58.0.8.module.sha512 +0 -1
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.8/expo.modules.sqlite-58.0.8.pom.md5 +0 -1
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.8/expo.modules.sqlite-58.0.8.pom.sha1 +0 -1
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.8/expo.modules.sqlite-58.0.8.pom.sha256 +0 -1
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.8/expo.modules.sqlite-58.0.8.pom.sha512 +0 -1
package/ios/SQLiteModule.swift
CHANGED
|
@@ -2,14 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
import ExpoModulesCore
|
|
4
4
|
|
|
5
|
-
private typealias SQLiteColumnValues = [Any]
|
|
6
|
-
private let SQLITE_TRANSIENT = unsafeBitCast(OpaquePointer(bitPattern: -1), to: sqlite3_destructor_type.self)
|
|
7
5
|
private let MEMORY_DB_NAME = ":memory:"
|
|
8
6
|
|
|
9
7
|
private let moduleQueue = DispatchQueue(label: "expo.module.sqlite.AsyncQueue", qos: .userInitiated, attributes: .concurrent)
|
|
10
8
|
|
|
11
9
|
// `@unchecked Sendable`: the `@JS(.concurrent)` members send the module off the JavaScript thread, which
|
|
12
|
-
// Swift 6 mode allows only for a `Sendable` module. The mutable state is either guarded by `
|
|
10
|
+
// Swift 6 mode allows only for a `Sendable` module. The mutable state is either guarded by `cacheLock` or
|
|
13
11
|
// only read off the JavaScript thread (`hasListeners`).
|
|
14
12
|
@ExpoModule("ExpoSQLite")
|
|
15
13
|
public final class SQLiteModule: Module, @unchecked Sendable {
|
|
@@ -17,7 +15,8 @@ public final class SQLiteModule: Module, @unchecked Sendable {
|
|
|
17
15
|
// will release the pair when `closeDatabase` is called.
|
|
18
16
|
private var contextPairs = [Unmanaged<AnyObject>]()
|
|
19
17
|
|
|
20
|
-
|
|
18
|
+
// Closing a cached connection also releases its update-hook context under this lock.
|
|
19
|
+
private static let cacheLock = NSRecursiveLock()
|
|
21
20
|
private var cachedDatabases = [NativeDatabase]()
|
|
22
21
|
private(set) var hasListeners = false
|
|
23
22
|
|
|
@@ -154,10 +153,7 @@ public final class SQLiteModule: Module, @unchecked Sendable {
|
|
|
154
153
|
}
|
|
155
154
|
|
|
156
155
|
AsyncFunction("closeAsync") { (database: NativeDatabase) in
|
|
157
|
-
try
|
|
158
|
-
if let db = removeCachedDatabase(of: database) {
|
|
159
|
-
try closeDatabase(db)
|
|
160
|
-
}
|
|
156
|
+
try closeDatabaseIfNeeded(database)
|
|
161
157
|
}.runOnQueue(moduleQueue)
|
|
162
158
|
// Interrupt must reach SQLite immediately, without waiting for the running query's queue.
|
|
163
159
|
Function("interruptSync") { (database: NativeDatabase) in
|
|
@@ -170,44 +166,13 @@ public final class SQLiteModule: Module, @unchecked Sendable {
|
|
|
170
166
|
exsqlite3_interrupt(database.pointer)
|
|
171
167
|
}
|
|
172
168
|
Function("closeSync") { (database: NativeDatabase) in
|
|
173
|
-
try
|
|
174
|
-
if let db = removeCachedDatabase(of: database) {
|
|
175
|
-
try closeDatabase(db)
|
|
176
|
-
}
|
|
169
|
+
try closeDatabaseIfNeeded(database)
|
|
177
170
|
}
|
|
178
171
|
}
|
|
179
172
|
|
|
180
|
-
// MARK: - NativeStatement
|
|
181
|
-
|
|
182
|
-
Class(NativeStatement.self) {
|
|
183
|
-
// swiftlint:disable line_length
|
|
184
|
-
|
|
185
|
-
AsyncFunction("runAsync") { (statement: NativeStatement, database: NativeDatabase, bindParams: [String: Any], bindBlobParams: [String: ArrayBuffer], shouldPassAsArray: Bool) -> [String: Any] in
|
|
186
|
-
return try run(statement: statement, database: database, bindParams: bindParams, bindBlobParams: bindBlobParams, shouldPassAsArray: shouldPassAsArray)
|
|
187
|
-
}.runOnQueue(moduleQueue)
|
|
188
|
-
Function("runSync") { (statement: NativeStatement, database: NativeDatabase, bindParams: [String: Any], bindBlobParams: [String: ArrayBuffer], shouldPassAsArray: Bool) -> [String: Any] in
|
|
189
|
-
return try run(statement: statement, database: database, bindParams: bindParams, bindBlobParams: bindBlobParams, shouldPassAsArray: shouldPassAsArray)
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// swiftlint:enable line_length
|
|
193
|
-
|
|
194
|
-
AsyncFunction("stepAsync") { (statement: NativeStatement, database: NativeDatabase) -> SQLiteColumnValues? in
|
|
195
|
-
return try step(statement: statement, database: database)
|
|
196
|
-
}.runOnQueue(moduleQueue)
|
|
197
|
-
Function("stepSync") { (statement: NativeStatement, database: NativeDatabase) -> SQLiteColumnValues? in
|
|
198
|
-
return try step(statement: statement, database: database)
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
AsyncFunction("getAllAsync") { (statement: NativeStatement, database: NativeDatabase) -> [SQLiteColumnValues] in
|
|
202
|
-
return try getAll(statement: statement, database: database)
|
|
203
|
-
}.runOnQueue(moduleQueue)
|
|
204
|
-
Function("getAllSync") { (statement: NativeStatement, database: NativeDatabase) -> [SQLiteColumnValues] in
|
|
205
|
-
return try getAll(statement: statement, database: database)
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// MARK: - NativeSession
|
|
173
|
+
// MARK: - NativeStatement and NativeSession
|
|
210
174
|
|
|
175
|
+
NativeStatement._synthesizedClassDefinition()
|
|
211
176
|
NativeSession._synthesizedClassDefinition()
|
|
212
177
|
}
|
|
213
178
|
|
|
@@ -261,98 +226,28 @@ public final class SQLiteModule: Module, @unchecked Sendable {
|
|
|
261
226
|
}
|
|
262
227
|
}
|
|
263
228
|
|
|
264
|
-
// swiftlint:disable line_length
|
|
265
|
-
|
|
266
|
-
private func run(statement: NativeStatement, database: NativeDatabase, bindParams: [String: Any], bindBlobParams: [String: any AnyArrayBuffer], shouldPassAsArray: Bool) throws -> [String: Any] {
|
|
267
|
-
try maybeThrowForClosedDatabase(database)
|
|
268
|
-
try maybeThrowForFinalizedStatement(statement)
|
|
269
|
-
|
|
270
|
-
// The statement with parameter bindings is stateful,
|
|
271
|
-
// we have to guard with a critical section for thread safety.
|
|
272
|
-
return try statement.lock.withLock { _ -> [String: Any] in
|
|
273
|
-
exsqlite3_reset(statement.pointer)
|
|
274
|
-
exsqlite3_clear_bindings(statement.pointer)
|
|
275
|
-
for (key, param) in bindParams {
|
|
276
|
-
let index = try getBindParamIndex(statement: statement, key: key, shouldPassAsArray: shouldPassAsArray)
|
|
277
|
-
if index > 0 {
|
|
278
|
-
try bindStatementParam(statement: statement, with: param, at: index)
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
for (key, param) in bindBlobParams {
|
|
282
|
-
let index = try getBindParamIndex(statement: statement, key: key, shouldPassAsArray: shouldPassAsArray)
|
|
283
|
-
if index > 0 {
|
|
284
|
-
try bindStatementParam(statement: statement, with: param, at: index)
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
let ret = exsqlite3_step(statement.pointer)
|
|
289
|
-
if ret != SQLITE_ROW && ret != SQLITE_DONE {
|
|
290
|
-
throw SQLiteErrorException(convertSqlLiteErrorToString(database))
|
|
291
|
-
}
|
|
292
|
-
let firstRowValues: SQLiteColumnValues = (ret == SQLITE_ROW) ? try getColumnValues(statement: statement) : []
|
|
293
|
-
return [
|
|
294
|
-
"lastInsertRowId": Int(exsqlite3_last_insert_rowid(database.pointer)),
|
|
295
|
-
"changes": Int(exsqlite3_changes(database.pointer)),
|
|
296
|
-
"firstRowValues": firstRowValues
|
|
297
|
-
]
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// swiftlint:enable line_length
|
|
302
|
-
|
|
303
|
-
private func step(statement: NativeStatement, database: NativeDatabase) throws -> SQLiteColumnValues? {
|
|
304
|
-
try maybeThrowForClosedDatabase(database)
|
|
305
|
-
try maybeThrowForFinalizedStatement(statement)
|
|
306
|
-
|
|
307
|
-
// Guard the stateful statement, see `run` above.
|
|
308
|
-
return try statement.lock.withLock { _ -> SQLiteColumnValues? in
|
|
309
|
-
let ret = exsqlite3_step(statement.pointer)
|
|
310
|
-
if ret == SQLITE_ROW {
|
|
311
|
-
return try getColumnValues(statement: statement)
|
|
312
|
-
}
|
|
313
|
-
if ret != SQLITE_DONE {
|
|
314
|
-
throw SQLiteErrorException(convertSqlLiteErrorToString(database))
|
|
315
|
-
}
|
|
316
|
-
return nil
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
private func getAll(statement: NativeStatement, database: NativeDatabase) throws -> [SQLiteColumnValues] {
|
|
321
|
-
try maybeThrowForClosedDatabase(database)
|
|
322
|
-
try maybeThrowForFinalizedStatement(statement)
|
|
323
|
-
|
|
324
|
-
// Guard the stateful statement, see `run` above.
|
|
325
|
-
return try statement.lock.withLock { _ -> [SQLiteColumnValues] in
|
|
326
|
-
var columnValuesList: [SQLiteColumnValues] = []
|
|
327
|
-
while true {
|
|
328
|
-
let ret = exsqlite3_step(statement.pointer)
|
|
329
|
-
if ret == SQLITE_ROW {
|
|
330
|
-
columnValuesList.append(try getColumnValues(statement: statement))
|
|
331
|
-
continue
|
|
332
|
-
}
|
|
333
|
-
if ret == SQLITE_DONE {
|
|
334
|
-
break
|
|
335
|
-
}
|
|
336
|
-
throw SQLiteErrorException(convertSqlLiteErrorToString(database))
|
|
337
|
-
}
|
|
338
|
-
return columnValuesList
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
229
|
private func convertSqlLiteErrorToString(_ db: NativeDatabase) -> String {
|
|
343
230
|
return db.lastErrorMessage()
|
|
344
231
|
}
|
|
345
232
|
|
|
346
|
-
|
|
233
|
+
func closeDatabase(_ db: NativeDatabase) throws {
|
|
234
|
+
Self.cacheLock.lock()
|
|
235
|
+
defer { Self.cacheLock.unlock() }
|
|
347
236
|
db.closeLock.lock()
|
|
348
237
|
defer { db.closeLock.unlock() }
|
|
349
|
-
|
|
238
|
+
db.statementLifecycleLock.lock()
|
|
239
|
+
defer { db.statementLifecycleLock.unlock() }
|
|
240
|
+
try db.ensureOpen()
|
|
350
241
|
try maybeFinalizeAllStatements(db)
|
|
351
242
|
|
|
352
243
|
let ret = exsqlite3_close(db.pointer)
|
|
244
|
+
// SQLITE_BUSY leaves the connection open, including its update hook.
|
|
245
|
+
if ret != SQLITE_OK {
|
|
246
|
+
throw SQLiteErrorException(convertSqlLiteErrorToString(db))
|
|
247
|
+
}
|
|
353
248
|
db.isClosed = true
|
|
354
249
|
|
|
355
|
-
Self.
|
|
250
|
+
Self.cacheLock.withLock {
|
|
356
251
|
if let index = contextPairs.firstIndex(where: {
|
|
357
252
|
guard let pair = $0.takeUnretainedValue() as? (SQLiteModule, NativeDatabase) else {
|
|
358
253
|
return false
|
|
@@ -366,10 +261,6 @@ public final class SQLiteModule: Module, @unchecked Sendable {
|
|
|
366
261
|
contextPairs.remove(at: index)
|
|
367
262
|
}
|
|
368
263
|
}
|
|
369
|
-
|
|
370
|
-
if ret != SQLITE_OK {
|
|
371
|
-
throw SQLiteErrorException(convertSqlLiteErrorToString(db))
|
|
372
|
-
}
|
|
373
264
|
}
|
|
374
265
|
|
|
375
266
|
private func deleteDatabase(databasePath: String) throws {
|
|
@@ -412,7 +303,7 @@ public final class SQLiteModule: Module, @unchecked Sendable {
|
|
|
412
303
|
|
|
413
304
|
private func addUpdateHook(_ database: NativeDatabase) {
|
|
414
305
|
let contextPair = Unmanaged.passRetained(((self, database) as AnyObject))
|
|
415
|
-
Self.
|
|
306
|
+
Self.cacheLock.withLock {
|
|
416
307
|
contextPairs.append(contextPair)
|
|
417
308
|
}
|
|
418
309
|
// swiftlint:disable:next multiline_arguments
|
|
@@ -430,7 +321,7 @@ public final class SQLiteModule: Module, @unchecked Sendable {
|
|
|
430
321
|
databaseName: String(cString: UnsafePointer(databaseName)),
|
|
431
322
|
databaseFilePath: String(cString: UnsafePointer(databaseFilePath)),
|
|
432
323
|
tableName: String(cString: UnsafePointer(tableName)),
|
|
433
|
-
rowId:
|
|
324
|
+
rowId: Double(rowId),
|
|
434
325
|
typeId: SQLAction.fromCode(value: action)
|
|
435
326
|
))
|
|
436
327
|
}
|
|
@@ -438,114 +329,39 @@ public final class SQLiteModule: Module, @unchecked Sendable {
|
|
|
438
329
|
contextPair.toOpaque())
|
|
439
330
|
}
|
|
440
331
|
|
|
441
|
-
private func getColumnValues(statement: NativeStatement) throws -> SQLiteColumnValues {
|
|
442
|
-
try maybeThrowForFinalizedStatement(statement)
|
|
443
|
-
let columnCount = Int(exsqlite3_column_count(statement.pointer))
|
|
444
|
-
var columnValues: SQLiteColumnValues = Array(repeating: 0, count: columnCount)
|
|
445
|
-
for i in 0..<columnCount {
|
|
446
|
-
columnValues[i] = try getColumnValue(statement: statement, at: Int32(i))
|
|
447
|
-
}
|
|
448
|
-
return columnValues
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
@inline(__always)
|
|
452
|
-
private func getColumnValue(statement: NativeStatement, at index: Int32) throws -> Any {
|
|
453
|
-
let instance = statement.pointer
|
|
454
|
-
let type = exsqlite3_column_type(instance, index)
|
|
455
|
-
|
|
456
|
-
switch type {
|
|
457
|
-
case SQLITE_INTEGER:
|
|
458
|
-
return exsqlite3_column_int64(instance, index)
|
|
459
|
-
case SQLITE_FLOAT:
|
|
460
|
-
return exsqlite3_column_double(instance, index)
|
|
461
|
-
case SQLITE_TEXT:
|
|
462
|
-
guard let text = exsqlite3_column_text(instance, index) else {
|
|
463
|
-
throw InvalidConvertibleException("Null text")
|
|
464
|
-
}
|
|
465
|
-
return String(cString: text)
|
|
466
|
-
case SQLITE_BLOB:
|
|
467
|
-
guard let blob = exsqlite3_column_blob(instance, index) else {
|
|
468
|
-
return ArrayBuffer(size: 0)
|
|
469
|
-
}
|
|
470
|
-
let size = exsqlite3_column_bytes(instance, index)
|
|
471
|
-
return ArrayBuffer.copy(of: blob, count: Int(size))
|
|
472
|
-
case SQLITE_NULL:
|
|
473
|
-
return NSNull()
|
|
474
|
-
default:
|
|
475
|
-
throw InvalidConvertibleException("Unsupported column type: \(type)")
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
private func bindStatementParam(statement: NativeStatement, with param: Any, at index: Int32) throws {
|
|
480
|
-
let instance = statement.pointer
|
|
481
|
-
switch param {
|
|
482
|
-
case Optional<Any>.none:
|
|
483
|
-
exsqlite3_bind_null(instance, index)
|
|
484
|
-
case _ as NSNull:
|
|
485
|
-
exsqlite3_bind_null(instance, index)
|
|
486
|
-
case let param as Int64:
|
|
487
|
-
exsqlite3_bind_int64(instance, index, Int64(param))
|
|
488
|
-
case let param as Double:
|
|
489
|
-
exsqlite3_bind_double(instance, index, param)
|
|
490
|
-
case let param as String:
|
|
491
|
-
exsqlite3_bind_text(instance, index, param, -1, SQLITE_TRANSIENT)
|
|
492
|
-
case let param as any AnyArrayBuffer:
|
|
493
|
-
_ = param.withUnsafeBytes {
|
|
494
|
-
exsqlite3_bind_blob(instance, index, $0.baseAddress, Int32(param.byteLength), SQLITE_TRANSIENT)
|
|
495
|
-
}
|
|
496
|
-
case let param as Bool:
|
|
497
|
-
exsqlite3_bind_int(instance, index, param ? 1 : 0)
|
|
498
|
-
default:
|
|
499
|
-
throw InvalidConvertibleException("Unsupported parameter type: \(type(of: param))")
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
|
|
503
332
|
private func maybeThrowForClosedDatabase(_ database: NativeDatabase) throws {
|
|
504
333
|
try database.ensureOpen()
|
|
505
334
|
}
|
|
506
335
|
|
|
507
|
-
private func maybeThrowForFinalizedStatement(_ statement: NativeStatement) throws {
|
|
508
|
-
try statement.ensureNotFinalized()
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
@inline(__always)
|
|
512
|
-
private func getBindParamIndex(statement: NativeStatement, key: String, shouldPassAsArray: Bool) throws -> Int32 {
|
|
513
|
-
let index: Int32
|
|
514
|
-
if shouldPassAsArray {
|
|
515
|
-
guard let intKey = Int32(key) else {
|
|
516
|
-
throw InvalidBindParameterException()
|
|
517
|
-
}
|
|
518
|
-
index = intKey + 1
|
|
519
|
-
} else {
|
|
520
|
-
index = exsqlite3_bind_parameter_index(statement.pointer, key.cString(using: .utf8))
|
|
521
|
-
}
|
|
522
|
-
return index
|
|
523
|
-
}
|
|
524
|
-
|
|
525
336
|
// MARK: - cachedDatabases managements
|
|
526
337
|
|
|
527
338
|
private func addCachedDatabase(_ database: NativeDatabase) {
|
|
528
|
-
Self.
|
|
339
|
+
Self.cacheLock.withLock {
|
|
529
340
|
cachedDatabases.append(database)
|
|
530
341
|
}
|
|
531
342
|
}
|
|
532
343
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
344
|
+
private func closeDatabaseIfNeeded(_ database: NativeDatabase) throws {
|
|
345
|
+
try Self.cacheLock.withLock {
|
|
346
|
+
try maybeThrowForClosedDatabase(database)
|
|
536
347
|
if let index = cachedDatabases.firstIndex(of: database) {
|
|
537
348
|
let db = cachedDatabases[index]
|
|
538
349
|
if db.release() == 0 {
|
|
350
|
+
do {
|
|
351
|
+
try closeDatabase(db)
|
|
352
|
+
} catch {
|
|
353
|
+
// Keep the connection cached and owned so callers can clean up and retry.
|
|
354
|
+
db.addRef()
|
|
355
|
+
throw error
|
|
356
|
+
}
|
|
539
357
|
cachedDatabases.remove(at: index)
|
|
540
|
-
return db
|
|
541
358
|
}
|
|
542
359
|
}
|
|
543
|
-
return nil
|
|
544
360
|
}
|
|
545
361
|
}
|
|
546
362
|
|
|
547
363
|
private func findCachedDatabase(where predicate: (NativeDatabase) -> Bool) -> NativeDatabase? {
|
|
548
|
-
return Self.
|
|
364
|
+
return Self.cacheLock.withLock {
|
|
549
365
|
if let database = cachedDatabases.first(where: predicate) {
|
|
550
366
|
return database
|
|
551
367
|
}
|
|
@@ -554,7 +370,7 @@ public final class SQLiteModule: Module, @unchecked Sendable {
|
|
|
554
370
|
}
|
|
555
371
|
|
|
556
372
|
private func removeAllCachedDatabases() -> [NativeDatabase] {
|
|
557
|
-
return Self.
|
|
373
|
+
return Self.cacheLock.withLock {
|
|
558
374
|
let databases = cachedDatabases
|
|
559
375
|
cachedDatabases.removeAll()
|
|
560
376
|
return databases
|
|
@@ -566,17 +382,14 @@ public final class SQLiteModule: Module, @unchecked Sendable {
|
|
|
566
382
|
guard database.openOptions.finalizeUnusedStatementsBeforeClosing else {
|
|
567
383
|
return
|
|
568
384
|
}
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
if ret != SQLITE_OK {
|
|
577
|
-
ExpoModulesCore.log.warn("exsqlite3_finalize failed: \(convertSqlLiteErrorToString(database))")
|
|
385
|
+
// Finalize through the wrappers so even a failed close leaves them invalidated.
|
|
386
|
+
// Do not destroy SQLite-internal statements owned by concurrent exec/backup operations.
|
|
387
|
+
for statement in database.statements {
|
|
388
|
+
do {
|
|
389
|
+
try statement.finalize(database: database)
|
|
390
|
+
} catch {
|
|
391
|
+
ExpoModulesCore.log.warn("Finalizing a statement during close failed: \(error)")
|
|
578
392
|
}
|
|
579
|
-
stmt = nextStmt
|
|
580
393
|
}
|
|
581
394
|
}
|
|
582
395
|
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// Copyright 2015-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import Testing
|
|
4
|
+
|
|
5
|
+
@testable import ExpoModulesCore
|
|
6
|
+
@testable import ExpoSQLite
|
|
7
|
+
|
|
8
|
+
/// Calls the statement members from JavaScript, so the bind parameters and the column values go
|
|
9
|
+
/// through the `SQLiteBindValue` and `SQLiteColumnValue` conversions and the result has the shape `src/` reads.
|
|
10
|
+
@Suite("NativeStatement from JavaScript")
|
|
11
|
+
@JavaScriptActor
|
|
12
|
+
struct NativeStatementJavaScriptTests {
|
|
13
|
+
let appContext: AppContext
|
|
14
|
+
let runtime: ExpoRuntime
|
|
15
|
+
|
|
16
|
+
init() throws {
|
|
17
|
+
appContext = AppContext.create()
|
|
18
|
+
runtime = try appContext.runtime
|
|
19
|
+
appContext.moduleRegistry.register(
|
|
20
|
+
holder: ModuleHolder(appContext: appContext, module: SQLiteModule(appContext: appContext), name: "ExpoSQLite")
|
|
21
|
+
)
|
|
22
|
+
_ = try runtime.eval(
|
|
23
|
+
"""
|
|
24
|
+
var NativeStatement = expo.modules.ExpoSQLite.NativeStatement;
|
|
25
|
+
var db = new expo.modules.ExpoSQLite.NativeDatabase(':memory:', { useNewConnection: true });
|
|
26
|
+
db.initSync();
|
|
27
|
+
db.execSync('CREATE TABLE t (id INTEGER PRIMARY KEY NOT NULL, value TEXT)');
|
|
28
|
+
function prepare(source) {
|
|
29
|
+
var statement = new NativeStatement();
|
|
30
|
+
db.prepareSync(statement, source);
|
|
31
|
+
return statement;
|
|
32
|
+
}
|
|
33
|
+
"""
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@Test
|
|
38
|
+
func `run decodes each parameter type and encodes each column type`() throws {
|
|
39
|
+
let result = try runtime.eval(
|
|
40
|
+
"""
|
|
41
|
+
(function () {
|
|
42
|
+
var statement = prepare('SELECT ?, ?, ?, ?, ?, typeof(?), ?');
|
|
43
|
+
var result = statement.runSync(db, { 0: 7, 1: 1.5, 2: 'text', 3: true, 4: null, 5: 7 }, { 6: new Uint8Array([1, 2, 3]) }, true);
|
|
44
|
+
var values = result.firstRowValues;
|
|
45
|
+
return JSON.stringify({
|
|
46
|
+
values: values.slice(0, 6),
|
|
47
|
+
blobIsArrayBuffer: values[6] instanceof ArrayBuffer,
|
|
48
|
+
blob: Array.from(new Uint8Array(values[6])),
|
|
49
|
+
});
|
|
50
|
+
})()
|
|
51
|
+
"""
|
|
52
|
+
)
|
|
53
|
+
// Numbers bind as doubles, as they did through the type-erased path, so `typeof` reports `real`.
|
|
54
|
+
#expect(try result.asString() == #"{"values":[7,1.5,"text",1,null,"real"],"blobIsArrayBuffer":true,"blob":[1,2,3]}"#)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@Test
|
|
58
|
+
func `run reports the last insert row id and the changes`() throws {
|
|
59
|
+
let result = try runtime.eval(
|
|
60
|
+
"""
|
|
61
|
+
(function () {
|
|
62
|
+
var statement = prepare('INSERT INTO t (id, value) VALUES ($id, $value)');
|
|
63
|
+
var result = statement.runSync(db, { $id: 42, $value: 'answer' }, {}, false);
|
|
64
|
+
return JSON.stringify([result.lastInsertRowId, result.changes, result.firstRowValues]);
|
|
65
|
+
})()
|
|
66
|
+
"""
|
|
67
|
+
)
|
|
68
|
+
#expect(try result.asString() == "[42,1,[]]")
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@Test
|
|
72
|
+
func `run reports a last insert row id above 2^53 as a number`() throws {
|
|
73
|
+
let result = try runtime.eval(
|
|
74
|
+
"""
|
|
75
|
+
(function () {
|
|
76
|
+
db.execSync('INSERT INTO t (id, value) VALUES (9007199254740993, "big")');
|
|
77
|
+
var statement = prepare('SELECT 1');
|
|
78
|
+
var result = statement.runSync(db, {}, {}, true);
|
|
79
|
+
return typeof result.lastInsertRowId + ' ' + result.lastInsertRowId;
|
|
80
|
+
})()
|
|
81
|
+
"""
|
|
82
|
+
)
|
|
83
|
+
// Above 2^53 the number loses precision, as integer columns do, but it must not throw. The rowid is
|
|
84
|
+
// connection-wide, so a throw here would make every later run on the connection fail too.
|
|
85
|
+
#expect(try result.asString() == "number 9007199254740992")
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@Test
|
|
89
|
+
func `step and getAll return rows of column values`() throws {
|
|
90
|
+
let result = try runtime.eval(
|
|
91
|
+
"""
|
|
92
|
+
(function () {
|
|
93
|
+
db.execSync("INSERT INTO t (id, value) VALUES (1, 'one'), (2, NULL)");
|
|
94
|
+
var statement = prepare('SELECT id, value FROM t ORDER BY id');
|
|
95
|
+
var stepped = [statement.stepSync(db), statement.stepSync(db), statement.stepSync(db)];
|
|
96
|
+
statement.resetSync(db);
|
|
97
|
+
return JSON.stringify({ stepped: stepped, all: statement.getAllSync(db) });
|
|
98
|
+
})()
|
|
99
|
+
"""
|
|
100
|
+
)
|
|
101
|
+
#expect(try result.asString() == #"{"stepped":[[1,"one"],[2,null],null],"all":[[1,"one"],[2,null]]}"#)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
@Test
|
|
105
|
+
func `run binds a bigint as an integer`() throws {
|
|
106
|
+
let result = try runtime.eval(
|
|
107
|
+
"""
|
|
108
|
+
(function () {
|
|
109
|
+
var result = prepare('SELECT ?, typeof(?)').runSync(db, { 0: 9007199254740993n, 1: 7n }, {}, true);
|
|
110
|
+
return JSON.stringify(result.firstRowValues);
|
|
111
|
+
})()
|
|
112
|
+
"""
|
|
113
|
+
)
|
|
114
|
+
// The column comes back as a number, like every integer column, so it loses precision above 2^53.
|
|
115
|
+
#expect(try result.asString() == #"[9007199254740992,"integer"]"#)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@Test
|
|
119
|
+
func `run rejects a parameter of an unsupported type`() throws {
|
|
120
|
+
#expect(throws: (any Error).self) {
|
|
121
|
+
try runtime.eval("prepare('SELECT ?').runSync(db, { 0: Symbol('nope') }, {}, true)")
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -87,4 +87,126 @@ final class NativeStatementTests {
|
|
|
87
87
|
try await statement.finalizeAsync(database: database)
|
|
88
88
|
#expect(statement.isFinalized)
|
|
89
89
|
}
|
|
90
|
+
|
|
91
|
+
@Test
|
|
92
|
+
func `run binds positional parameters and reports the changes`() throws {
|
|
93
|
+
let statement = prepare("INSERT INTO test (id, value) VALUES (?, ?)")
|
|
94
|
+
let result = try statement.runSync(
|
|
95
|
+
database: database,
|
|
96
|
+
bindParams: ["0": .double(3), "1": .text("three")],
|
|
97
|
+
bindBlobParams: [:],
|
|
98
|
+
shouldPassAsArray: true
|
|
99
|
+
)
|
|
100
|
+
#expect(result.lastInsertRowId == 3)
|
|
101
|
+
#expect(result.changes == 1)
|
|
102
|
+
#expect(result.firstRowValues.isEmpty)
|
|
103
|
+
#expect(selectValue(id: 3) == "three")
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@Test
|
|
107
|
+
func `run binds named parameters`() throws {
|
|
108
|
+
let statement = prepare("INSERT INTO test (id, value) VALUES ($id, $value)")
|
|
109
|
+
_ = try statement.runSync(
|
|
110
|
+
database: database,
|
|
111
|
+
bindParams: ["$id": .double(4), "$value": .text("four")],
|
|
112
|
+
bindBlobParams: [:],
|
|
113
|
+
shouldPassAsArray: false
|
|
114
|
+
)
|
|
115
|
+
#expect(selectValue(id: 4) == "four")
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@Test
|
|
119
|
+
func `run returns the values of the first row`() throws {
|
|
120
|
+
let statement = prepare("SELECT id, value FROM test WHERE id = ?")
|
|
121
|
+
let result = try statement.runSync(
|
|
122
|
+
database: database,
|
|
123
|
+
bindParams: ["0": .double(2)],
|
|
124
|
+
bindBlobParams: [:],
|
|
125
|
+
shouldPassAsArray: true
|
|
126
|
+
)
|
|
127
|
+
#expect(result.firstRowValues == [.double(2), .text("two")])
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
@Test
|
|
131
|
+
func `run binds blob and null parameters`() throws {
|
|
132
|
+
let statement = prepare("SELECT ?, ?, typeof(?)")
|
|
133
|
+
let bytes: [UInt8] = [1, 2, 3]
|
|
134
|
+
let result = try statement.runSync(
|
|
135
|
+
database: database,
|
|
136
|
+
bindParams: ["1": nil, "2": nil],
|
|
137
|
+
bindBlobParams: ["0": ArrayBuffer.copy(of: bytes, count: bytes.count)],
|
|
138
|
+
shouldPassAsArray: true
|
|
139
|
+
)
|
|
140
|
+
#expect(result.firstRowValues.count == 3)
|
|
141
|
+
#expect(blobBytes(result.firstRowValues[0]) == [1, 2, 3])
|
|
142
|
+
#expect(result.firstRowValues[1] == nil)
|
|
143
|
+
#expect(result.firstRowValues[2] == .text("null"))
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
@Test
|
|
147
|
+
func `step returns one row at a time and nil once done`() throws {
|
|
148
|
+
let statement = prepare("SELECT id FROM test ORDER BY id")
|
|
149
|
+
#expect(try statement.stepSync(database: database) == [.double(1)])
|
|
150
|
+
#expect(try statement.stepSync(database: database) == [.double(2)])
|
|
151
|
+
#expect(try statement.stepSync(database: database) == nil)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
@Test
|
|
155
|
+
func `getAll returns every row with each storage class`() throws {
|
|
156
|
+
let statement = prepare("SELECT id, value, 1.5, NULL FROM test ORDER BY id")
|
|
157
|
+
let rows = try statement.getAllSync(database: database)
|
|
158
|
+
#expect(rows == [
|
|
159
|
+
[.double(1), .text("one"), .double(1.5), nil],
|
|
160
|
+
[.double(2), .text("two"), .double(1.5), nil]
|
|
161
|
+
])
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
@Test
|
|
165
|
+
func `run, step and getAll throw once the statement is finalized`() throws {
|
|
166
|
+
let statement = prepare("SELECT id FROM test")
|
|
167
|
+
try statement.finalizeSync(database: database)
|
|
168
|
+
#expect(throws: AccessClosedResourceException.self) {
|
|
169
|
+
try statement.runSync(database: database, bindParams: [:], bindBlobParams: [:], shouldPassAsArray: true)
|
|
170
|
+
}
|
|
171
|
+
#expect(throws: AccessClosedResourceException.self) {
|
|
172
|
+
try statement.stepSync(database: database)
|
|
173
|
+
}
|
|
174
|
+
#expect(throws: AccessClosedResourceException.self) {
|
|
175
|
+
try statement.getAllSync(database: database)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
@Test
|
|
180
|
+
func `async run, step and getAll run the same operations`() async throws {
|
|
181
|
+
let statement = prepare("SELECT id FROM test WHERE id >= ? ORDER BY id")
|
|
182
|
+
let result = try await statement.runAsync(
|
|
183
|
+
database: database,
|
|
184
|
+
bindParams: ["0": .double(1)],
|
|
185
|
+
bindBlobParams: [:],
|
|
186
|
+
shouldPassAsArray: true
|
|
187
|
+
)
|
|
188
|
+
#expect(result.firstRowValues == [.double(1)])
|
|
189
|
+
#expect(try await statement.stepAsync(database: database) == [.double(2)])
|
|
190
|
+
#expect(try await statement.stepAsync(database: database) == nil)
|
|
191
|
+
try statement.resetSync(database: database)
|
|
192
|
+
#expect(try await statement.getAllAsync(database: database) == [[.double(1)], [.double(2)]])
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private func selectValue(id: Int32) -> String? {
|
|
196
|
+
var statement: OpaquePointer?
|
|
197
|
+
exsqlite3_prepare_v2(database.pointer, "SELECT value FROM test WHERE id = ?", -1, &statement, nil)
|
|
198
|
+
defer { exsqlite3_finalize(statement) }
|
|
199
|
+
exsqlite3_bind_int(statement, 1, id)
|
|
200
|
+
guard exsqlite3_step(statement) == SQLITE_ROW, let text = exsqlite3_column_text(statement, 0) else {
|
|
201
|
+
return nil
|
|
202
|
+
}
|
|
203
|
+
return String(cString: text)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private func blobBytes(_ value: SQLiteColumnValue?) -> [UInt8]? {
|
|
207
|
+
guard case .blob(let buffer)? = value else {
|
|
208
|
+
return nil
|
|
209
|
+
}
|
|
210
|
+
return buffer.withUnsafeBytes { Array($0) }
|
|
211
|
+
}
|
|
90
212
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Copyright 2015-present 650 Industries. All rights reserved.
|
|
2
2
|
|
|
3
|
-
import ExpoModulesCore
|
|
4
3
|
import Testing
|
|
5
4
|
|
|
5
|
+
@testable import ExpoModulesCore
|
|
6
6
|
@testable import ExpoSQLite
|
|
7
7
|
|
|
8
8
|
/// Exercises the module-level surface: the constants JavaScript reads, the change-event payload shape
|
|
@@ -47,6 +47,23 @@ struct SQLiteModuleTests {
|
|
|
47
47
|
#expect(module.hasListeners == false)
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
@Test
|
|
51
|
+
func `database change event encodes a row id above 2^53 as a number`() throws {
|
|
52
|
+
let appContext = AppContext.create()
|
|
53
|
+
let runtime = try appContext.runtime
|
|
54
|
+
let event = DatabaseChangeEvent(
|
|
55
|
+
databaseName: "main",
|
|
56
|
+
databaseFilePath: "/tmp/test.db",
|
|
57
|
+
tableName: "test",
|
|
58
|
+
rowId: 9007199254740993,
|
|
59
|
+
typeId: .insert
|
|
60
|
+
)
|
|
61
|
+
// Above 2^53 the number loses precision, like integer columns do, but encoding must not fail: a
|
|
62
|
+
// payload that fails to encode drops the `onDatabaseChange` event.
|
|
63
|
+
let encoded = try DatabaseChangeEvent.encode(event, in: runtime)
|
|
64
|
+
#expect(encoded.getObject().getProperty("rowId").getDouble() == 9007199254740992)
|
|
65
|
+
}
|
|
66
|
+
|
|
50
67
|
@Test
|
|
51
68
|
func `database change event carries the fields JavaScript reads`() {
|
|
52
69
|
let event = DatabaseChangeEvent(
|