expo-sqlite 58.0.9 → 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 +12 -0
- package/android/build.gradle +2 -2
- package/ios/Benchmarks/JavaScriptBenchmarks.swift +65 -0
- package/ios/DatabaseChangeEvent.swift +4 -3
- package/ios/NativeStatement.swift +156 -0
- package/ios/SQLiteBindValue.swift +39 -0
- package/ios/SQLiteColumnValue.swift +54 -0
- package/ios/SQLiteModule.swift +3 -191
- 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 +69 -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.9/expo.modules.sqlite-58.0.9.module → 58.0.10/expo.modules.sqlite-58.0.10.module} +17 -17
- 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.9/expo.modules.sqlite-58.0.9.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 +3 -3
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.9/expo.modules.sqlite-58.0.9.aar +0 -0
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.9/expo.modules.sqlite-58.0.9.aar.md5 +0 -1
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.9/expo.modules.sqlite-58.0.9.aar.sha1 +0 -1
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.9/expo.modules.sqlite-58.0.9.aar.sha256 +0 -1
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.9/expo.modules.sqlite-58.0.9.aar.sha512 +0 -1
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.9/expo.modules.sqlite-58.0.9.module.md5 +0 -1
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.9/expo.modules.sqlite-58.0.9.module.sha1 +0 -1
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.9/expo.modules.sqlite-58.0.9.module.sha256 +0 -1
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.9/expo.modules.sqlite-58.0.9.module.sha512 +0 -1
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.9/expo.modules.sqlite-58.0.9.pom.md5 +0 -1
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.9/expo.modules.sqlite-58.0.9.pom.sha1 +0 -1
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.9/expo.modules.sqlite-58.0.9.pom.sha256 +0 -1
- package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.9/expo.modules.sqlite-58.0.9.pom.sha512 +0 -1
- /package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/{58.0.9/expo.modules.sqlite-58.0.9-sources.jar → 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.9/expo.modules.sqlite-58.0.9-sources.jar.md5 → 58.0.10/expo.modules.sqlite-58.0.10-sources.jar.md5} +0 -0
- /package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/{58.0.9/expo.modules.sqlite-58.0.9-sources.jar.sha1 → 58.0.10/expo.modules.sqlite-58.0.10-sources.jar.sha1} +0 -0
- /package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/{58.0.9/expo.modules.sqlite-58.0.9-sources.jar.sha256 → 58.0.10/expo.modules.sqlite-58.0.10-sources.jar.sha256} +0 -0
- /package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/{58.0.9/expo.modules.sqlite-58.0.9-sources.jar.sha512 → 58.0.10/expo.modules.sqlite-58.0.10-sources.jar.sha512} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 58.0.10
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [iOS] Speed up binding parameters and reading rows by converting them through typed values instead of `Any`. Reading rows is up to about 3.3 times faster and inserts up to about 2.8 times faster in the native benchmarks.
|
|
8
|
+
|
|
9
|
+
A `bigint` bind parameter now binds as a 64-bit integer instead of `NULL`. It still reads back as a number, with the same precision loss above 2^53 as other integer columns.
|
|
10
|
+
|
|
11
|
+
`onDatabaseChange` no longer drops the event for a row id above 2^53.
|
|
12
|
+
|
|
13
|
+
([#50894](https://github.com/expo/expo/pull/50894) by [@tsapeta](https://github.com/tsapeta))
|
|
14
|
+
|
|
3
15
|
## 58.0.9
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/android/build.gradle
CHANGED
|
@@ -47,13 +47,13 @@ def reactNativeArchitectures() {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
group = 'host.exp.exponent'
|
|
50
|
-
version = '58.0.
|
|
50
|
+
version = '58.0.10'
|
|
51
51
|
|
|
52
52
|
android {
|
|
53
53
|
namespace "expo.modules.sqlite"
|
|
54
54
|
defaultConfig {
|
|
55
55
|
versionCode 18
|
|
56
|
-
versionName "58.0.
|
|
56
|
+
versionName "58.0.10"
|
|
57
57
|
buildConfigField "boolean", "WITH_SQLITE_VEC", project.ext.WITH_SQLITE_VEC.toString()
|
|
58
58
|
|
|
59
59
|
externalNativeBuild {
|
|
@@ -177,6 +177,71 @@ extension Benchmarks {
|
|
|
177
177
|
}
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
/// `SQLiteDatabase.runSync(source, { $name: value })`: named parameters resolve their index through
|
|
181
|
+
/// `sqlite3_bind_parameter_index` instead of the array position.
|
|
182
|
+
@Test
|
|
183
|
+
func `JS: insert with named parameters`() async throws {
|
|
184
|
+
try await sqliteBenchmarkCase { appContext in
|
|
185
|
+
let runtime = try prepareRuntime(appContext)
|
|
186
|
+
_ = try runtime.eval(
|
|
187
|
+
"""
|
|
188
|
+
var insert = new NativeStatement();
|
|
189
|
+
db.prepareSync(insert, 'INSERT INTO t (int_value, real_value, text_value, null_value) VALUES ($i, $r, $t, $n)');
|
|
190
|
+
"""
|
|
191
|
+
)
|
|
192
|
+
let run = try driver(
|
|
193
|
+
runtime,
|
|
194
|
+
"insert.runSync(db, { $i: i, $r: 1.5, $t: 'row', $n: null }, {}, false);"
|
|
195
|
+
)
|
|
196
|
+
try benchmark("JS: runSync insert with 4 named parameters", runtime: runtime) { iterations in
|
|
197
|
+
_ = try run.call(arguments: iterations)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/// `SQLiteDatabase.runSync(source, [blob])`: the blob goes through the separate blob parameters.
|
|
203
|
+
@Test
|
|
204
|
+
func `JS: insert with a blob parameter`() async throws {
|
|
205
|
+
try await sqliteBenchmarkCase { appContext in
|
|
206
|
+
let runtime = try prepareRuntime(appContext)
|
|
207
|
+
_ = try runtime.eval(
|
|
208
|
+
"""
|
|
209
|
+
var insert = new NativeStatement();
|
|
210
|
+
db.prepareSync(insert, 'INSERT INTO blobs (data) VALUES (?)');
|
|
211
|
+
var blob = new Uint8Array(1024);
|
|
212
|
+
"""
|
|
213
|
+
)
|
|
214
|
+
let run = try driver(runtime, "insert.runSync(db, {}, { 0: blob }, true);")
|
|
215
|
+
try benchmark("JS: runSync insert with a 1 KiB blob parameter", runtime: runtime) { iterations in
|
|
216
|
+
_ = try run.call(arguments: iterations)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/// `SQLiteStatement.executeSync(...)` iterated row by row: one native call per row.
|
|
222
|
+
@Test
|
|
223
|
+
func `JS: step through 100 rows`() async throws {
|
|
224
|
+
try await sqliteBenchmarkCase { appContext in
|
|
225
|
+
let runtime = try prepareRuntime(appContext)
|
|
226
|
+
_ = try runtime.eval(
|
|
227
|
+
"""
|
|
228
|
+
var cursor = new NativeStatement();
|
|
229
|
+
db.prepareSync(cursor, 'SELECT \(BenchmarkSchema.selectColumns) FROM t WHERE id <= 100');
|
|
230
|
+
"""
|
|
231
|
+
)
|
|
232
|
+
let run = try driver(
|
|
233
|
+
runtime,
|
|
234
|
+
"""
|
|
235
|
+
cursor.resetSync(db);
|
|
236
|
+
while (cursor.stepSync(db) != null) {}
|
|
237
|
+
"""
|
|
238
|
+
)
|
|
239
|
+
try benchmark("JS: stepSync through 100 rows", runtime: runtime) { iterations in
|
|
240
|
+
_ = try run.call(arguments: iterations)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
180
245
|
@Test
|
|
181
246
|
func `JS: column names of a prepared statement`() async throws {
|
|
182
247
|
try await sqliteBenchmarkCase { appContext in
|
|
@@ -8,8 +8,9 @@ struct DatabaseChangeEvent {
|
|
|
8
8
|
var databaseName: String
|
|
9
9
|
var databaseFilePath: String
|
|
10
10
|
var tableName: String
|
|
11
|
-
// `
|
|
12
|
-
//
|
|
13
|
-
|
|
11
|
+
// A `Double` rather than SQLite's `Int64` or an `Int`: JavaScript reads the row id as a number, an
|
|
12
|
+
// `Int64` would encode as a BigInt, and an `Int` throws above 2^53, which would drop the event. Above
|
|
13
|
+
// 2^53 it loses precision, like integer columns do.
|
|
14
|
+
var rowId: Double
|
|
14
15
|
var typeId: SQLAction
|
|
15
16
|
}
|
|
@@ -57,6 +57,46 @@ final class NativeStatement: SharedObject, @unchecked Sendable {
|
|
|
57
57
|
try finalize(database: database)
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
@JS(.concurrent)
|
|
61
|
+
func runAsync(
|
|
62
|
+
database: NativeDatabase,
|
|
63
|
+
bindParams: [String: SQLiteBindValue?],
|
|
64
|
+
bindBlobParams: [String: ArrayBuffer],
|
|
65
|
+
shouldPassAsArray: Bool
|
|
66
|
+
) async throws -> SQLiteRunResult {
|
|
67
|
+
return try run(database: database, bindParams: bindParams, bindBlobParams: bindBlobParams, shouldPassAsArray: shouldPassAsArray)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@JS
|
|
71
|
+
func runSync(
|
|
72
|
+
database: NativeDatabase,
|
|
73
|
+
bindParams: [String: SQLiteBindValue?],
|
|
74
|
+
bindBlobParams: [String: ArrayBuffer],
|
|
75
|
+
shouldPassAsArray: Bool
|
|
76
|
+
) throws -> SQLiteRunResult {
|
|
77
|
+
return try run(database: database, bindParams: bindParams, bindBlobParams: bindBlobParams, shouldPassAsArray: shouldPassAsArray)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@JS(.concurrent)
|
|
81
|
+
func stepAsync(database: NativeDatabase) async throws -> [SQLiteColumnValue?]? {
|
|
82
|
+
return try step(database: database)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@JS
|
|
86
|
+
func stepSync(database: NativeDatabase) throws -> [SQLiteColumnValue?]? {
|
|
87
|
+
return try step(database: database)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
@JS(.concurrent)
|
|
91
|
+
func getAllAsync(database: NativeDatabase) async throws -> [[SQLiteColumnValue?]] {
|
|
92
|
+
return try getAll(database: database)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@JS
|
|
96
|
+
func getAllSync(database: NativeDatabase) throws -> [[SQLiteColumnValue?]] {
|
|
97
|
+
return try getAll(database: database)
|
|
98
|
+
}
|
|
99
|
+
|
|
60
100
|
// MARK: - Implementation shared by the sync and async members
|
|
61
101
|
|
|
62
102
|
private func reset(database: NativeDatabase) throws {
|
|
@@ -99,6 +139,122 @@ final class NativeStatement: SharedObject, @unchecked Sendable {
|
|
|
99
139
|
}
|
|
100
140
|
}
|
|
101
141
|
}
|
|
142
|
+
|
|
143
|
+
private func run(
|
|
144
|
+
database: NativeDatabase,
|
|
145
|
+
bindParams: [String: SQLiteBindValue?],
|
|
146
|
+
bindBlobParams: [String: ArrayBuffer],
|
|
147
|
+
shouldPassAsArray: Bool
|
|
148
|
+
) throws -> SQLiteRunResult {
|
|
149
|
+
return try lock.withLock { _ in
|
|
150
|
+
try ensureNotFinalized()
|
|
151
|
+
try database.ensureOpen()
|
|
152
|
+
|
|
153
|
+
exsqlite3_reset(pointer)
|
|
154
|
+
exsqlite3_clear_bindings(pointer)
|
|
155
|
+
for (key, param) in bindParams {
|
|
156
|
+
let index = try bindParamIndex(for: key, shouldPassAsArray: shouldPassAsArray)
|
|
157
|
+
guard index > 0 else {
|
|
158
|
+
continue
|
|
159
|
+
}
|
|
160
|
+
if let param {
|
|
161
|
+
param.bind(to: pointer, at: index)
|
|
162
|
+
} else {
|
|
163
|
+
exsqlite3_bind_null(pointer, index)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
for (key, param) in bindBlobParams {
|
|
167
|
+
let index = try bindParamIndex(for: key, shouldPassAsArray: shouldPassAsArray)
|
|
168
|
+
if index > 0 {
|
|
169
|
+
SQLiteBindValue.blob(param).bind(to: pointer, at: index)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let ret = exsqlite3_step(pointer)
|
|
174
|
+
if ret != SQLITE_ROW && ret != SQLITE_DONE {
|
|
175
|
+
throw SQLiteErrorException(database.lastErrorMessage())
|
|
176
|
+
}
|
|
177
|
+
return SQLiteRunResult(
|
|
178
|
+
lastInsertRowId: Double(exsqlite3_last_insert_rowid(database.pointer)),
|
|
179
|
+
changes: Int(exsqlite3_changes(database.pointer)),
|
|
180
|
+
firstRowValues: ret == SQLITE_ROW ? try columnValues() : []
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private func step(database: NativeDatabase) throws -> [SQLiteColumnValue?]? {
|
|
186
|
+
return try lock.withLock { _ in
|
|
187
|
+
try ensureNotFinalized()
|
|
188
|
+
try database.ensureOpen()
|
|
189
|
+
|
|
190
|
+
let ret = exsqlite3_step(pointer)
|
|
191
|
+
if ret == SQLITE_ROW {
|
|
192
|
+
return try columnValues()
|
|
193
|
+
}
|
|
194
|
+
if ret != SQLITE_DONE {
|
|
195
|
+
throw SQLiteErrorException(database.lastErrorMessage())
|
|
196
|
+
}
|
|
197
|
+
return nil
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private func getAll(database: NativeDatabase) throws -> [[SQLiteColumnValue?]] {
|
|
202
|
+
return try lock.withLock { _ in
|
|
203
|
+
try ensureNotFinalized()
|
|
204
|
+
try database.ensureOpen()
|
|
205
|
+
|
|
206
|
+
var rows: [[SQLiteColumnValue?]] = []
|
|
207
|
+
while true {
|
|
208
|
+
let ret = exsqlite3_step(pointer)
|
|
209
|
+
if ret == SQLITE_ROW {
|
|
210
|
+
rows.append(try columnValues())
|
|
211
|
+
continue
|
|
212
|
+
}
|
|
213
|
+
if ret == SQLITE_DONE {
|
|
214
|
+
break
|
|
215
|
+
}
|
|
216
|
+
throw SQLiteErrorException(database.lastErrorMessage())
|
|
217
|
+
}
|
|
218
|
+
return rows
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/// The values of the current row. Call it inside `lock`, after a step that returned `SQLITE_ROW`.
|
|
223
|
+
private func columnValues() throws -> [SQLiteColumnValue?] {
|
|
224
|
+
let columnCount = exsqlite3_column_count(pointer)
|
|
225
|
+
var values: [SQLiteColumnValue?] = []
|
|
226
|
+
values.reserveCapacity(Int(columnCount))
|
|
227
|
+
for index in 0..<columnCount {
|
|
228
|
+
values.append(try SQLiteColumnValue.read(from: pointer, column: index))
|
|
229
|
+
}
|
|
230
|
+
return values
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/// The 1-based SQLite index of a bind parameter: an array position for positional parameters, or the
|
|
234
|
+
/// index of the named parameter otherwise. Zero means the statement has no such parameter.
|
|
235
|
+
private func bindParamIndex(for key: String, shouldPassAsArray: Bool) throws -> Int32 {
|
|
236
|
+
if shouldPassAsArray {
|
|
237
|
+
guard let position = Int32(key) else {
|
|
238
|
+
throw InvalidBindParameterException()
|
|
239
|
+
}
|
|
240
|
+
return position + 1
|
|
241
|
+
}
|
|
242
|
+
return exsqlite3_bind_parameter_index(pointer, key)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/// The result of `NativeStatement.run`: the effect of the statement and the values of the first row
|
|
247
|
+
/// it returned, if any.
|
|
248
|
+
@Record
|
|
249
|
+
struct SQLiteRunResult {
|
|
250
|
+
// A `Double` rather than SQLite's `Int64` or an `Int`: JavaScript reads the row id as a number, an
|
|
251
|
+
// `Int64` would encode as a BigInt, and an `Int` throws above 2^53. The row id is connection-wide, so a
|
|
252
|
+
// throw would also fail every later run on the connection. Above 2^53 it loses precision, like integer
|
|
253
|
+
// columns do.
|
|
254
|
+
var lastInsertRowId: Double
|
|
255
|
+
// `sqlite3_changes` returns a 32-bit count, which always fits a JavaScript number.
|
|
256
|
+
var changes: Int
|
|
257
|
+
var firstRowValues: [SQLiteColumnValue?]
|
|
102
258
|
}
|
|
103
259
|
|
|
104
260
|
// `==` lives in an extension: an operator declared inside a type that carries a member-attribute macro
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Copyright 2015-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import ExpoModulesCore
|
|
4
|
+
|
|
5
|
+
/// A bind parameter as it comes from JavaScript: `number | string | boolean | bigint | ArrayBuffer`.
|
|
6
|
+
/// `null` arrives as `nil` of the optional around it.
|
|
7
|
+
///
|
|
8
|
+
/// A union decodes its cases in declaration order and the first match wins, so `double` goes first:
|
|
9
|
+
/// the `Int64` decode accepts any number and rounds it, which would turn `1.5` into `2`. Ordered after
|
|
10
|
+
/// `double`, `integer` only matches a `bigint`.
|
|
11
|
+
@Union
|
|
12
|
+
enum SQLiteBindValue: Sendable {
|
|
13
|
+
case double(Double)
|
|
14
|
+
case text(String)
|
|
15
|
+
case boolean(Bool)
|
|
16
|
+
case integer(Int64)
|
|
17
|
+
case blob(ArrayBuffer)
|
|
18
|
+
|
|
19
|
+
/// Binds the value to the parameter at `index` of `statement`.
|
|
20
|
+
@inline(__always)
|
|
21
|
+
func bind(to statement: OpaquePointer?, at index: Int32) {
|
|
22
|
+
switch self {
|
|
23
|
+
case .double(let value):
|
|
24
|
+
exsqlite3_bind_double(statement, index, value)
|
|
25
|
+
case .text(let value):
|
|
26
|
+
exsqlite3_bind_text(statement, index, value, -1, SQLITE_TRANSIENT)
|
|
27
|
+
case .boolean(let value):
|
|
28
|
+
exsqlite3_bind_int(statement, index, value ? 1 : 0)
|
|
29
|
+
case .integer(let value):
|
|
30
|
+
exsqlite3_bind_int64(statement, index, value)
|
|
31
|
+
case .blob(let value):
|
|
32
|
+
_ = value.withUnsafeBytes {
|
|
33
|
+
exsqlite3_bind_blob(statement, index, $0.baseAddress, Int32(value.byteLength), SQLITE_TRANSIENT)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private let SQLITE_TRANSIENT = unsafeBitCast(OpaquePointer(bitPattern: -1), to: sqlite3_destructor_type.self)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Copyright 2015-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import ExpoModulesCore
|
|
4
|
+
|
|
5
|
+
/// A column value as it goes to JavaScript: `number | string | ArrayBuffer`. `NULL` goes as `nil` of
|
|
6
|
+
/// the optional around it.
|
|
7
|
+
///
|
|
8
|
+
/// `INTEGER` columns are read as `double`, so they reach JavaScript as a number, not a `bigint`. That
|
|
9
|
+
/// loses precision above 2^53, as the type-erased conversion did before.
|
|
10
|
+
@Union
|
|
11
|
+
enum SQLiteColumnValue: Sendable, Equatable {
|
|
12
|
+
case double(Double)
|
|
13
|
+
case text(String)
|
|
14
|
+
case blob(ArrayBuffer)
|
|
15
|
+
|
|
16
|
+
/// Reads the value of the column at `index` in the current row of `statement`, or `nil` for `NULL`.
|
|
17
|
+
@inline(__always)
|
|
18
|
+
static func read(from statement: OpaquePointer?, column index: Int32) throws -> SQLiteColumnValue? {
|
|
19
|
+
switch exsqlite3_column_type(statement, index) {
|
|
20
|
+
case SQLITE_INTEGER:
|
|
21
|
+
return .double(Double(exsqlite3_column_int64(statement, index)))
|
|
22
|
+
case SQLITE_FLOAT:
|
|
23
|
+
return .double(exsqlite3_column_double(statement, index))
|
|
24
|
+
case SQLITE_TEXT:
|
|
25
|
+
guard let text = exsqlite3_column_text(statement, index) else {
|
|
26
|
+
throw InvalidConvertibleException("Null text")
|
|
27
|
+
}
|
|
28
|
+
return .text(String(cString: text))
|
|
29
|
+
case SQLITE_BLOB:
|
|
30
|
+
guard let blob = exsqlite3_column_blob(statement, index) else {
|
|
31
|
+
return .blob(ArrayBuffer(size: 0))
|
|
32
|
+
}
|
|
33
|
+
let size = exsqlite3_column_bytes(statement, index)
|
|
34
|
+
return .blob(ArrayBuffer.copy(of: blob, count: Int(size)))
|
|
35
|
+
case SQLITE_NULL:
|
|
36
|
+
return nil
|
|
37
|
+
case let type:
|
|
38
|
+
throw InvalidConvertibleException("Unsupported column type: \(type)")
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
static func == (lhs: SQLiteColumnValue, rhs: SQLiteColumnValue) -> Bool {
|
|
43
|
+
switch (lhs, rhs) {
|
|
44
|
+
case let (.double(lhs), .double(rhs)):
|
|
45
|
+
return lhs == rhs
|
|
46
|
+
case let (.text(lhs), .text(rhs)):
|
|
47
|
+
return lhs == rhs
|
|
48
|
+
case let (.blob(lhs), .blob(rhs)):
|
|
49
|
+
return lhs.withUnsafeBytes { lhs in rhs.withUnsafeBytes { rhs in lhs.elementsEqual(rhs) } }
|
|
50
|
+
default:
|
|
51
|
+
return false
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
package/ios/SQLiteModule.swift
CHANGED
|
@@ -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:
|
|
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,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(
|
|
@@ -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
|
}
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fad06d1b56a5b3ce76fff8218367c30e
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
daaf8670dcbff2b9ab9410878e0a87e85eabad6f
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
c4c90adee96c2308551babad7ac1fca8e3782ab14771b68933f1151a03436333
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
8da2bcca001991403ad15af7ae5fe40b6f02cf613801c3c109fc64b0a25980c07019c47fca22cbfef180ce826225442d307fc3b4416cc29127fda0c3880c666c
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"component": {
|
|
4
4
|
"group": "host.exp.exponent",
|
|
5
5
|
"module": "expo.modules.sqlite",
|
|
6
|
-
"version": "58.0.
|
|
6
|
+
"version": "58.0.10",
|
|
7
7
|
"attributes": {
|
|
8
8
|
"org.gradle.status": "release"
|
|
9
9
|
}
|
|
@@ -24,13 +24,13 @@
|
|
|
24
24
|
},
|
|
25
25
|
"files": [
|
|
26
26
|
{
|
|
27
|
-
"name": "expo.modules.sqlite-58.0.
|
|
28
|
-
"url": "expo.modules.sqlite-58.0.
|
|
29
|
-
"size":
|
|
30
|
-
"sha512": "
|
|
31
|
-
"sha256": "
|
|
32
|
-
"sha1": "
|
|
33
|
-
"md5": "
|
|
27
|
+
"name": "expo.modules.sqlite-58.0.10.aar",
|
|
28
|
+
"url": "expo.modules.sqlite-58.0.10.aar",
|
|
29
|
+
"size": 3958905,
|
|
30
|
+
"sha512": "8da2bcca001991403ad15af7ae5fe40b6f02cf613801c3c109fc64b0a25980c07019c47fca22cbfef180ce826225442d307fc3b4416cc29127fda0c3880c666c",
|
|
31
|
+
"sha256": "c4c90adee96c2308551babad7ac1fca8e3782ab14771b68933f1151a03436333",
|
|
32
|
+
"sha1": "daaf8670dcbff2b9ab9410878e0a87e85eabad6f",
|
|
33
|
+
"md5": "fad06d1b56a5b3ce76fff8218367c30e"
|
|
34
34
|
}
|
|
35
35
|
]
|
|
36
36
|
},
|
|
@@ -67,13 +67,13 @@
|
|
|
67
67
|
],
|
|
68
68
|
"files": [
|
|
69
69
|
{
|
|
70
|
-
"name": "expo.modules.sqlite-58.0.
|
|
71
|
-
"url": "expo.modules.sqlite-58.0.
|
|
72
|
-
"size":
|
|
73
|
-
"sha512": "
|
|
74
|
-
"sha256": "
|
|
75
|
-
"sha1": "
|
|
76
|
-
"md5": "
|
|
70
|
+
"name": "expo.modules.sqlite-58.0.10.aar",
|
|
71
|
+
"url": "expo.modules.sqlite-58.0.10.aar",
|
|
72
|
+
"size": 3958905,
|
|
73
|
+
"sha512": "8da2bcca001991403ad15af7ae5fe40b6f02cf613801c3c109fc64b0a25980c07019c47fca22cbfef180ce826225442d307fc3b4416cc29127fda0c3880c666c",
|
|
74
|
+
"sha256": "c4c90adee96c2308551babad7ac1fca8e3782ab14771b68933f1151a03436333",
|
|
75
|
+
"sha1": "daaf8670dcbff2b9ab9410878e0a87e85eabad6f",
|
|
76
|
+
"md5": "fad06d1b56a5b3ce76fff8218367c30e"
|
|
77
77
|
}
|
|
78
78
|
]
|
|
79
79
|
},
|
|
@@ -87,8 +87,8 @@
|
|
|
87
87
|
},
|
|
88
88
|
"files": [
|
|
89
89
|
{
|
|
90
|
-
"name": "expo.modules.sqlite-58.0.
|
|
91
|
-
"url": "expo.modules.sqlite-58.0.
|
|
90
|
+
"name": "expo.modules.sqlite-58.0.10-sources.jar",
|
|
91
|
+
"url": "expo.modules.sqlite-58.0.10-sources.jar",
|
|
92
92
|
"size": 12434,
|
|
93
93
|
"sha512": "47d6b003042ef32b80e19b64fd015ffcfa159bb8f08971459211cd6ab971304da58dee6236c2be5319f606f9062d023530dcaa5c2e530bb17e42ab1465d5b95f",
|
|
94
94
|
"sha256": "67dbdc88a1d7fc60c31092ad150a98a30d93d8ca82bbf869609890965425524d",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ca75ac17a7818d5fc42a65c0840dfe04
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
c83619ccf6810e1e67de33d891531e884de12020
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
dfc2cb2d98cfef3bd6b5be8f805dd3444b34c4755dfc2f6ff0e0b653ba123cd3
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
73ae3ffc162990e89b804689ba3d929767b2619e57e56d78c277b4c772fd8cd284ff0cda1a1418342b5f21c68fd4538548ab273f38c390a31dbd5a6e8800143f
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
<modelVersion>4.0.0</modelVersion>
|
|
10
10
|
<groupId>host.exp.exponent</groupId>
|
|
11
11
|
<artifactId>expo.modules.sqlite</artifactId>
|
|
12
|
-
<version>58.0.
|
|
12
|
+
<version>58.0.10</version>
|
|
13
13
|
<packaging>aar</packaging>
|
|
14
14
|
<name>expo.modules.sqlite</name>
|
|
15
15
|
<url>https://github.com/expo/expo</url>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
92a9db327ca2c2ad57ff6a10c3ad05bd
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
d104ae54dc9a284a629157c7f4bc6a76d4b1d5e3
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
d46287f54d70926645728cdb6a436ad843ea632fbdf9274a29c6227de2e61532
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
584fceef6dc748071a484a1cee817dbc73fc8ce8838fbcb29a1d429a2b8eb36d608d7acd3a17c10f8e35a78ded5064bc01bb839376dc11b59e5c82937a23f4bf
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-sqlite",
|
|
3
|
-
"version": "58.0.
|
|
3
|
+
"version": "58.0.10",
|
|
4
4
|
"description": "Provides access to a database using SQLite (https://www.sqlite.org/). The database is persisted across restarts of your app.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"async-storage",
|
|
@@ -55,8 +55,8 @@
|
|
|
55
55
|
"@types/jest": "^30.0.0",
|
|
56
56
|
"@types/node": "^22.14.0",
|
|
57
57
|
"@types/react": "~19.3.0",
|
|
58
|
-
"expo": "58.0.
|
|
59
|
-
"expo-module-scripts": "56.0.
|
|
58
|
+
"expo": "58.0.4",
|
|
59
|
+
"expo-module-scripts": "56.0.7",
|
|
60
60
|
"react-error-boundary": "^4.0.11",
|
|
61
61
|
"tar": "^7.5.22"
|
|
62
62
|
},
|
package/local-maven-repo/host/exp/exponent/expo.modules.sqlite/58.0.9/expo.modules.sqlite-58.0.9.aar
DELETED
|
Binary file
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0c582335d4a9a710a780e9e636dbc73a
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
3ef893dc3f6aec4bda329646065115814fd42d0a
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
96b87814356fa93bcc9eb091c5b2f96f243fa661bd70740c352dae65cca23d8f
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
d3fbb6aad5541429fc0b0c3edb30d9e71f12636e9215617d2cb19750aecb36c4df1efc1bc5f84fb75f816d9db01c4ad9a0f08457d67ae892e2ebb59d3bd26246
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
027bb16e5aa93ebd85e027ee2f46126a
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
214b155b65fff08267e3c51f3c305d0a60393fcd
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
a1a758d13d8bfa0a508a0e8e76d619145bf6e9fed21e2793b4c3250c747ab0e2
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
edceb164c582da1e7acbb0d0e0b64074ef17bb2af5a95a63f0d37e2272b93e8be9802bae030c92050f7dc7b357aa14ccbfa864c78d3818301aec13fecb0b04b0
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
b2d62f1ac9414101b72082d1f7184972
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
4966c5a227bb7b1cd816624a1c7490c2214b26fd
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
2835028ead37c42578edaaa5b9c5f7610643bd86b825c6e099c90cbfdc764bd7
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
f3a422c4b7869c9b5bc74f3d85670925330e84ea0af7dbc1366e13122238618d5615159ab2db6d2e5d1b6a489ea69c2eee6051a395a64d4b4f6c841dc4c4bc23
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|