Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 1.6.0

* Update core extension to 0.4.6 ([changelog](https://github.com/powersync-ja/powersync-sqlite-core/releases/tag/v0.4.6))
* Add `getCrudTransactions()`, returning an async sequence of transactions.
* Compatibility with Swift 6.2 and XCode 26.

## 1.5.1

* Update core extension to 0.4.5 ([changelog](https://github.com/powersync-ja/powersync-sqlite-core/releases/tag/v0.4.5))
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Demo/PowerSyncExample/PowerSync/FtsSetup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -159,10 +159,11 @@ func configureFts(db: PowerSyncDatabaseProtocol, schema: Schema) async throws {
// --- Execute all generated SQL statements ---

if !allSqlStatements.isEmpty {
let resultingStatements: [String] = allSqlStatements
do {
print("[FTS] Executing \(allSqlStatements.count) SQL statements in a transaction...")
_ = try await db.writeTransaction { transaction in
for sql in allSqlStatements {
for sql in resultingStatements {
print("[FTS] Executing SQL:\n\(sql)")
_ = try transaction.execute(sql: sql, parameters: [])
}
Expand Down
4 changes: 2 additions & 2 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ if let kotlinSdkPath = localKotlinSdkOverride {
// Not using a local build, so download from releases
conditionalTargets.append(.binaryTarget(
name: "PowerSyncKotlin",
url: "https://github.com/powersync-ja/powersync-kotlin/releases/download/v1.5.1/PowersyncKotlinRelease.zip",
checksum: "3a2de1863d2844d49cebf4428d0ab49956ba384dcab9f3cc2ddbc7836013c434"
url: "https://github.com/powersync-ja/powersync-kotlin/releases/download/v1.6.0/PowersyncKotlinRelease.zip",
checksum: "4f70331c11e30625eecf4ebcebe7b562e2e0165774890d2a43480ebc3a9081cc"
))
}

Expand All @@ -45,7 +45,7 @@ if let corePath = localCoreExtension {
// Not using a local build, so download from releases
conditionalDependencies.append(.package(
url: "https://github.com/powersync-ja/powersync-sqlite-core-swift.git",
exact: "0.4.5"
exact: "0.4.6"
))
}

Expand Down
18 changes: 6 additions & 12 deletions Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,9 @@ final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol,
connector: PowerSyncBackendConnectorProtocol,
options: ConnectOptions?
) async throws {
let connectorAdapter = PowerSyncBackendConnectorAdapter(
swiftBackendConnector: connector,
db: self
)
let connectorAdapter = swiftBackendConnectorToPowerSyncConnector(connector: SwiftBackendConnectorBridge(
swiftBackendConnector: connector, db: self
))

let resolvedOptions = options ?? ConnectOptions()
try await kotlinDatabase.connect(
Expand All @@ -75,14 +74,9 @@ final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol,
batch: base
)
}

func getNextCrudTransaction() async throws -> CrudTransaction? {
guard let base = try await kotlinDatabase.getNextCrudTransaction() else {
return nil
}
return try KotlinCrudTransaction(
transaction: base
)

func getCrudTransactions() -> any CrudTransactions {
return KotlinCrudTransactions(db: kotlinDatabase)
}

func getPowerSyncVersion() async throws -> String {
Expand Down
1 change: 1 addition & 0 deletions Sources/PowerSync/Kotlin/KotlinTypes.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import PowerSyncKotlin

typealias KotlinSwiftBackendConnector = PowerSyncKotlin.SwiftBackendConnector
typealias KotlinPowerSyncBackendConnector = PowerSyncKotlin.PowerSyncBackendConnector
typealias KotlinPowerSyncCredentials = PowerSyncKotlin.PowerSyncCredentials
typealias KotlinPowerSyncDatabase = PowerSyncKotlin.PowerSyncDatabase
Expand Down
30 changes: 11 additions & 19 deletions Sources/PowerSync/Kotlin/PowerSyncBackendConnectorAdapter.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import OSLog
import PowerSyncKotlin

final class PowerSyncBackendConnectorAdapter: KotlinPowerSyncBackendConnector,
// We need to declare this since we declared KotlinPowerSyncBackendConnector as @unchecked Sendable
@unchecked Sendable
{
final class SwiftBackendConnectorBridge: KotlinSwiftBackendConnector, Sendable {
let swiftBackendConnector: PowerSyncBackendConnectorProtocol
let db: any PowerSyncDatabaseProtocol
let logTag = "PowerSyncBackendConnector"
Expand All @@ -15,31 +13,25 @@ final class PowerSyncBackendConnectorAdapter: KotlinPowerSyncBackendConnector,
self.swiftBackendConnector = swiftBackendConnector
self.db = db
}

override func __fetchCredentials() async throws -> KotlinPowerSyncCredentials? {
func __fetchCredentials() async throws -> PowerSyncResult {
do {
let result = try await swiftBackendConnector.fetchCredentials()
return result?.kotlinCredentials
return PowerSyncResult.Success(value: result?.kotlinCredentials)
} catch {
db.logger.error("Error while fetching credentials", tag: logTag)
/// We can't use throwKotlinPowerSyncError here since the Kotlin connector
/// runs this in a Job - this seems to break the SKIEE error propagation.
/// returning nil here should still cause a retry
return nil
return PowerSyncResult.Failure(exception: error.toPowerSyncError())
}
}

override func __uploadData(database _: KotlinPowerSyncDatabase) async throws {
func __uploadData() async throws -> PowerSyncResult {
do {
// Pass the Swift DB protocal to the connector
return try await swiftBackendConnector.uploadData(database: db)
try await swiftBackendConnector.uploadData(database: self.db)
return PowerSyncResult.Success(value: nil)
} catch {
db.logger.error("Error while uploading data: \(error)", tag: logTag)
// Relay the error to the Kotlin SDK
try throwKotlinPowerSyncError(
message: "Connector errored while uploading data: \(error.localizedDescription)",
cause: error.localizedDescription
)
return PowerSyncResult.Failure(exception: error.toPowerSyncError())
}
}
}
39 changes: 39 additions & 0 deletions Sources/PowerSync/Kotlin/db/KotlinCrudTransactions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import PowerSyncKotlin

struct KotlinCrudTransactions: CrudTransactions {
typealias Element = KotlinCrudTransaction

private let db: KotlinPowerSyncDatabase

init(db: KotlinPowerSyncDatabase) {
self.db = db
}

public func makeAsyncIterator() -> CrudTransactionIterator {
let kotlinIterator = errorHandledCrudTransactions(db: self.db).makeAsyncIterator()
return CrudTransactionIterator(inner: kotlinIterator)
}

struct CrudTransactionIterator: CrudTransactionsIterator {
private var inner: PowerSyncKotlin.SkieSwiftFlowIterator<PowerSyncKotlin.PowerSyncResult>

internal init(inner: PowerSyncKotlin.SkieSwiftFlowIterator<PowerSyncKotlin.PowerSyncResult>) {
self.inner = inner
}

public mutating func next() async throws -> KotlinCrudTransaction? {
if let innerTx = await self.inner.next() {
if let success = innerTx as? PowerSyncResult.Success {
let tx = success.value as! PowerSyncKotlin.CrudTransaction
return try KotlinCrudTransaction(transaction: tx)
} else if let failure = innerTx as? PowerSyncResult.Failure {
try throwPowerSyncException(exception: failure.exception)
}

fatalError("unreachable")
} else {
return nil
}
}
}
}
42 changes: 33 additions & 9 deletions Sources/PowerSync/Protocol/PowerSyncDatabaseProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -188,19 +188,24 @@ public protocol PowerSyncDatabaseProtocol: Queries, Sendable {
/// data by transaction. One batch may contain data from multiple transactions,
/// and a single transaction may be split over multiple batches.
func getCrudBatch(limit: Int32) async throws -> CrudBatch?

/// Get the next recorded transaction to upload.
/// Obtains an async iterator of completed transactions with local writes against the database.
///
/// Returns nil if there is no data to upload.
/// This is typically used from the ``PowerSyncBackendConnectorProtocol/uploadData(database:)`` callback.
/// Each entry emitted by teh returned flow is a full transaction containing all local writes made while that transaction was
/// active.
///
/// Use this from the `PowerSyncBackendConnector.uploadData` callback.
/// Unlike ``getNextCrudTransaction()``, which always returns the oldest transaction that hasn't been
/// ``CrudTransaction/complete()``d yet, this iterator can be used to upload multiple transactions.
/// Calling ``CrudTransaction/complete()`` will mark that and all prior transactions returned by this iterator as
/// completed.
///
/// Once the data have been successfully uploaded, call `CrudTransaction.complete` before
/// requesting the next transaction.
/// This can be used to upload multiple transactions in a single batch, e.g. with
///
/// Unlike `getCrudBatch`, this only returns data from a single transaction at a time.
/// All data for the transaction is loaded into memory.
func getNextCrudTransaction() async throws -> CrudTransaction?
/// ```Swift
///
/// ```
func getCrudTransactions() -> any CrudTransactions

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

public extension PowerSyncDatabaseProtocol {
/// Get the next recorded transaction to upload.
///
/// Returns nil if there is no data to upload.
///
/// Use this from the `PowerSyncBackendConnector.uploadData` callback.
///
/// Once the data have been successfully uploaded, call `CrudTransaction.complete` before
/// requesting the next transaction.
///
/// Unlike `getCrudBatch`, this only returns data from a single transaction at a time.
/// All data for the transaction is loaded into memory.
func getNextCrudTransaction() async throws -> CrudTransaction? {
for try await transaction in self.getCrudTransactions() {
return transaction
}

return nil
}

///
/// The connection is automatically re-opened if it fails for any reason.
///
Expand Down
8 changes: 8 additions & 0 deletions Sources/PowerSync/Protocol/db/CrudTransaction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,11 @@ public extension CrudTransaction {
)
}
}

/// A sequence of crud transactions in a PowerSync database.
///
/// For details, see ``PowerSyncDatabaseProtocol/getCrudTransactions()``.
public protocol CrudTransactions: AsyncSequence where Element: CrudTransaction, AsyncIterator: CrudTransactionsIterator {}

/// The iterator returned by ``CrudTransactions``.
public protocol CrudTransactionsIterator: AsyncIteratorProtocol where Element: CrudTransaction {}
39 changes: 39 additions & 0 deletions Tests/PowerSyncTests/CrudTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -199,4 +199,43 @@ final class CrudTests: XCTestCase {
let finalValidationBatch = try await database.getCrudBatch(limit: 100)
XCTAssertNil(finalValidationBatch)
}

func testCrudTransactions() async throws {
func insertInTransaction(size: Int) async throws {
try await database.writeTransaction { tx in
for _ in 0 ..< size {
try tx.execute(
sql: "INSERT INTO users (id, name, email) VALUES (uuid(), null, null)",
parameters: []
)
}
}
}

// Before inserting any data, the iterator should be empty.
for try await _ in database.getCrudTransactions() {
XCTFail("Unexpected transaction")
}

try await insertInTransaction(size: 5)
try await insertInTransaction(size: 10)
try await insertInTransaction(size: 15)

var batch = [CrudEntry]()
var lastTx: CrudTransaction? = nil
for try await tx in database.getCrudTransactions() {
batch.append(contentsOf: tx.crud)
lastTx = tx

if (batch.count >= 10) {
break
}
}

XCTAssertEqual(batch.count, 15)
try await lastTx!.complete()

let finalTx = try await database.getNextCrudTransaction()
XCTAssertEqual(finalTx!.crud.count, 15)
}
}
Loading