@capgo/capacitor-fast-sql 8.2.18 → 8.3.1

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 (32) hide show
  1. package/Package.swift +2 -1
  2. package/README.md +22 -14
  3. package/android/build.gradle +3 -0
  4. package/android/src/main/java/app/capgo/capacitor/fastsql/BinaryFrame.java +152 -0
  5. package/android/src/main/java/app/capgo/capacitor/fastsql/CapgoCapacitorFastSqlPlugin.java +5 -1
  6. package/android/src/main/java/app/capgo/capacitor/fastsql/DatabaseConnection.java +2 -0
  7. package/android/src/main/java/app/capgo/capacitor/fastsql/EncryptedSQLDatabase.java +13 -2
  8. package/android/src/main/java/app/capgo/capacitor/fastsql/SQLDatabase.java +13 -2
  9. package/android/src/main/java/app/capgo/capacitor/fastsql/SQLHTTPServer.java +114 -57
  10. package/dist/esm/binary-frame.d.ts +6 -0
  11. package/dist/esm/binary-frame.d.ts.map +1 -0
  12. package/dist/esm/binary-frame.js +100 -0
  13. package/dist/esm/definitions.d.ts +17 -0
  14. package/dist/esm/definitions.d.ts.map +1 -1
  15. package/dist/esm/fast-sql.d.ts.map +1 -1
  16. package/dist/esm/fast-sql.js +13 -3
  17. package/dist/esm/native-sql-connection.d.ts +18 -7
  18. package/dist/esm/native-sql-connection.d.ts.map +1 -1
  19. package/dist/esm/native-sql-connection.js +145 -59
  20. package/dist/esm/web.d.ts.map +1 -1
  21. package/dist/esm/web.js +2 -0
  22. package/dist/plugin.cjs.js +260 -62
  23. package/dist/plugin.cjs.js.map +1 -1
  24. package/dist/plugin.js +260 -62
  25. package/dist/plugin.js.map +1 -1
  26. package/ios/Sources/CapgoCapacitorFastSqlPlugin/BinaryFrame.swift +95 -0
  27. package/ios/Sources/CapgoCapacitorFastSqlPlugin/CapgoCapacitorFastSqlPlugin.swift +7 -3
  28. package/ios/Sources/CapgoCapacitorFastSqlPlugin/SQLDatabase.swift +20 -3
  29. package/ios/Sources/CapgoCapacitorFastSqlPlugin/SQLHTTPServer.swift +59 -76
  30. package/ios/Tests/CapgoCapacitorFastSqlPluginTests/BinaryFrameTests.swift +65 -0
  31. package/ios/Tests/CapgoCapacitorFastSqlPluginTests/Fixtures/binary-v1.bin +0 -0
  32. package/package.json +3 -2
