expo-sqlite 58.0.9 → 58.0.11

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 (81) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/android/build.gradle +6 -2
  3. package/android/src/main/cpp/NativeDatabaseBinding.cpp +11 -8
  4. package/android/src/main/cpp/NativeStatementBinding.cpp +43 -2
  5. package/android/src/main/cpp/NativeStatementBinding.h +3 -0
  6. package/android/src/main/cpp/SQLiteError.h +34 -0
  7. package/android/src/main/java/expo/modules/sqlite/DatabaseConnection.kt +68 -0
  8. package/android/src/main/java/expo/modules/sqlite/NativeDatabase.kt +135 -22
  9. package/android/src/main/java/expo/modules/sqlite/NativeSession.kt +126 -6
  10. package/android/src/main/java/expo/modules/sqlite/NativeStatement.kt +199 -14
  11. package/android/src/main/java/expo/modules/sqlite/NativeStatementBinding.kt +1 -0
  12. package/android/src/main/java/expo/modules/sqlite/SQLRecords.kt +19 -13
  13. package/android/src/main/java/expo/modules/sqlite/SQLiteModule.kt +176 -577
  14. package/android/src/main/java/expo/modules/sqlite/SQLiteOptions.kt +3 -10
  15. package/android/src/main/java/expo/modules/sqlite/SQLiteValues.kt +47 -0
  16. package/build/NativeDatabase.d.ts +2 -2
  17. package/build/SQLiteDatabase.js +9 -3
  18. package/build/SQLiteDatabase.js.map +1 -1
  19. package/dev-plugin-dist/+not-found.html +1 -1
  20. package/dev-plugin-dist/_expo/static/js/web/{__common-9b94dd2fbdbb825a662f42cf8d2077ee.js → __common-8b6d10c7ab64ecfd128d45105ef5590b.js} +51 -48
  21. package/dev-plugin-dist/_expo/static/js/web/{_layout-b2894030c1dfbd95b717a85901b6c271.js → _layout-1da259c4109daa50bd04899675f5d333.js} +23 -23
  22. package/dev-plugin-dist/_expo/static/js/web/{entry-04fdb7d857a65c0ed638ef2d774a4b6e.js → entry-5e24130038c41e25d9a78d08664ef6cb.js} +753 -753
  23. package/dev-plugin-dist/_expo/static/js/web/{index-4bf2da199f699eb47a42074d5e183c07.js → index-17ca77580cae7c249688c43b3b3e0949.js} +1740 -1740
  24. package/dev-plugin-dist/_expo/static/js/web/{worker-d51aa6be89e271e571111274ab8e7f5c.js → worker-cc2e40fb110f53b73733dd7e24d9d8f6.js} +17 -17
  25. package/dev-plugin-dist/_sitemap.html +1 -1
  26. package/dev-plugin-dist/index.html +1 -1
  27. package/expo-module.config.json +0 -1
  28. package/ios/Benchmarks/JavaScriptBenchmarks.swift +65 -0
  29. package/ios/DatabaseChangeEvent.swift +4 -3
  30. package/ios/NativeDatabase.swift +5 -8
  31. package/ios/NativeStatement.swift +168 -5
  32. package/ios/SQLiteBindValue.swift +39 -0
  33. package/ios/SQLiteColumnValue.swift +54 -0
  34. package/ios/SQLiteModule.swift +3 -191
  35. package/ios/SQLiteResult.swift +31 -0
  36. package/ios/Tests/NativeStatementJavaScriptTests.swift +124 -0
  37. package/ios/Tests/NativeStatementTests.swift +122 -0
  38. package/ios/Tests/SQLiteModuleTests.swift +18 -1
  39. package/ios/Tests/test_ErrorReporting.swift +25 -0
  40. package/ios/Tests/test_StatementCleanup.swift +69 -0
  41. package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.11/expo.modules.sqlite-58.0.11-sources.jar +0 -0
  42. package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.11/expo.modules.sqlite-58.0.11-sources.jar.md5 +1 -0
  43. package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.11/expo.modules.sqlite-58.0.11-sources.jar.sha1 +1 -0
  44. package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.11/expo.modules.sqlite-58.0.11-sources.jar.sha256 +1 -0
  45. package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.11/expo.modules.sqlite-58.0.11-sources.jar.sha512 +1 -0
  46. package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.11/expo.modules.sqlite-58.0.11.aar +0 -0
  47. package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.11/expo.modules.sqlite-58.0.11.aar.md5 +1 -0
  48. package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.11/expo.modules.sqlite-58.0.11.aar.sha1 +1 -0
  49. package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.11/expo.modules.sqlite-58.0.11.aar.sha256 +1 -0
  50. package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.11/expo.modules.sqlite-58.0.11.aar.sha512 +1 -0
  51. package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/{58.0.9/expo.modules.sqlite-58.0.9.module → 58.0.11/expo.modules.sqlite-58.0.11.module} +22 -22
  52. package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.11/expo.modules.sqlite-58.0.11.module.md5 +1 -0
  53. package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.11/expo.modules.sqlite-58.0.11.module.sha1 +1 -0
  54. package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.11/expo.modules.sqlite-58.0.11.module.sha256 +1 -0
  55. package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.11/expo.modules.sqlite-58.0.11.module.sha512 +1 -0
  56. package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/{58.0.9/expo.modules.sqlite-58.0.9.pom → 58.0.11/expo.modules.sqlite-58.0.11.pom} +1 -1
  57. package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.11/expo.modules.sqlite-58.0.11.pom.md5 +1 -0
  58. package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.11/expo.modules.sqlite-58.0.11.pom.sha1 +1 -0
  59. package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.11/expo.modules.sqlite-58.0.11.pom.sha256 +1 -0
  60. package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.11/expo.modules.sqlite-58.0.11.pom.sha512 +1 -0
  61. package/package.json +4 -4
  62. package/src/NativeDatabase.ts +2 -2
  63. package/src/SQLiteDatabase.ts +11 -3
  64. package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.9/expo.modules.sqlite-58.0.9-sources.jar +0 -0
  65. package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.9/expo.modules.sqlite-58.0.9-sources.jar.md5 +0 -1
  66. package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.9/expo.modules.sqlite-58.0.9-sources.jar.sha1 +0 -1
  67. package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.9/expo.modules.sqlite-58.0.9-sources.jar.sha256 +0 -1
  68. package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.9/expo.modules.sqlite-58.0.9-sources.jar.sha512 +0 -1
  69. package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.9/expo.modules.sqlite-58.0.9.aar +0 -0
  70. package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.9/expo.modules.sqlite-58.0.9.aar.md5 +0 -1
  71. package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.9/expo.modules.sqlite-58.0.9.aar.sha1 +0 -1
  72. package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.9/expo.modules.sqlite-58.0.9.aar.sha256 +0 -1
  73. package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.9/expo.modules.sqlite-58.0.9.aar.sha512 +0 -1
  74. package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.9/expo.modules.sqlite-58.0.9.module.md5 +0 -1
  75. package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.9/expo.modules.sqlite-58.0.9.module.sha1 +0 -1
  76. package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.9/expo.modules.sqlite-58.0.9.module.sha256 +0 -1
  77. package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.9/expo.modules.sqlite-58.0.9.module.sha512 +0 -1
  78. package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.9/expo.modules.sqlite-58.0.9.pom.md5 +0 -1
  79. package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.9/expo.modules.sqlite-58.0.9.pom.sha1 +0 -1
  80. package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.9/expo.modules.sqlite-58.0.9.pom.sha256 +0 -1
  81. package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.9/expo.modules.sqlite-58.0.9.pom.sha512 +0 -1
