Skip to content

Commit 89fc335

Browse files
committed
Update Kotlin SDK
1 parent 0e144cf commit 89fc335

File tree

8 files changed

+147
-57
lines changed

8 files changed

+147
-57
lines changed

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
# Changelog
22

3-
## 1.5.1
3+
## 1.6.0 (unreleased)
44

55
* Update core extension to 0.4.6 ([changelog](https://github.com/powersync-ja/powersync-sqlite-core/releases/tag/v0.4.6))
6+
* Add `getCrudTransactions()`, returning an async sequence of transactions.
7+
* Compatibility with Swift 6.2 and XCode 26.
8+
9+
## 1.5.1
10+
11+
* Update core extension to 0.4.5 ([changelog](https://github.com/powersync-ja/powersync-sqlite-core/releases/tag/v0.4.5))
612
* Additional Swift 6 Strict Concurrency Checking declarations added for remaining protocols.
713
* Fix issue in legacy sync client where local writes made offline could have their upload delayed until a keepalive event was received. This could also cause downloaded updates to be delayed even further until all uploads were completed.
814

Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,9 @@ final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol,
4848
connector: PowerSyncBackendConnectorProtocol,
4949
options: ConnectOptions?
5050
) async throws {
51-
let connectorAdapter = PowerSyncBackendConnectorAdapter(
52-
swiftBackendConnector: connector,
53-
db: self
54-
)
51+
let connectorAdapter = swiftBackendConnectorToPowerSyncConnector(connector: SwiftBackendConnectorBridge(
52+
swiftBackendConnector: connector, db: self
53+
))
5554

5655
let resolvedOptions = options ?? ConnectOptions()
5756
try await kotlinDatabase.connect(
@@ -75,14 +74,9 @@ final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol,
7574
batch: base
7675
)
7776
}
78-
79-
func getNextCrudTransaction() async throws -> CrudTransaction? {
80-
guard let base = try await kotlinDatabase.getNextCrudTransaction() else {
81-
return nil
82-
}
83-
return try KotlinCrudTransaction(
84-
transaction: base
85-
)
77+
78+
func getCrudTransactions() -> CrudTransactions {
79+
return CrudTransactions(db: kotlinDatabase)
8680
}
8781

8882
func getPowerSyncVersion() async throws -> String {

Sources/PowerSync/Kotlin/KotlinTypes.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import PowerSyncKotlin
22

3+
typealias KotlinSwiftBackendConnector = PowerSyncKotlin.SwiftBackendConnector
34
typealias KotlinPowerSyncBackendConnector = PowerSyncKotlin.PowerSyncBackendConnector
45
typealias KotlinPowerSyncCredentials = PowerSyncKotlin.PowerSyncCredentials
56
typealias KotlinPowerSyncDatabase = PowerSyncKotlin.PowerSyncDatabase
Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import OSLog
2+
import PowerSyncKotlin
23

3-
final class PowerSyncBackendConnectorAdapter: KotlinPowerSyncBackendConnector,
4-
// We need to declare this since we declared KotlinPowerSyncBackendConnector as @unchecked Sendable
5-
@unchecked Sendable
6-
{
4+
final class SwiftBackendConnectorBridge: KotlinSwiftBackendConnector, Sendable {
75
let swiftBackendConnector: PowerSyncBackendConnectorProtocol
86
let db: any PowerSyncDatabaseProtocol
97
let logTag = "PowerSyncBackendConnector"
@@ -15,31 +13,25 @@ final class PowerSyncBackendConnectorAdapter: KotlinPowerSyncBackendConnector,
1513
self.swiftBackendConnector = swiftBackendConnector
1614
self.db = db
1715
}
18-
19-
override func __fetchCredentials() async throws -> KotlinPowerSyncCredentials? {
16+
17+
func __fetchCredentials() async throws -> PowerSyncResult {
2018
do {
2119
let result = try await swiftBackendConnector.fetchCredentials()
22-
return result?.kotlinCredentials
20+
return PowerSyncResult.Success(value: result?.kotlinCredentials)
2321
} catch {
2422
db.logger.error("Error while fetching credentials", tag: logTag)
25-
/// We can't use throwKotlinPowerSyncError here since the Kotlin connector
26-
/// runs this in a Job - this seems to break the SKIEE error propagation.
27-
/// returning nil here should still cause a retry
28-
return nil
23+
return PowerSyncResult.Failure(exception: error.toPowerSyncError())
2924
}
3025
}
31-
32-
override func __uploadData(database _: KotlinPowerSyncDatabase) async throws {
26+
27+
func __uploadData() async throws -> PowerSyncResult {
3328
do {
3429
// Pass the Swift DB protocal to the connector
35-
return try await swiftBackendConnector.uploadData(database: db)
30+
try await swiftBackendConnector.uploadData(database: self.db)
31+
return PowerSyncResult.Success(value: nil)
3632
} catch {
3733
db.logger.error("Error while uploading data: \(error)", tag: logTag)
38-
// Relay the error to the Kotlin SDK
39-
try throwKotlinPowerSyncError(
40-
message: "Connector errored while uploading data: \(error.localizedDescription)",
41-
cause: error.localizedDescription
42-
)
34+
return PowerSyncResult.Failure(exception: error.toPowerSyncError())
4335
}
4436
}
4537
}

Sources/PowerSync/Protocol/PowerSyncDatabaseProtocol.swift

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -188,19 +188,24 @@ public protocol PowerSyncDatabaseProtocol: Queries, Sendable {
188188
/// data by transaction. One batch may contain data from multiple transactions,
189189
/// and a single transaction may be split over multiple batches.
190190
func getCrudBatch(limit: Int32) async throws -> CrudBatch?
191-
192-
/// Get the next recorded transaction to upload.
191+
192+
/// Obtains an async iterator of completed transactions with local writes against the database.
193193
///
194-
/// Returns nil if there is no data to upload.
194+
/// This is typically used from the ``PowerSyncBackendConnectorProtocol/uploadData(database:)`` callback.
195+
/// Each entry emitted by teh returned flow is a full transaction containing all local writes made while that transaction was
196+
/// active.
195197
///
196-
/// Use this from the `PowerSyncBackendConnector.uploadData` callback.
198+
/// Unlike ``getNextCrudTransaction()``, which always returns the oldest transaction that hasn't been
199+
/// ``CrudTransaction/complete()``d yet, this iterator can be used to upload multiple transactions.
200+
/// Calling ``CrudTransaction/complete()`` will mark that and all prior transactions returned by this iterator as
201+
/// completed.
197202
///
198-
/// Once the data have been successfully uploaded, call `CrudTransaction.complete` before
199-
/// requesting the next transaction.
203+
/// This can be used to upload multiple transactions in a single batch, e.g. with
200204
///
201-
/// Unlike `getCrudBatch`, this only returns data from a single transaction at a time.
202-
/// All data for the transaction is loaded into memory.
203-
func getNextCrudTransaction() async throws -> CrudTransaction?
205+
/// ```Swift
206+
///
207+
/// ```
208+
func getCrudTransactions() -> CrudTransactions
204209

205210
/// Convenience method to get the current version of PowerSync.
206211
func getPowerSyncVersion() async throws -> String
@@ -226,6 +231,25 @@ public protocol PowerSyncDatabaseProtocol: Queries, Sendable {
226231
}
227232

228233
public extension PowerSyncDatabaseProtocol {
234+
/// Get the next recorded transaction to upload.
235+
///
236+
/// Returns nil if there is no data to upload.
237+
///
238+
/// Use this from the `PowerSyncBackendConnector.uploadData` callback.
239+
///
240+
/// Once the data have been successfully uploaded, call `CrudTransaction.complete` before
241+
/// requesting the next transaction.
242+
///
243+
/// Unlike `getCrudBatch`, this only returns data from a single transaction at a time.
244+
/// All data for the transaction is loaded into memory.
245+
func getNextCrudTransaction() async throws -> CrudTransaction? {
246+
for try await transaction in self.getCrudTransactions() {
247+
return transaction
248+
}
249+
250+
return nil
251+
}
252+
229253
///
230254
/// The connection is automatically re-opened if it fails for any reason.
231255
///

Sources/PowerSync/Protocol/db/CrudTransaction.swift

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Foundation
2+
import PowerSyncKotlin
23

34
/// A transaction of client-side changes.
45
public protocol CrudTransaction: Sendable {
@@ -24,3 +25,44 @@ public extension CrudTransaction {
2425
)
2526
}
2627
}
28+
29+
/// A sequence of crud transactions in a PowerSync database.
30+
///
31+
/// For details, see ``PowerSyncDatabaseProtocol/getCrudTransactions()``.
32+
public struct CrudTransactions: AsyncSequence {
33+
private let db: KotlinPowerSyncDatabase
34+
35+
init(db: KotlinPowerSyncDatabase) {
36+
self.db = db
37+
}
38+
39+
public func makeAsyncIterator() -> CrudTransactionIterator {
40+
let kotlinIterator = errorHandledCrudTransactions(db: self.db).makeAsyncIterator()
41+
return CrudTransactionIterator(inner: kotlinIterator)
42+
}
43+
44+
public struct CrudTransactionIterator: AsyncIteratorProtocol {
45+
public typealias Element = any CrudTransaction
46+
47+
private var inner: PowerSyncKotlin.SkieSwiftFlowIterator<PowerSyncKotlin.PowerSyncResult>
48+
49+
internal init(inner: PowerSyncKotlin.SkieSwiftFlowIterator<PowerSyncKotlin.PowerSyncResult>) {
50+
self.inner = inner
51+
}
52+
53+
public mutating func next() async throws -> (any CrudTransaction)? {
54+
if let innerTx = await self.inner.next() {
55+
if let success = innerTx as? PowerSyncResult.Success {
56+
let tx = success.value as! PowerSyncKotlin.CrudTransaction
57+
return try KotlinCrudTransaction(transaction: tx)
58+
} else if let failure = innerTx as? PowerSyncResult.Failure {
59+
try throwPowerSyncException(exception: failure.exception)
60+
}
61+
62+
fatalError("unreachable")
63+
} else {
64+
return nil
65+
}
66+
}
67+
}
68+
}

Tests/PowerSyncTests/CrudTests.swift

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,4 +199,43 @@ final class CrudTests: XCTestCase {
199199
let finalValidationBatch = try await database.getCrudBatch(limit: 100)
200200
XCTAssertNil(finalValidationBatch)
201201
}
202+
203+
func testCrudTransactions() async throws {
204+
func insertInTransaction(size: Int) async throws {
205+
try await database.writeTransaction { tx in
206+
for _ in 0 ..< size {
207+
try tx.execute(
208+
sql: "INSERT INTO users (id, name, email) VALUES (uuid(), null, null)",
209+
parameters: []
210+
)
211+
}
212+
}
213+
}
214+
215+
// Before inserting any data, the iterator should be empty.
216+
for try await _ in database.getCrudTransactions() {
217+
XCTFail("Unexpected transaction")
218+
}
219+
220+
try await insertInTransaction(size: 5)
221+
try await insertInTransaction(size: 10)
222+
try await insertInTransaction(size: 15)
223+
224+
var batch = [CrudEntry]()
225+
var lastTx: CrudTransaction? = nil
226+
for try await tx in database.getCrudTransactions() {
227+
batch.append(contentsOf: tx.crud)
228+
lastTx = tx
229+
230+
if (batch.count >= 10) {
231+
break
232+
}
233+
}
234+
235+
XCTAssertEqual(batch.count, 15)
236+
try await lastTx!.complete()
237+
238+
let finalTx = try await database.getNextCrudTransaction()
239+
XCTAssertEqual(finalTx!.crud.count, 15)
240+
}
202241
}

Tests/PowerSyncTests/Kotlin/KotlinPowerSyncDatabaseImplTests.swift

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,7 @@ final class KotlinPowerSyncDatabaseImplTests: XCTestCase {
4242
XCTFail("Expected an error to be thrown")
4343
} catch {
4444
XCTAssertEqual(error.localizedDescription, """
45-
error while compiling: INSERT INTO usersfail (id, name, email) VALUES (?, ?, ?)
46-
no such table: usersfail
45+
SqliteException(1): SQL logic error, no such table: usersfail for SQL: INSERT INTO usersfail (id, name, email) VALUES (?, ?, ?)
4746
""")
4847
}
4948
}
@@ -85,8 +84,7 @@ final class KotlinPowerSyncDatabaseImplTests: XCTestCase {
8584
XCTFail("Expected an error to be thrown")
8685
} catch {
8786
XCTAssertEqual(error.localizedDescription, """
88-
error while compiling: SELECT id, name, email FROM usersfail WHERE id = ?
89-
no such table: usersfail
87+
SqliteException(1): SQL logic error, no such table: usersfail for SQL: SELECT id, name, email FROM usersfail WHERE id = ?
9088
""")
9189
}
9290
}
@@ -131,8 +129,7 @@ final class KotlinPowerSyncDatabaseImplTests: XCTestCase {
131129
XCTFail("Expected an error to be thrown")
132130
} catch {
133131
XCTAssertEqual(error.localizedDescription, """
134-
error while compiling: SELECT id, name, email FROM usersfail WHERE id = ?
135-
no such table: usersfail
132+
SqliteException(1): SQL logic error, no such table: usersfail for SQL: SELECT id, name, email FROM usersfail WHERE id = ?
136133
""")
137134
}
138135
}
@@ -197,8 +194,7 @@ final class KotlinPowerSyncDatabaseImplTests: XCTestCase {
197194
XCTFail("Expected an error to be thrown")
198195
} catch {
199196
XCTAssertEqual(error.localizedDescription, """
200-
error while compiling: SELECT id, name, email FROM usersfail WHERE id = ?
201-
no such table: usersfail
197+
SqliteException(1): SQL logic error, no such table: usersfail for SQL: SELECT id, name, email FROM usersfail WHERE id = ?
202198
""")
203199
}
204200
}
@@ -281,8 +277,7 @@ final class KotlinPowerSyncDatabaseImplTests: XCTestCase {
281277
XCTFail("Expected an error to be thrown")
282278
} catch {
283279
XCTAssertEqual(error.localizedDescription, """
284-
error while compiling: EXPLAIN SELECT name FROM usersfail ORDER BY id
285-
no such table: usersfail
280+
SqliteException(1): SQL logic error, no such table: usersfail for SQL: EXPLAIN SELECT name FROM usersfail ORDER BY id
286281
""")
287282
}
288283
}
@@ -371,8 +366,7 @@ final class KotlinPowerSyncDatabaseImplTests: XCTestCase {
371366
}
372367
} catch {
373368
XCTAssertEqual(error.localizedDescription, """
374-
error while compiling: INSERT INTO usersfail (id, name, email) VALUES (?, ?, ?)
375-
no such table: usersfail
369+
SqliteException(1): SQL logic error, no such table: usersfail for SQL: INSERT INTO usersfail (id, name, email) VALUES (?, ?, ?)
376370
""")
377371
}
378372
}
@@ -392,8 +386,7 @@ final class KotlinPowerSyncDatabaseImplTests: XCTestCase {
392386
}
393387
} catch {
394388
XCTAssertEqual(error.localizedDescription, """
395-
error while compiling: INSERT INTO usersfail (id, name, email) VALUES (?, ?, ?)
396-
no such table: usersfail
389+
SqliteException(1): SQL logic error, no such table: usersfail for SQL: INSERT INTO usersfail (id, name, email) VALUES (?, ?, ?)
397390
""")
398391
}
399392

@@ -436,8 +429,7 @@ final class KotlinPowerSyncDatabaseImplTests: XCTestCase {
436429
}
437430
} catch {
438431
XCTAssertEqual(error.localizedDescription, """
439-
error while compiling: SELECT COUNT(*) FROM usersfail
440-
no such table: usersfail
432+
SqliteException(1): SQL logic error, no such table: usersfail for SQL: SELECT COUNT(*) FROM usersfail
441433
""")
442434
}
443435
}

0 commit comments

Comments
 (0)