@@ -0,0 +1,95 @@
1
+ import Foundation
2
+ import CoreFoundation
3
+
4
+ enum BinaryFrameError: Error {
5
+ case invalid
6
+ case limit
7
+ }
8
+
9
+ /// Version 1 envelope shared with Android and JavaScript. Data stays binary.
10
+ enum BinaryFrame {
11
+ static let contentType = "application/vnd.capgo.fast-sql.v1+binary"
12
+ static let maxBytes = 64 * 1024 * 1024
13
+ private static let maxMetadata = 1024 * 1024
14
+ private static let headerBytes = 12
15
+ private static let magic: [UInt8] = [70, 83, 81, 49]
16
+
17
+ static func encode(_ value: Any) throws -> Data {
18
+ var attachments = Data()
19
+ var nodes = 0
20
+ var blobs = 0
21
+ func encodeValue(_ value: Any, depth: Int) throws -> Any {
22
+ nodes += 1
23
+ guard nodes <= 100000, depth <= 32 else { throw BinaryFrameError.limit }
24
+ if let bytes = value as? Data {
25
+ blobs += 1
26
+ guard blobs <= 4096, attachments.count + bytes.count <= maxBytes - headerBytes else { throw BinaryFrameError.limit }
27
+ let reference = ["$blob": [attachments.count, bytes.count]]
28
+ attachments.append(bytes)
29
+ return reference
30
+ }
31
+ if let number = value as? NSNumber, !number.doubleValue.isFinite { throw BinaryFrameError.invalid }
32
+ if let array = value as? [Any] { return try array.map { try encodeValue($0, depth: depth + 1) } }
33
+ if let object = value as? [String: Any] {
34
+ return try object.mapValues { try encodeValue($0, depth: depth + 1) }
35
+ }
36
+ return value
37
+ }
38
+ let metadata = try JSONSerialization.data(withJSONObject: encodeValue(value, depth: 0), options: [.fragmentsAllowed])
39
+ guard metadata.count <= maxMetadata, headerBytes + metadata.count + attachments.count <= maxBytes else { throw BinaryFrameError.limit }
40
+ var output = Data(magic)
41
+ for count in [metadata.count, attachments.count] {
42
+ var littleEndian = UInt32(count).littleEndian
43
+ withUnsafeBytes(of: &littleEndian) { output.append(contentsOf: $0) }
44
+ }
45
+ output.append(metadata)
46
+ output.append(attachments)
47
+ return output
48
+ }
49
+
50
+ static func decode(_ frame: Data) throws -> Any {
51
+ guard frame.count >= headerBytes, frame.count <= maxBytes, Array(frame.prefix(4)) == magic else { throw BinaryFrameError.invalid }
52
+ let metadataLength = frame.withUnsafeBytes { Int(UInt32(littleEndian: $0.loadUnaligned(fromByteOffset: 4, as: UInt32.self))) }
53
+ let payloadLength = frame.withUnsafeBytes { Int(UInt32(littleEndian: $0.loadUnaligned(fromByteOffset: 8, as: UInt32.self))) }
54
+ let payloadOffset = headerBytes + metadataLength
55
+ guard metadataLength > 0, metadataLength <= maxMetadata, payloadOffset + payloadLength == frame.count else { throw BinaryFrameError.invalid }
56
+ let start = frame.startIndex
57
+ let metadata = frame.subdata(in: (start + headerBytes)..<(start + payloadOffset))
58
+ guard String(data: metadata, encoding: .utf8) != nil else { throw BinaryFrameError.invalid }
59
+ var nesting = 0
60
+ var quoted = false
61
+ var escaped = false
62
+ for byte in metadata {
63
+ if quoted {
64
+ if escaped { escaped = false } else if byte == 92 { escaped = true } else if byte == 34 { quoted = false }
65
+ } else if byte == 34 { quoted = true } else if byte == 91 || byte == 123 {
66
+ nesting += 1
67
+ guard nesting <= 33 else { throw BinaryFrameError.limit }
68
+ } else if byte == 93 || byte == 125 { nesting -= 1 }
69
+ }
70
+ guard let value = try? JSONSerialization.jsonObject(with: metadata, options: [.fragmentsAllowed]) else { throw BinaryFrameError.invalid }
71
+ var nodes = 0
72
+ var blobs = 0
73
+ var referencedBytes = 0
74
+ func decodeValue(_ value: Any, depth: Int) throws -> Any {
75
+ nodes += 1
76
+ guard nodes <= 100000, depth <= 32 else { throw BinaryFrameError.limit }
77
+ if let array = value as? [Any] { return try array.map { try decodeValue($0, depth: depth + 1) } }
78
+ guard let object = value as? [String: Any] else { return value }
79
+ if object.count == 1, let raw = object["$blob"] {
80
+ guard let reference = raw as? [NSNumber], reference.count == 2 else { throw BinaryFrameError.invalid }
81
+ let offset = reference[0].doubleValue
82
+ let count = reference[1].doubleValue
83
+ guard reference.allSatisfy({ CFGetTypeID($0) != CFBooleanGetTypeID() }), offset.isFinite, count.isFinite,
84
+ offset >= 0, count >= 0, offset.rounded() == offset, count.rounded() == count,
85
+ offset + count <= Double(payloadLength) else { throw BinaryFrameError.invalid }
86
+ referencedBytes += Int(count)
87
+ blobs += 1
88
+ guard blobs <= 4096, referencedBytes <= maxBytes else { throw BinaryFrameError.limit }
89
+ return frame.subdata(in: (start + payloadOffset + Int(offset))..<(start + payloadOffset + Int(offset) + Int(count)))
90
+ }
91
+ return try object.mapValues { try decodeValue($0, depth: depth + 1) }
92
+ }
93
+ return try decodeValue(value, depth: 0)
94
+ }
95
+ }
@@ -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.18"
13
+ private let pluginVersion: String = "8.3.1"
14
14
  public let identifier = "CapgoCapacitorFastSqlPlugin"