@@ -2,8 +2,6 @@
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)
@@ -172,37 +170,9 @@ public final class SQLiteModule: Module, @unchecked Sendable {
172
170
  }
173
171
  }
174
172
 
175
- // MARK: - NativeStatement
176
-
177
- Class(NativeStatement.self) {
178
- // swiftlint:disable line_length
179
-
180
- AsyncFunction("runAsync") { (statement: NativeStatement, database: NativeDatabase, bindParams: [String: Any], bindBlobParams: [String: ArrayBuffer], shouldPassAsArray: Bool) -> [String: Any] in
181
- return try run(statement: statement, database: database, bindParams: bindParams, bindBlobParams: bindBlobParams, shouldPassAsArray: shouldPassAsArray)
182
- }.runOnQueue(moduleQueue)
183
- Function("runSync") { (statement: NativeStatement, database: NativeDatabase, bindParams: [String: Any], bindBlobParams: [String: ArrayBuffer], shouldPassAsArray: Bool) -> [String: Any] in
184
- return try run(statement: statement, database: database, bindParams: bindParams, bindBlobParams: bindBlobParams, shouldPassAsArray: shouldPassAsArray)
185
- }
186
-
187
- // swiftlint:enable line_length
188
-
189
- AsyncFunction("stepAsync") { (statement: NativeStatement, database: NativeDatabase) -> SQLiteColumnValues? in
190
- return try step(statement: statement, database: database)
191
- }.runOnQueue(moduleQueue)
192
- Function("stepSync") { (statement: NativeStatement, database: NativeDatabase) -> SQLiteColumnValues? in
193
- return try step(statement: statement, database: database)
194
- }
195
-
196
- AsyncFunction("getAllAsync") { (statement: NativeStatement, database: NativeDatabase) -> [SQLiteColumnValues] in
197
- return try getAll(statement: statement, database: database)
198
- }.runOnQueue(moduleQueue)
199
- Function("getAllSync") { (statement: NativeStatement, database: NativeDatabase) -> [SQLiteColumnValues] in
200
- return try getAll(statement: statement, database: database)
201
- }
202
- }
203
-
204
- // MARK: - NativeSession
173
+ // MARK: - NativeStatement and NativeSession
205
174
 
