@capgo/capacitor-fast-sql 8.2.16 → 8.2.18

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 (37) hide show
  1. package/README.md +17 -16
  2. package/android/build.gradle +1 -0
  3. package/android/consumer-rules.pro +14 -0
  4. package/android/src/main/java/app/capgo/capacitor/fastsql/CapgoCapacitorFastSqlPlugin.java +29 -6
  5. package/android/src/main/java/app/capgo/capacitor/fastsql/DatabaseConnection.java +2 -0
  6. package/android/src/main/java/app/capgo/capacitor/fastsql/EncryptedSQLDatabase.java +33 -46
  7. package/android/src/main/java/app/capgo/capacitor/fastsql/SQLDatabase.java +32 -50
  8. package/android/src/main/java/app/capgo/capacitor/fastsql/SQLHTTPServer.java +11 -23
  9. package/dist/esm/definitions.d.ts +17 -2
  10. package/dist/esm/definitions.d.ts.map +1 -1
  11. package/dist/esm/definitions.js +3 -1
  12. package/dist/esm/fast-sql.d.ts +4 -0
  13. package/dist/esm/fast-sql.d.ts.map +1 -1
  14. package/dist/esm/fast-sql.js +35 -3
  15. package/dist/esm/native-sql-connection.d.ts +1 -0
  16. package/dist/esm/native-sql-connection.d.ts.map +1 -1
  17. package/dist/esm/native-sql-connection.js +13 -6
  18. package/dist/esm/transaction-isolation.d.ts +3 -0
  19. package/dist/esm/transaction-isolation.d.ts.map +1 -0
  20. package/dist/esm/transaction-isolation.js +6 -0
  21. package/dist/esm/vector-store.d.ts.map +1 -1
  22. package/dist/esm/vector-store.js +5 -2
  23. package/dist/esm/web-sql-connection.d.ts.map +1 -1
  24. package/dist/esm/web-sql-connection.js +2 -0
  25. package/dist/esm/web.d.ts +2 -0
  26. package/dist/esm/web.d.ts.map +1 -1
  27. package/dist/esm/web.js +32 -4
  28. package/dist/plugin.cjs.js +93 -16
  29. package/dist/plugin.cjs.js.map +1 -1
  30. package/dist/plugin.js +93 -16
  31. package/dist/plugin.js.map +1 -1
  32. package/ios/Sources/CapgoCapacitorFastSqlPlugin/CapgoCapacitorFastSqlPlugin.swift +25 -3
  33. package/ios/Sources/CapgoCapacitorFastSqlPlugin/SQLDatabase.swift +15 -5
  34. package/ios/Sources/CapgoCapacitorFastSqlPlugin/SQLHTTPServer.swift +6 -0
  35. package/ios/Tests/CapgoCapacitorFastSqlPluginTests/CapgoCapacitorFastSqlPluginTests.swift +13 -3
  36. package/ios/Tests/CapgoCapacitorFastSqlPluginTests/SQLResultMetadataTests.swift +15 -0
  37. package/package.json +4 -2
@@ -10,7 +10,7 @@ import SQLite3
10
10
  */
11
11
  @objc(CapgoCapacitorFastSqlPlugin)
