diff --git a/CHANGELOG.md b/CHANGELOG.md index 20ec1d0..c62bdeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,9 @@ ## Unreleased +* Fix null values in CRUD entries being reported as strings. * [Internal] Instantiate Kotlin Kermit logger directly. +* [Internal] Improved connection context error handling. ## 1.4.0 diff --git a/Package.swift b/Package.swift index fdae3ac..d8f589b 100644 --- a/Package.swift +++ b/Package.swift @@ -2,6 +2,7 @@ // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription + let packageName = "PowerSync" // Set this to the absolute path of your Kotlin SDK checkout if you want to use a local Kotlin @@ -31,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.4.0/PowersyncKotlinRelease.zip", - checksum: "e800db216fc1c9722e66873deb4f925530267db6dbd5e2114dd845cc62c28cd9" + url: "https://github.com/powersync-ja/powersync-kotlin/releases/download/v1.5.0/PowersyncKotlinRelease.zip", + checksum: "cb1d717d28411aff0bfdeeaa837ae01514ebf5d64203dc565a9520a2912bae9d" )) } @@ -59,7 +60,8 @@ let package = Package( // Products define the executables and libraries a package produces, making them visible to other packages. .library( name: packageName, - targets: ["PowerSync"]), + targets: ["PowerSync"] + ) ], dependencies: conditionalDependencies, targets: [ @@ -70,10 +72,11 @@ let package = Package( dependencies: [ kotlinTargetDependency, .product(name: "PowerSyncSQLiteCore", package: corePackageName) - ]), + ] + ), .testTarget( name: "PowerSyncTests", dependencies: ["PowerSync"] - ), + ) ] + conditionalTargets ) diff --git a/Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift b/Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift index d78b370..931df1b 100644 --- a/Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift +++ b/Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift @@ -275,9 +275,7 @@ final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol { return try await wrapPowerSyncException { try safeCast( await kotlinDatabase.writeLock( - callback: LockCallback( - callback: callback - ) + callback: wrapLockContext(callback: callback) ), to: R.self ) @@ -290,9 +288,7 @@ final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol { return try await wrapPowerSyncException { try safeCast( await kotlinDatabase.writeTransaction( - callback: TransactionCallback( - callback: callback - ) + callback: wrapTransactionContext(callback: callback) ), to: R.self ) @@ -307,9 +303,7 @@ final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol { return try await wrapPowerSyncException { try safeCast( await kotlinDatabase.readLock( - callback: LockCallback( - callback: callback - ) + callback: wrapLockContext(callback: callback) ), to: R.self ) @@ -322,9 +316,7 @@ final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol { return try await wrapPowerSyncException { try safeCast( await kotlinDatabase.readTransaction( - callback: TransactionCallback( - callback: callback - ) + callback: wrapTransactionContext(callback: callback) ), to: R.self ) @@ -372,11 +364,11 @@ final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol { } ) - let rootPages = rows.compactMap { r in - if (r.opcode == "OpenRead" || r.opcode == "OpenWrite") && - r.p3 == 0 && r.p2 != 0 + let rootPages = rows.compactMap { row in + if (row.opcode == "OpenRead" || row.opcode == "OpenWrite") && + row.p3 == 0 && row.p2 != 0 { - return r.p2 + return row.p2 } return nil } @@ -389,7 +381,7 @@ final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol { message: "Failed to convert pages data to UTF-8 string" ) } - + let tableRows = try await getAll( sql: "SELECT tbl_name FROM sqlite_master WHERE rootpage IN (SELECT json_each.value FROM json_each(?))", parameters: [ @@ -414,3 +406,50 @@ private struct ExplainQueryResult { let p2: Int64 let p3: Int64 } + +extension Error { + func toPowerSyncError() -> PowerSyncKotlin.PowerSyncException { + return PowerSyncKotlin.PowerSyncException( + message: localizedDescription, + cause: PowerSyncKotlin.KotlinThrowable(message: localizedDescription) + ) + } +} + +func wrapLockContext( + callback: @escaping (any ConnectionContext) throws -> Any +) throws -> PowerSyncKotlin.ThrowableLockCallback { + PowerSyncKotlin.wrapContextHandler { kotlinContext in + do { + return try PowerSyncKotlin.PowerSyncResult.Success( + value: callback( + KotlinConnectionContext( + ctx: kotlinContext + ) + )) + } catch { + return PowerSyncKotlin.PowerSyncResult.Failure( + exception: error.toPowerSyncError() + ) + } + } +} + +func wrapTransactionContext( + callback: @escaping (any Transaction) throws -> Any +) throws -> PowerSyncKotlin.ThrowableTransactionCallback { + PowerSyncKotlin.wrapTransactionContextHandler { kotlinContext in + do { + return try PowerSyncKotlin.PowerSyncResult.Success( + value: callback( + KotlinTransactionContext( + ctx: kotlinContext + ) + )) + } catch { + return PowerSyncKotlin.PowerSyncResult.Failure( + exception: error.toPowerSyncError() + ) + } + } +} diff --git a/Sources/PowerSync/Kotlin/TransactionCallback.swift b/Sources/PowerSync/Kotlin/TransactionCallback.swift deleted file mode 100644 index 78f460d..0000000 --- a/Sources/PowerSync/Kotlin/TransactionCallback.swift +++ /dev/null @@ -1,67 +0,0 @@ -import PowerSyncKotlin - -/// Internal Wrapper for Kotlin lock context lambdas -class LockCallback: PowerSyncKotlin.ThrowableLockCallback { - let callback: (ConnectionContext) throws -> R - - init(callback: @escaping (ConnectionContext) throws -> R) { - self.callback = callback - } - - // The Kotlin SDK does not gracefully handle exceptions thrown from Swift callbacks. - // If a Swift callback throws an exception, it results in a `BAD ACCESS` crash. - // - // To prevent this, we catch the exception and return it as a `PowerSyncException`, - // allowing Kotlin to propagate the error correctly. - // - // This approach is a workaround. Ideally, we should introduce an internal mechanism - // in the Kotlin SDK to handle errors from Swift more robustly. - // - // Currently, we wrap the public `PowerSyncDatabase` class in Kotlin, which limits our - // ability to handle exceptions cleanly. Instead, we should expose an internal implementation - // from a "core" package in Kotlin that provides better control over exception handling - // and other functionality—without modifying the public `PowerSyncDatabase` API to include - // Swift-specific logic. - func execute(context: PowerSyncKotlin.ConnectionContext) throws -> Any { - do { - return try callback( - KotlinConnectionContext( - ctx: context - ) - ) - } catch { - return PowerSyncKotlin.PowerSyncException( - message: error.localizedDescription, - cause: PowerSyncKotlin.KotlinThrowable( - message: error.localizedDescription - ) - ) - } - } -} - -/// Internal Wrapper for Kotlin transaction context lambdas -class TransactionCallback: PowerSyncKotlin.ThrowableTransactionCallback { - let callback: (Transaction) throws -> R - - init(callback: @escaping (Transaction) throws -> R) { - self.callback = callback - } - - func execute(transaction: PowerSyncKotlin.PowerSyncTransaction) throws -> Any { - do { - return try callback( - KotlinTransactionContext( - ctx: transaction - ) - ) - } catch { - return PowerSyncKotlin.PowerSyncException( - message: error.localizedDescription, - cause: PowerSyncKotlin.KotlinThrowable( - message: error.localizedDescription - ) - ) - } - } -}