175
+ NativeStatement._synthesizedClassDefinition()
206
176
  NativeSession._synthesizedClassDefinition()
207
177
  }
208
178
 
@@ -256,84 +226,6 @@ public final class SQLiteModule: Module, @unchecked Sendable {
256
226
  }
257
227
  }
258
228
 
259
- // swiftlint:disable line_length
260
-
261
- private func run(statement: NativeStatement, database: NativeDatabase, bindParams: [String: Any], bindBlobParams: [String: any AnyArrayBuffer], shouldPassAsArray: Bool) throws -> [String: Any] {
262
- // The statement with parameter bindings is stateful,
263
- // we have to guard with a critical section for thread safety.
264
- return try statement.lock.withLock { _ -> [String: Any] in
265
- try maybeThrowForFinalizedStatement(statement)
266
- try maybeThrowForClosedDatabase(database)
267
-
268
- exsqlite3_reset(statement.pointer)
269
- exsqlite3_clear_bindings(statement.pointer)
270
- for (key, param) in bindParams {
271
- let index = try getBindParamIndex(statement: statement, key: key, shouldPassAsArray: shouldPassAsArray)
272
- if index > 0 {
273
- try bindStatementParam(statement: statement, with: param, at: index)
274
- }
275
- }
276
- for (key, param) in bindBlobParams {
277
- let index = try getBindParamIndex(statement: statement, key: key, shouldPassAsArray: shouldPassAsArray)
278
- if index > 0 {
279
- try bindStatementParam(statement: statement, with: param, at: index)
280
- }
281
- }
282
-
283
- let ret = exsqlite3_step(statement.pointer)
284
- if ret != SQLITE_ROW && ret != SQLITE_DONE {
285
- throw SQLiteErrorException(convertSqlLiteErrorToString(database))
286
- }
287
- let firstRowValues: SQLiteColumnValues = (ret == SQLITE_ROW) ? try getColumnValues(statement: statement) : []
288
- return [
289
- "lastInsertRowId": Int(exsqlite3_last_insert_rowid(database.pointer)),
290
- "changes": Int(exsqlite3_changes(database.pointer)),
291
- "firstRowValues": firstRowValues
292
- ]
293
- }
294
- }
295
-
296
- // swiftlint:enable line_length
297
-
298
- private func step(statement: NativeStatement, database: NativeDatabase) throws -> SQLiteColumnValues? {
299
- // Guard the stateful statement, see `run` above.
300
- return try statement.lock.withLock { _ -> SQLiteColumnValues? in
301
- try maybeThrowForFinalizedStatement(statement)
302
- try maybeThrowForClosedDatabase(database)
303
-
304
- let ret = exsqlite3_step(statement.pointer)
305
- if ret == SQLITE_ROW {
306
- return try getColumnValues(statement: statement)
307
- }
308
- if ret != SQLITE_DONE {
309
- throw SQLiteErrorException(convertSqlLiteErrorToString(database))
310
- }
311
- return nil
312
- }
313
- }
314
-
315
- private func getAll(statement: NativeStatement, database: NativeDatabase) throws -> [SQLiteColumnValues] {
316
- // Guard the stateful statement, see `run` above.
317
- return try statement.lock.withLock { _ -> [SQLiteColumnValues] in
318
- try maybeThrowForFinalizedStatement(statement)
319
- try maybeThrowForClosedDatabase(database)
320
-
321
- var columnValuesList: [SQLiteColumnValues] = []
322
- while true {
323
- let ret = exsqlite3_step(statement.pointer)
324
- if ret == SQLITE_ROW {
325
- columnValuesList.append(try getColumnValues(statement: statement))
326
- continue
327
- }
328
- if ret == SQLITE_DONE {
329
- break
330
- }
331
- throw SQLiteErrorException(convertSqlLiteErrorToString(database))
332
- }
333
- return columnValuesList
334
- }
335
- }
336
-
337
229
  private func convertSqlLiteErrorToString(_ db: NativeDatabase) -> String {
338
230
  return db.lastErrorMessage()
339
231
  }