12
12
  public class CapgoCapacitorFastSqlPlugin: CAPPlugin, CAPBridgedPlugin {
13
- private let pluginVersion: String = "8.2.16"
13
+ private let pluginVersion: String = "8.2.18"
14
14
  public let identifier = "CapgoCapacitorFastSqlPlugin"
15
15
  public let jsName = "CapgoCapacitorFastSql"
16
16
  public let pluginMethods: [CAPPluginMethod] = [
@@ -44,7 +44,11 @@ public class CapgoCapacitorFastSqlPlugin: CAPPlugin, CAPBridgedPlugin {
44
44
  }
45
45
 
46
46
  // Check if already connected
47
- if databases[database] != nil {
47
+ if let existing = databases[database] {
48
+ guard existing.isReadOnly == (call.getBool("readOnly") ?? false) else {
49
+ call.reject("Database is already open with a different readOnly option")
50
+ return
51
+ }
48
52
  if let server = server {
49
53
  call.resolve([
50
54
  "port": server.port,
@@ -63,6 +67,7 @@ public class CapgoCapacitorFastSqlPlugin: CAPPlugin, CAPBridgedPlugin {
63
67
  let db = try SQLDatabase(
64
68
  path: dbPath,
65
69
  encrypted: encrypted,
70
+ readOnly: call.getBool("readOnly") ?? false,
66
71
  encryptionKey: encryptionKey,
67
72
  walMode: walMode,
68
73
  performancePresets: performancePresets
@@ -177,6 +182,10 @@ public class CapgoCapacitorFastSqlPlugin: CAPPlugin, CAPBridgedPlugin {
177
182
  }
178
183
 
179
184
  @objc func beginTransaction(_ call: CAPPluginCall) {
185
+ guard (call.getString("isolationLevel") ?? "SERIALIZABLE") == "SERIALIZABLE" else {
186
+ call.reject("Only SERIALIZABLE transaction isolation is supported")
187
+ return
188
+ }
180
189
  guard let database = call.getString("database") else {
181
190
  call.reject("Database name is required")
182
191
  return
@@ -245,6 +254,12 @@ public class CapgoCapacitorFastSqlPlugin: CAPPlugin, CAPBridgedPlugin {
245
254
  }
246
255
 
247
256
  private func getDatabasePath(_ database: String) throws -> String {
257
+ guard !database.isEmpty, database != ".", database != "..",
258
+ !database.contains("/"), !database.contains("\\"), !database.contains("\0") else {
259
+ throw NSError(domain: "CapgoCapacitorFastSQL", code: 2, userInfo: [
260
+ NSLocalizedDescriptionKey: "Database must be a non-empty file name without path separators"
261
+ ])
262
+ }
248
263
  let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
249
264
  guard let documentsDirectory = paths.first else {
250
265
  throw NSError(domain: "CapgoCapacitorFastSQL", code: 1, userInfo: [
@@ -253,6 +268,13 @@ public class CapgoCapacitorFastSqlPlugin: CAPPlugin, CAPBridgedPlugin {
253
268
  }
254
269
 
255
270
  let dbPath = (documentsDirectory as NSString).appendingPathComponent("\(database).db")
256
- return dbPath
271
+ let root = URL(fileURLWithPath: documentsDirectory).resolvingSymlinksInPath().standardizedFileURL
272
+ let resolved = URL(fileURLWithPath: dbPath).resolvingSymlinksInPath().standardizedFileURL
273
+ guard resolved.deletingLastPathComponent().path == root.path else {
274
+ throw NSError(domain: "CapgoCapacitorFastSQL", code: 2, userInfo: [
275
+ NSLocalizedDescriptionKey: "Database path must remain inside app storage"
276
+ ])
277
+ }
278
+ return resolved.path
257
279
  }
258
280
  }
@@ -13,6 +13,7 @@ private let sqliteTransient = unsafeBitCast(-1, to: sqlite3_destructor_type.self
13
13
  * SQLite database wrapper for iOS
14
14
  */
15
15
  class SQLDatabase {
16
+ let isReadOnly: Bool
16
17
  private var db: OpaquePointer?
17
18
  private let path: String
18
19
  private var inTransaction = false
@@ -20,13 +21,15 @@ class SQLDatabase {
20
21
  init(
21
22
  path: String,
22
23
  encrypted: Bool = false,
24
+ readOnly: Bool = false,
23
25
  encryptionKey: String? = nil,
24
26
  walMode: Bool = false,
25
27
  performancePresets: Bool = false
26
28
  ) throws {
27
29
  self.path = path
30
+ self.isReadOnly = readOnly
28
31
 
29
- let flags = SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE | SQLITE_OPEN_FULLMUTEX
32
+ let flags = (readOnly ? SQLITE_OPEN_READONLY : SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE) | SQLITE_OPEN_FULLMUTEX
30
33
  let result = sqlite3_open_v2(path, &db, flags, nil)
31
34
 
32
35
  guard result == SQLITE_OK else {
@@ -64,7 +67,7 @@ class SQLDatabase {
64
67
  #endif
65
68
  }
66
69
 
67
- try applyPerformanceOptions(walMode: walMode, performancePresets: performancePresets)
70
+ try applyPerformanceOptions(walMode: walMode && !readOnly, performancePresets: performancePresets)
68
71
  needsClose = false
69
72
  }
70
73
 
@@ -148,8 +151,9 @@ class SQLDatabase {
148
151
  }
149
152
  }
150
153
 
151
- let changes = sqlite3_changes(db)
152
- let lastInsertId = sqlite3_last_insert_rowid(db)
154
+ let readOnly = sqlite3_stmt_readonly(stmt) != 0
155
+ let changes = readOnly ? 0 : sqlite3_changes(db)
156
+ let lastInsertId = readOnly ? 0 : sqlite3_last_insert_rowid(db)
153
157
 
154
158
  return [
155
159
  "rows": rows,
@@ -211,7 +215,10 @@ class SQLDatabase {
211
215
  type == "binary",
212
216
  let base64 = dict["_data"] as? String,
213
217
  let data = Data(base64Encoded: base64) {
214
- // Handle binary data
218
+ // SQLite treats a null blob pointer as SQL NULL, even with length zero.
219
+ if data.isEmpty {
220
+ return sqlite3_bind_zeroblob(stmt, index, 0)
221
+ }
215
222
  return data.withUnsafeBytes { (bytes: UnsafeRawBufferPointer) in
216
223
  sqlite3_bind_blob(stmt, index, bytes.baseAddress, Int32(data.count), sqliteTransient)
217
224
  }
@@ -238,6 +245,9 @@ class SQLDatabase {
238
245
  }
239
246
  return NSNull()
240
247
  case SQLITE_BLOB:
248
+ if sqlite3_column_bytes(stmt, index) == 0 {
249
+ return ["_type": "binary", "_data": ""]
250
+ }
241
251
  if let blob = sqlite3_column_blob(stmt, index) {
242
252
  let size = sqlite3_column_bytes(stmt, index)
243
253
  let data = Data(bytes: blob, count: Int(size))
@@ -283,6 +283,12 @@ class SQLHTTPServer {
283
283
  }
284
284
 
285
285
  private func handleBeginTransactionRequest(request: HTTPRequest) -> HTTPResponse {
286
+ if !request.body.isEmpty {
287
+ guard let body = try? JSONSerialization.jsonObject(with: request.body) as? [String: Any],
288
+ (body["isolationLevel"] as? String ?? "SERIALIZABLE") == "SERIALIZABLE" else {
289
+ return HTTPResponse(.badRequest, body: Data("Only SERIALIZABLE transaction isolation is supported".utf8))
290
+ }
291
+ }
286
292
  // Authenticate
287
293
  guard let authHeader = request.headers["Authorization"],
288
294
  authHeader == "Bearer \(token)" else {
@@ -2,8 +2,18 @@ import XCTest
2
2
  @testable import CapgoCapacitorFastSqlPlugin
3
3
 
4
4
  final class CapgoCapacitorFastSqlPluginTests: XCTestCase {
5
- func testExample() throws {
6
- // This is an example of a functional test case.
7
- XCTAssertTrue(true)
5
+ func testEmptyBlobRemainsDistinctFromNull() throws {
6
+ let database = try SQLDatabase(path: ":memory:")
7
+ defer { database.close() }
8
+ let result = try database.execute(
9
+ statement: "SELECT typeof(?) AS kind, ? AS value, NULL AS missing",
10
+ params: [["_type": "binary", "_data": ""], ["_type": "binary", "_data": ""]]
11
+ )
12
+ let rows = try XCTUnwrap(result["rows"] as? [[String: Any]])
13
+ XCTAssertEqual(rows.first?["kind"] as? String, "blob")
14
+ let value = try XCTUnwrap(rows.first?["value"] as? [String: String])
15
+ XCTAssertEqual(value["_type"], "binary")
16
+ XCTAssertEqual(value["_data"], "")
17
+ XCTAssertTrue(rows.first?["missing"] is NSNull)
8
18
  }
9
19
  }
@@ -0,0 +1,15 @@
1
+ import XCTest
2
+ @testable import CapgoCapacitorFastSqlPlugin
3
+
4
+ final class SQLResultMetadataTests: XCTestCase {
5
+ func testSelectDoesNotInheritPreviousInsertMetadata() throws {
6
+ let database = try SQLDatabase(path: ":memory:")
7
+ defer { database.close() }
8
+ _ = try database.execute(statement: "CREATE TABLE sample (id INTEGER PRIMARY KEY)", params: [])
9
+ let inserted = try database.execute(statement: "INSERT INTO sample DEFAULT VALUES", params: [])
10
+ XCTAssertEqual(inserted["rowsAffected"] as? Int32, 1)
11
+ let selected = try database.execute(statement: "SELECT id FROM sample", params: [])
12
+ XCTAssertEqual(selected["rowsAffected"] as? Int32, 0)
13
+ XCTAssertTrue(selected["insertId"] is NSNull)
14
+ }
15
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-fast-sql",
3
- "version": "8.2.16",
3
+ "version": "8.2.18",
4
4
  "description": "High-performance native SQLite plugin with custom protocol for efficient sync operations and IndexedDB replacement",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",
@@ -9,6 +9,7 @@
9
9
  "files": [
10
10
  "android/src/main/",
11
11
  "android/build.gradle",
12
+ "android/consumer-rules.pro",
12
13
  "dist/",
13
14
  "ios/Sources",
14
15
  "ios/Tests",
@@ -55,7 +56,8 @@
55
56
  "check:cap9-deprecated": "node scripts/check-cap9-deprecated.mjs",
56
57
  "example:install": "cd example-app && bun install --frozen-lockfile",
57
58
  "example:build": "bun run build && cd example-app && bun install --frozen-lockfile && bun run build",
58
- "changelog:ai": "node scripts/generate-ai-changelog.mjs"
59
+ "changelog:ai": "node scripts/generate-ai-changelog.mjs",
60
+ "benchmark:check": "bun test tests/benchmark.test.ts && tsc -p benchmarks/tsconfig.json"
59
61
  },
60
62
  "dependencies": {
61
63
  "@sqlite.org/sqlite-wasm": "3.53.0-build1"