15
15
  public let jsName = "CapgoCapacitorFastSql"
16
16
  public let pluginMethods: [CAPPluginMethod] = [
@@ -53,6 +53,7 @@ public class CapgoCapacitorFastSqlPlugin: CAPPlugin, CAPBridgedPlugin {
53
53
  call.resolve([
54
54
  "port": server.port,
55
55
  "token": server.token,
56
+ "binaryProtocolVersion": 1,
56
57
  "database": database
57
58
  ])
58
59
  return
@@ -102,6 +103,7 @@ public class CapgoCapacitorFastSqlPlugin: CAPPlugin, CAPBridgedPlugin {
102
103
  call.resolve([
103
104
  "port": server.port,
104
105
  "token": server.token,
106
+ "binaryProtocolVersion": 1,
105
107
  "database": database
106
108
  ])
107
109
  } catch {
@@ -151,7 +153,8 @@ public class CapgoCapacitorFastSqlPlugin: CAPPlugin, CAPBridgedPlugin {
151
153
 
152
154
  call.resolve([
153
155
  "port": server.port,
154
- "token": server.token
156
+ "token": server.token,
157
+ "binaryProtocolVersion": 1
155
158
  ])
156
159
  }
157
160
 
@@ -244,7 +247,8 @@ public class CapgoCapacitorFastSqlPlugin: CAPPlugin, CAPBridgedPlugin {
244
247
 
245
248
  @objc func getPluginVersion(_ call: CAPPluginCall) {
246
249
  call.resolve([
247
- "version": pluginVersion
250
+ "version": pluginVersion,
251
+ "binaryProtocolVersion": 1
248
252
  ])
249
253
  }
250
254
 
@@ -14,6 +14,7 @@ private let sqliteTransient = unsafeBitCast(-1, to: sqlite3_destructor_type.self
14
14
  */
15
15
  class SQLDatabase {
16
16
  let isReadOnly: Bool
17
+ private let operationLock = NSRecursiveLock()
17
18
  private var db: OpaquePointer?
18
19
  private let path: String
19
20
  private var inTransaction = false
@@ -86,13 +87,17 @@ class SQLDatabase {
86
87
  }
87
88
 
88
89
  func close() {
90
+ operationLock.lock()
91
+ defer { operationLock.unlock() }
89
92
  if let db = db {
90
93
  sqlite3_close(db)
91
94
  self.db = nil
92
95
  }
93
96
  }
94
97
 
95
- func execute(statement: String, params: [Any]) throws -> [String: Any] {
98
+ func execute(statement: String, params: [Any], binaryResults: Bool = false) throws -> [String: Any] {
99
+ operationLock.lock()
100
+ defer { operationLock.unlock() }
96
101
  guard let db = db else {
97
102
  throw SQLError.notOpen
98
103
  }
@@ -139,7 +144,7 @@ class SQLDatabase {
139
144
  var row: [String: Any] = [:]
140
145
  for columnIndex in 0..<columnCount {
141
146
  let columnName = columnNames[Int(columnIndex)]
142
- let value = getColumnValue(stmt: stmt, index: columnIndex)
147
+ let value = getColumnValue(stmt: stmt, index: columnIndex, binaryResults: binaryResults)
143
148
  row[columnName] = value
144
149
  }
145
150
  rows.append(row)
@@ -163,6 +168,8 @@ class SQLDatabase {
163
168
  }
164
169
 
165
170
  func beginTransaction() throws {
171
+ operationLock.lock()
172
+ defer { operationLock.unlock() }
166
173
  guard !inTransaction else {
167
174
  throw SQLError.transactionAlreadyActive
168
175
  }
@@ -171,6 +178,8 @@ class SQLDatabase {
171
178
  }
172
179
 
173
180
  func commitTransaction() throws {
181
+ operationLock.lock()
182
+ defer { operationLock.unlock() }
174
183
  guard inTransaction else {
175
184
  throw SQLError.noTransactionActive
176
185
  }
@@ -179,6 +188,8 @@ class SQLDatabase {
179
188
  }
180
189
 
181
190
  func rollbackTransaction() throws {
191
+ operationLock.lock()
192
+ defer { operationLock.unlock() }
182
193
  guard inTransaction else {
183
194
  throw SQLError.noTransactionActive
184
195
  }
@@ -197,6 +208,10 @@ class SQLDatabase {
197
208
  }
198
209
 
199
210
  // Handle different types
211
+ if let data = value as? Data {
212
+ if data.isEmpty { return sqlite3_bind_zeroblob(stmt, index, 0) }
213
+ return data.withUnsafeBytes { sqlite3_bind_blob(stmt, index, $0.baseAddress, Int32(data.count), sqliteTransient) }
214
+ }
200
215
  if let str = value as? String {
201
216
  return sqlite3_bind_text(stmt, index, str, -1, sqliteTransient)
202
217
  } else if let num = value as? NSNumber {
@@ -227,7 +242,7 @@ class SQLDatabase {
227
242
  return SQLITE_ERROR
228
243
  }
229
244
 
230
- private func getColumnValue(stmt: OpaquePointer?, index: Int32) -> Any {
245
+ private func getColumnValue(stmt: OpaquePointer?, index: Int32, binaryResults: Bool) -> Any {
231
246
  guard let stmt = stmt else {
232
247
  return NSNull()
233
248
  }
@@ -246,11 +261,13 @@ class SQLDatabase {
246
261
  return NSNull()
247
262
  case SQLITE_BLOB:
248
263
  if sqlite3_column_bytes(stmt, index) == 0 {
264
+ if binaryResults { return Data() }
249
265
  return ["_type": "binary", "_data": ""]
250
266
  }
251
267
  if let blob = sqlite3_column_blob(stmt, index) {
252
268
  let size = sqlite3_column_bytes(stmt, index)
253
269
  let data = Data(bytes: blob, count: Int(size))
270
+ if binaryResults { return data }
254
271
  return [
255
272
  "_type": "binary",
256
273
  "_data": data.base64EncodedString()
@@ -100,7 +100,7 @@ class SQLHTTPServer {
100
100
  guard let server = server else { return }
101
101
 
102
102
  // CORS preflight for all endpoints
103
- let preflightPaths = ["/execute", "/batch", "/transaction/begin", "/transaction/commit", "/transaction/rollback"]
103
+ let preflightPaths = ["/execute", "/batch", "/echo", "/transaction/begin", "/transaction/commit", "/transaction/rollback"]
104
104
  for path in preflightPaths {
105
105
  server.route(.OPTIONS, path) { [weak self] _ in
106
106
  guard let self = self else {
@@ -126,6 +126,11 @@ class SQLHTTPServer {
126
126
  return self.addCorsHeaders(self.handleBatchRequest(request: request))
127
127
  }
128
128
 
129
+ server.route(.POST, "/echo") { [weak self] request in
130
+ guard let self = self else { return HTTPResponse(.internalServerError) }
131
+ return self.addCorsHeaders(self.handleDataRequest(request: request, kind: "echo"))
132
+ }
133
+
129
134
  // Transaction endpoints
130
135
  server.route(.POST, "/transaction/begin") { [weak self] request in
131
136
  guard let self = self else {
@@ -196,90 +201,68 @@ class SQLHTTPServer {
196
201
  // MARK: - Request Handlers
197
202
 
198
203
  private func handleExecuteRequest(request: HTTPRequest) -> HTTPResponse {
199
- // Authenticate
200
- guard let authHeader = request.headers["Authorization"],
201
- authHeader == "Bearer \(token)" else {
202
- return HTTPResponse(.unauthorized)
203
- }
204
-
205
- // Get database name
206
- guard let database = request.headers["X-Database"] else {
207
- let bodyData = "Database header required".data(using: .utf8)!
208
- return HTTPResponse(.badRequest, body: bodyData)
209
- }
210
-
211
- // Check if database exists
212
- guard let db = registeredDatabase(forKey: database) else {
213
- let bodyData = "Database not found".data(using: .utf8)!
214
- return HTTPResponse(.notFound, body: bodyData)
215
- }
216
-
217
- // Parse request body
218
- let bodyData = request.body
219
- guard let json = try? JSONSerialization.jsonObject(with: bodyData) as? [String: Any],
220
- let statement = json["statement"] as? String else {
221
- let errorData = "Invalid request body".data(using: .utf8)!
222
- return HTTPResponse(.badRequest, body: errorData)
223
- }
224
-
225
- let params = (json["params"] as? [Any] ?? [])
226
-
227
- do {
228
- let result = try db.execute(statement: statement, params: params)
229
- let resultData = try JSONSerialization.data(withJSONObject: result)
230
- return HTTPResponse(.ok, headers: [.contentType: "application/json"], body: resultData)
231
- } catch {
232
- let errorData = "Error: \(error.localizedDescription)".data(using: .utf8)!
233
- return HTTPResponse(.internalServerError, body: errorData)
234
- }
204
+ return handleDataRequest(request: request, kind: "execute")
235
205
  }
236
206
 
237
207
  private func handleBatchRequest(request: HTTPRequest) -> HTTPResponse {
238
- // Authenticate
239
- guard let authHeader = request.headers["Authorization"],
240
- authHeader == "Bearer \(token)" else {
241
- return HTTPResponse(.unauthorized)
242
- }
243
-
244
- // Get database name
245
- guard let database = request.headers["X-Database"] else {
246
- let bodyData = "Database header required".data(using: .utf8)!
247
- return HTTPResponse(.badRequest, body: bodyData)
248
- }
249
-
250
- // Check if database exists
251
- guard let db = registeredDatabase(forKey: database) else {
252
- let bodyData = "Database not found".data(using: .utf8)!
253
- return HTTPResponse(.notFound, body: bodyData)
254
- }
255
-
256
- // Parse request body
257
- let bodyData = request.body
258
- guard let json = try? JSONSerialization.jsonObject(with: bodyData) as? [String: Any],
259
- let operations = json["operations"] as? [[String: Any]] else {
260
- let errorData = "Invalid request body".data(using: .utf8)!
261
- return HTTPResponse(.badRequest, body: errorData)
262
- }
263
-
264
- var results: [[String: Any]] = []
208
+ return handleDataRequest(request: request, kind: "batch")
209
+ }
265
210
 
211
+ private func handleDataRequest(request: HTTPRequest, kind: String) -> HTTPResponse {
212
+ guard request.headers["Authorization"] == "Bearer \(token)" else { return HTTPResponse(.unauthorized) }
213
+ guard let database = request.headers["X-Database"] else { return HTTPResponse(.badRequest) }
214
+ guard let db = registeredDatabase(forKey: database) else { return HTTPResponse(.notFound) }
215
+ guard request.body.count <= BinaryFrame.maxBytes else { return HTTPResponse(.payloadTooLarge) }
216
+ let binary = request.headers["Content-Type"] == BinaryFrame.contentType
266
217
  do {
267
- for operation in operations {
268
- guard let statement = operation["statement"] as? String else {
269
- let errorData = "Invalid operation: missing statement".data(using: .utf8)!
270
- return HTTPResponse(.badRequest, body: errorData)
218
+ let decoded: Any
219
+ if binary {
220
+ decoded = try BinaryFrame.decode(request.body)
221
+ } else {
222
+ guard let json = try? JSONSerialization.jsonObject(with: request.body) else { throw BinaryFrameError.invalid }
223
+ decoded = json
224
+ }
225
+ guard let json = decoded as? [String: Any] else { throw BinaryFrameError.invalid }
226
+ let result: Any
227
+ if kind == "batch" {
228
+ guard let operations = json["operations"] as? [[String: Any]], operations.count <= 10000 else { throw BinaryFrameError.invalid }
229
+ let parsed = try operations.map { try parseOperation($0) }
230
+ result = try parsed.map { try db.execute(statement: $0.statement, params: $0.params, binaryResults: binary) }
231
+ } else {
232
+ let operation = try parseOperation(json)
233
+ let params = operation.params
234
+ if kind == "echo" {
235
+ guard params.count == 1 else { throw BinaryFrameError.invalid }
236
+ result = ["rows": [["value": params[0]]], "rowsAffected": 0]
237
+ } else {
238
+ result = try db.execute(statement: operation.statement, params: params, binaryResults: binary)
271
239
  }
272
- let params = (operation["params"] as? [Any] ?? [])
273
- let result = try db.execute(statement: statement, params: params)
274
- results.append(result)
275
240
  }
276
-
277
- let resultData = try JSONSerialization.data(withJSONObject: results)
278
- return HTTPResponse(.ok, headers: [.contentType: "application/json"], body: resultData)
241
+ let data: Data
242
+ do {
243
+ if !binary && !JSONSerialization.isValidJSONObject(result) { throw BinaryFrameError.invalid }
244
+ data = try binary ? BinaryFrame.encode(result) : JSONSerialization.data(withJSONObject: result)
245
+ } catch {
246
+ return HTTPResponse(.internalServerError, body: Data("Could not encode SQL response; a write may already be committed".utf8))
247
+ }
248
+ return HTTPResponse(.ok, headers: [.contentType: binary ? BinaryFrame.contentType : "application/json"], body: data)
249
+ } catch is BinaryFrameError {
250
+ return HTTPResponse(.badRequest, body: Data("Invalid binary/SQL request".utf8))
279
251
  } catch {
280
- let errorData = "Error: \(error.localizedDescription)".data(using: .utf8)!
281
- return HTTPResponse(.internalServerError, body: errorData)
252
+ return HTTPResponse(.internalServerError, body: Data("Error: \(error.localizedDescription)".utf8))
253
+ }
254
+ }
255
+
256
+ private func parseOperation(_ operation: [String: Any]) throws -> (statement: String, params: [Any]) {
257
+ guard let statement = operation["statement"] as? String else { throw BinaryFrameError.invalid }
258
+ if let raw = operation["params"], !(raw is [Any]) { throw BinaryFrameError.invalid }
259
+ for value in operation["params"] as? [Any] ?? [] {
260
+ if value is NSNull || value is String || value is Data { continue }
261
+ if let number = value as? NSNumber, number.doubleValue.isFinite { continue }
262
+ if let blob = value as? [String: Any], blob["_type"] as? String == "binary", blob["_data"] is String { continue }
263
+ throw BinaryFrameError.invalid
282
264
  }
265
+ return (statement, operation["params"] as? [Any] ?? [])
283
266
  }
284
267
 
285
268
  private func handleBeginTransactionRequest(request: HTTPRequest) -> HTTPResponse {
@@ -0,0 +1,65 @@
1
+ import XCTest
2
+ @testable import CapgoCapacitorFastSqlPlugin
3
+
4
+ final class BinaryFrameTests: XCTestCase {
5
+ func testHTTPRejectsInfiniteResponsesWithoutCrashing() async throws {
6
+ let database = try SQLDatabase(path: ":memory:")
7
+ let server = try SQLHTTPServer(databases: ["test": database])
8
+ try server.start()
9
+ defer { server.stop(); database.close() }
10
+ for binary in [false, true] {
11
+ let url = try XCTUnwrap(URL(string: "http://127.0.0.1:\(server.port)/execute"))
12
+ var request = URLRequest(url: url)
13
+ request.httpMethod = "POST"
14
+ request.timeoutInterval = 10
15
+ request.setValue("Bearer \(server.token)", forHTTPHeaderField: "Authorization")
16
+ request.setValue("test", forHTTPHeaderField: "X-Database")
17
+ request.setValue(binary ? BinaryFrame.contentType : "application/json", forHTTPHeaderField: "Content-Type")
18
+ let payload: [String: Any] = ["statement": "SELECT 1e999 AS value", "params": []]
19
+ request.httpBody = try binary ? BinaryFrame.encode(payload) : JSONSerialization.data(withJSONObject: payload)
20
+ let (_, response) = try await URLSession.shared.data(for: request)
21
+ XCTAssertEqual((response as? HTTPURLResponse)?.statusCode, 500)
22
+ }
23
+ }
24
+
25
+ func testSharedJavaScriptGolden() throws {
26
+ let url = try XCTUnwrap(Bundle.module.url(forResource: "binary-v1", withExtension: "bin", subdirectory: "Fixtures"))
27
+ let request = try XCTUnwrap(try BinaryFrame.decode(Data(contentsOf: url)) as? [String: Any])
28
+ let params = try XCTUnwrap(request["params"] as? [Any])
29
+ XCTAssertEqual(params[0] as? Data, Data([0, 1, 255]))
30
+ XCTAssertEqual(params[1] as? Data, Data())
31
+ XCTAssertEqual(params[2] as? Double, 1.25)
32
+ XCTAssertEqual(params[3] as? String, "😀")
33
+ }
34
+
35
+ func testRawDataSQLiteRoundTrip() throws {
36
+ let database = try SQLDatabase(path: ":memory:")
37
+ defer { database.close() }
38
+ let result = try database.execute(statement: "SELECT ? AS bytes, ? AS empty, ? AS missing, ? AS fraction", params: [Data([0, 1, 255]), Data(), NSNull(), 1.25], binaryResults: true)
39
+ let decoded = try XCTUnwrap(try BinaryFrame.decode(BinaryFrame.encode(result)) as? [String: Any])
40
+ let rows = try XCTUnwrap(decoded["rows"] as? [[String: Any]])
41
+ XCTAssertEqual(rows[0]["bytes"] as? Data, Data([0, 1, 255]))
42
+ XCTAssertEqual(rows[0]["empty"] as? Data, Data())
43
+ XCTAssertTrue(rows[0]["missing"] is NSNull)
44
+ XCTAssertEqual(rows[0]["fraction"] as? Double, 1.25)
45
+ }
46
+
47
+ func testMalformedFrames() throws {
48
+ for number in [Double.infinity, -Double.infinity, Double.nan] {
49
+ XCTAssertThrowsError(try BinaryFrame.encode(["rows": [["value": number]]]))
50
+ }
51
+ let database = try SQLDatabase(path: ":memory:")
52
+ defer { database.close() }
53
+ let infinite = try database.execute(statement: "SELECT 1e999 AS value", params: [], binaryResults: true)
54
+ XCTAssertThrowsError(try BinaryFrame.encode(infinite))
55
+ XCTAssertFalse(JSONSerialization.isValidJSONObject(infinite))
56
+ let valid = try BinaryFrame.encode(["value": Data([1, 2])])
57
+ XCTAssertThrowsError(try BinaryFrame.decode(valid.dropLast()))
58
+ var version = valid
59
+ version[3] = 50
60
+ XCTAssertThrowsError(try BinaryFrame.decode(version))
61
+ for reference in [[-1.0, 1], [0, 999], [0.5, 0]] {
62
+ XCTAssertThrowsError(try BinaryFrame.decode(BinaryFrame.encode(["$blob": reference])))
63
+ }
64
+ }
65
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-fast-sql",
3
- "version": "8.2.18",
3
+ "version": "8.3.1",
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",
@@ -57,7 +57,8 @@
57
57
  "example:install": "cd example-app && bun install --frozen-lockfile",
58
58
  "example:build": "bun run build && cd example-app && bun install --frozen-lockfile && bun run build",
59
59
  "changelog:ai": "node scripts/generate-ai-changelog.mjs",
60
- "benchmark:check": "bun test tests/benchmark.test.ts && tsc -p benchmarks/tsconfig.json"
60
+ "benchmark:check": "bun test tests/benchmark.test.ts && tsc -p benchmarks/tsconfig.json",
61
+ "test:unit": "bun test tests"
61
62
  },
62
63
  "dependencies": {
63
64
  "@sqlite.org/sqlite-wasm": "3.53.0-build1"