@@ -429,7 +321,7 @@ public final class SQLiteModule: Module, @unchecked Sendable {
429
321
  databaseName: String(cString: UnsafePointer(databaseName)),
430
322
  databaseFilePath: String(cString: UnsafePointer(databaseFilePath)),
431
323
  tableName: String(cString: UnsafePointer(tableName)),
432
- rowId: Int(rowId),
324
+ rowId: Double(rowId),
433
325
  typeId: SQLAction.fromCode(value: action)
434
326
  ))
435
327
  }
@@ -437,90 +329,10 @@ public final class SQLiteModule: Module, @unchecked Sendable {
437
329
  contextPair.toOpaque())
438
330
  }
439
331
 
440
- private func getColumnValues(statement: NativeStatement) throws -> SQLiteColumnValues {
441
- try maybeThrowForFinalizedStatement(statement)
442
- let columnCount = Int(exsqlite3_column_count(statement.pointer))
443
- var columnValues: SQLiteColumnValues = Array(repeating: 0, count: columnCount)
444
- for i in 0..<columnCount {
445
- columnValues[i] = try getColumnValue(statement: statement, at: Int32(i))
446
- }
447
- return columnValues
448
- }
449
-
450
- @inline(__always)
451
- private func getColumnValue(statement: NativeStatement, at index: Int32) throws -> Any {
452
- let instance = statement.pointer
453
- let type = exsqlite3_column_type(instance, index)
454
-
455
- switch type {
456
- case SQLITE_INTEGER:
457
- return exsqlite3_column_int64(instance, index)
458
- case SQLITE_FLOAT:
459
- return exsqlite3_column_double(instance, index)
460
- case SQLITE_TEXT:
461
- guard let text = exsqlite3_column_text(instance, index) else {
462
- throw InvalidConvertibleException("Null text")
463
- }
464
- return String(cString: text)
465
- case SQLITE_BLOB:
466
- guard let blob = exsqlite3_column_blob(instance, index) else {
467
- return ArrayBuffer(size: 0)
468
- }
469
- let size = exsqlite3_column_bytes(instance, index)
470
- return ArrayBuffer.copy(of: blob, count: Int(size))
471
- case SQLITE_NULL:
472
- return NSNull()
473
- default:
474
- throw InvalidConvertibleException("Unsupported column type: \(type)")
475
- }
476
- }
477
-
478
- private func bindStatementParam(statement: NativeStatement, with param: Any, at index: Int32) throws {
479
- let instance = statement.pointer
480
- switch param {
481
- case Optional<Any>.none:
482
- exsqlite3_bind_null(instance, index)
483
- case _ as NSNull:
484
- exsqlite3_bind_null(instance, index)
485
- case let param as Int64:
486
- exsqlite3_bind_int64(instance, index, Int64(param))
487
- case let param as Double:
488
- exsqlite3_bind_double(instance, index, param)
489
- case let param as String:
490
- exsqlite3_bind_text(instance, index, param, -1, SQLITE_TRANSIENT)
491
- case let param as any AnyArrayBuffer:
492
- _ = param.withUnsafeBytes {
493
- exsqlite3_bind_blob(instance, index, $0.baseAddress, Int32(param.byteLength), SQLITE_TRANSIENT)
494
- }
495
- case let param as Bool:
496
- exsqlite3_bind_int(instance, index, param ? 1 : 0)
497
- default:
498
- throw InvalidConvertibleException("Unsupported parameter type: \(type(of: param))")
499
- }
500
- }
501
-
502
332
  private func maybeThrowForClosedDatabase(_ database: NativeDatabase) throws {
503
333
  try database.ensureOpen()
504
334
  }
505
335
 
506
- private func maybeThrowForFinalizedStatement(_ statement: NativeStatement) throws {
507
- try statement.ensureNotFinalized()
508
- }
509
-
510
- @inline(__always)
511
- private func getBindParamIndex(statement: NativeStatement, key: String, shouldPassAsArray: Bool) throws -> Int32 {
512
- let index: Int32
513
- if shouldPassAsArray {
514
- guard let intKey = Int32(key) else {
515
- throw InvalidBindParameterException()
516
- }
517
- index = intKey + 1
518
- } else {
519
- index = exsqlite3_bind_parameter_index(statement.pointer, key.cString(using: .utf8))
520
- }
521
- return index
522
- }
523
-
524
336
  // MARK: - cachedDatabases managements
525
337
 
526
338
  private func addCachedDatabase(_ database: NativeDatabase) {
@@ -0,0 +1,31 @@
1
+ // Copyright 2026-present 650 Industries. All rights reserved.
2
+
3
+ // Capture a statement's error while SQLite's recursive connection mutex still protects it.
4
+ // A different statement can otherwise replace both errcode and errmsg before we report the failure.
5
+ internal func sqliteErrorMessage(for db: OpaquePointer?, code: Int32) -> String {
6
+ let message = String(cString: exsqlite3_errmsg(db), encoding: .utf8) ?? ""
7
+ return "Error code \(code): \(message)"
8
+ }
9
+
10
+ internal func withSQLiteDatabaseLock<T>(for db: OpaquePointer?, _ operation: () throws -> T) rethrows -> T {
11
+ let mutex = exsqlite3_db_mutex(db)
12
+ exsqlite3_mutex_enter(mutex)
13
+ defer { exsqlite3_mutex_leave(mutex) }
14
+ return try operation()
15
+ }
16
+
17
+ internal func sqliteErrorMessage(for db: OpaquePointer?) -> String {
18
+ return withSQLiteDatabaseLock(for: db) {
19
+ return sqliteErrorMessage(for: db, code: exsqlite3_errcode(db))
20
+ }
21
+ }
22
+
23
+ internal func sqliteResult(for db: OpaquePointer?, _ operation: () -> Int32) -> (code: Int32, message: String?) {
24
+ return withSQLiteDatabaseLock(for: db) {
25
+ let code = operation()
26
+ guard code != SQLITE_OK && code != SQLITE_ROW && code != SQLITE_DONE else {
27
+ return (code, nil)
28
+ }
29
+ return (code, sqliteErrorMessage(for: db, code: code))
30
+ }
31
+ }
@@ -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(
@@ -0,0 +1,25 @@
1
+ // Copyright 2026-present 650 Industries. All rights reserved.
2
+
3
+ import Testing
4
+
5
+ @testable import ExpoSQLite
6
+
7
+ @Suite("SQLite error reporting")
8
+ struct SQLiteErrorReportingTests {
9
+ @Test
10
+ func `captures the operation code and detail before another statement changes the connection error`() throws {
11
+ var db: OpaquePointer?
12
+ #expect(exsqlite3_open(":memory:", &db) == SQLITE_OK)
13
+ defer { exsqlite3_close(db) }
14
+ #expect(exsqlite3_exec(db, "CREATE TABLE item(id INTEGER PRIMARY KEY); INSERT INTO item VALUES (1)", nil, nil, nil) == SQLITE_OK)
15
+ var statement: OpaquePointer?
16
+ #expect(exsqlite3_prepare_v2(db, "INSERT INTO item VALUES (1)", -1, &statement, nil) == SQLITE_OK)
17
+ let execution = sqliteResult(for: db) { exsqlite3_step(statement) }
18
+ let cleanup = sqliteResult(for: db) { exsqlite3_finalize(statement) }
19
+ #expect(exsqlite3_exec(db, "SELECT 1", nil, nil, nil) == SQLITE_OK)
20
+ #expect(execution.code == SQLITE_CONSTRAINT)
21
+ #expect(cleanup.code == SQLITE_CONSTRAINT)
22
+ #expect(execution.message == "Error code 19: UNIQUE constraint failed: item.id")
23
+ #expect(cleanup.message == execution.message)
24
+ }
25
+ }
@@ -81,4 +81,73 @@ struct StatementCleanupTests {
81
81
  #expect(statement.pointer == nil)
82
82
  }
83
83
  }
84
+
85
+ @Test
86
+ @JavaScriptActor
87
+ func `run, step and getAll reject a statement that a failed close finalized`() throws {
88
+ let appContext = AppContext.create()
89
+ let module = SQLiteModule(appContext: appContext)
90
+ var pointer: OpaquePointer?
91
+ var destination: OpaquePointer?
92
+ #expect(exsqlite3_open(":memory:", &pointer) == SQLITE_OK)
93
+ #expect(exsqlite3_open(":memory:", &destination) == SQLITE_OK)
94
+ let database = NativeDatabase(pointer, databasePath: ":memory:", openOptions: OpenDatabaseOptions())
95
+ defer {
96
+ if !database.isClosed { try? module.closeDatabase(database) }
97
+ exsqlite3_close(destination)
98
+ }
99
+
100
+ let statement = NativeStatement()
101
+ try database.prepareSync(statement: statement, source: "SELECT ? AS value")
102
+
103
+ // The pending backup makes the close fail after it has finalized the statement, as above.
104
+ let backup = try #require(exsqlite3_backup_init(destination, "main", pointer, "main"))
105
+ defer { exsqlite3_backup_finish(backup) }
106
+ #expect(throws: SQLiteErrorException.self) { try module.closeDatabase(database) }
107
+ try #require(statement.isFinalized)
108
+ #expect(statement.pointer == nil)
109
+
110
+ #expect(throws: AccessClosedResourceException.self) {
111
+ try statement.runSync(database: database, bindParams: ["0": .double(1)], bindBlobParams: [:], shouldPassAsArray: true)
112
+ }
113
+ #expect(throws: AccessClosedResourceException.self) { try statement.stepSync(database: database) }
114
+ #expect(throws: AccessClosedResourceException.self) { try statement.getAllSync(database: database) }
115
+ }
116
+
117
+ @Test
118
+ func `getAll during close either reads the rows or rejects the finalized statement`() async throws {
119
+ let appContext = AppContext.create()
120
+ let module = SQLiteModule(appContext: appContext)
121
+ for _ in 0..<50 {
122
+ var pointer: OpaquePointer?
123
+ #expect(exsqlite3_open(":memory:", &pointer) == SQLITE_OK)
124
+ let database = NativeDatabase(pointer, databasePath: ":memory:", openOptions: OpenDatabaseOptions())
125
+ defer {
126
+ if !database.isClosed { try? module.closeDatabase(database) }
127
+ }
128
+ let statement = NativeStatement()
129
+ try await database.prepareAsync(statement: statement, source: "SELECT 1 AS value UNION ALL SELECT 2")
130
+ await withTaskGroup(of: Void.self) { group in
131
+ group.addTask {
132
+ do {
133
+ let rows = try await statement.getAllAsync(database: database)
134
+ #expect(rows == [[.double(1)], [.double(2)]])
135
+ } catch is AccessClosedResourceException {
136
+ // Close finalized the statement first.
137
+ } catch {
138
+ Issue.record(error)
139
+ }
140
+ }
141
+ group.addTask {
142
+ do {
143
+ try module.closeDatabase(database)
144
+ } catch {
145
+ Issue.record(error)
146
+ }
147
+ }
148
+ }
149
+ #expect(database.isClosed)
150
+ #expect(statement.isFinalized)
151
+ }
152
+ }
84
153
  }
@@ -0,0 +1 @@
1
+ e4feeb357b13c3da0d6690deff9aab2186a7874185369a06c55fa6da992fdc9c
@@ -0,0 +1 @@
1
+ 93ce18d06cc3125618ca50503fd5b7f6d7407b05e931ddf6381fa7e70d8b41c6719c65a433fafa28626a12335b6e9f9a5e4cd05036f715e7a5b01c94047fddf7
@@ -0,0 +1 @@
1
+ 0832753b21e57b16e425e77702b1f0fdcb2cf1a6614c5a5bc0721f92bf3fb60c
@@ -0,0 +1 @@
1
+ fe5e05c035e8a3b493952282f2a26f117cf856664daf52600e32e532e1767da7a0636626289a4a99fcd50afabbe4f558d8015e0a2df8f4ced1b5eaeea59e216a