From 90857432213e9503bdef979e8b45b63001c53d8b Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Thu, 2 Oct 2025 11:50:45 +0200 Subject: [PATCH 01/15] Update Kotlin SDK to 1.7.0 --- CHANGELOG.md | 4 ++++ Package.swift | 4 ++-- Sources/PowerSync/Kotlin/sync/KotlinSyncStatus.swift | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3923238..40a8370 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 1.6.1 (unreleased) + +* Update Kotlin SDK to 1.7.0. + ## 1.6.0 * Update core extension to 0.4.6 ([changelog](https://github.com/powersync-ja/powersync-sqlite-core/releases/tag/v0.4.6)) diff --git a/Package.swift b/Package.swift index 797d586..cece14d 100644 --- a/Package.swift +++ b/Package.swift @@ -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.6.0/PowersyncKotlinRelease.zip", - checksum: "4f70331c11e30625eecf4ebcebe7b562e2e0165774890d2a43480ebc3a9081cc" + url: "https://github.com/powersync-ja/powersync-kotlin/releases/download/v1.7.0/PowersyncKotlinRelease.zip", + checksum: "836ac106c26a184c10373c862745d9af195737ad01505bb965f197797aa88535" )) } diff --git a/Sources/PowerSync/Kotlin/sync/KotlinSyncStatus.swift b/Sources/PowerSync/Kotlin/sync/KotlinSyncStatus.swift index db8ce2d..95a187f 100644 --- a/Sources/PowerSync/Kotlin/sync/KotlinSyncStatus.swift +++ b/Sources/PowerSync/Kotlin/sync/KotlinSyncStatus.swift @@ -5,7 +5,7 @@ import PowerSyncKotlin final class KotlinSyncStatus: KotlinSyncStatusDataProtocol, SyncStatus { private let baseStatus: PowerSyncKotlin.SyncStatus - var base: any PowerSyncKotlin.SyncStatusData { + var base: PowerSyncKotlin.SyncStatusData { baseStatus } From 9f457dc3a6feb84a539b21561409471a435e5830 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Fri, 17 Oct 2025 17:31:45 +0200 Subject: [PATCH 02/15] Test encryption --- Package.resolved | 9 +++ Package.swift | 17 ++++- .../Kotlin/KotlinPowerSyncDatabaseImpl.swift | 5 +- Tests/PowerSyncTests/EncryptionTests.swift | 67 +++++++++++++++++++ 4 files changed, 93 insertions(+), 5 deletions(-) create mode 100644 Tests/PowerSyncTests/EncryptionTests.swift diff --git a/Package.resolved b/Package.resolved index a62a2d5..75347d5 100644 --- a/Package.resolved +++ b/Package.resolved @@ -8,6 +8,15 @@ "revision" : "b2a81af14e9ad83393eb187bb02e62e6db8b5ad6", "version" : "0.4.6" } + }, + { + "identity" : "sqlcipher.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/sqlcipher/SQLCipher.swift.git", + "state" : { + "revision" : "247b042574b4838c7eefdb95d6a250a843dcf1ad", + "version" : "4.11.0" + } } ], "version" : 2 diff --git a/Package.swift b/Package.swift index cece14d..6dafc3a 100644 --- a/Package.swift +++ b/Package.swift @@ -7,12 +7,14 @@ let packageName = "PowerSync" // Set this to the absolute path of your Kotlin SDK checkout if you want to use a local Kotlin // build. Also see docs/LocalBuild.md for details -let localKotlinSdkOverride: String? = nil +let localKotlinSdkOverride: String? = "/Users/simon/src/powersync-kotlin" // Set this to the absolute path of your powersync-sqlite-core checkout if you want to use a // local build of the core extension. let localCoreExtension: String? = nil +let encryption = true + // Our target and dependency setup is different when a local Kotlin SDK is used. Without the local // SDK, we have no package dependency on Kotlin and download the XCFramework from Kotlin releases as // a binary target. @@ -22,10 +24,16 @@ var conditionalDependencies: [Package.Dependency] = [] var conditionalTargets: [Target] = [] var kotlinTargetDependency = Target.Dependency.target(name: "PowerSyncKotlin") +if encryption { + conditionalDependencies.append(.package(url: "https://github.com/sqlcipher/SQLCipher.swift.git", from: "4.10.0")) +} else { + conditionalDependencies.append(.package(url: "https://github.com/sbooth/CSQLite.git", from: "3.50.4")) +} + if let kotlinSdkPath = localKotlinSdkOverride { // We can't depend on local XCFrameworks outside of this project's root, so there's a Package.swift // in the PowerSyncKotlin project pointing towards a local build. - conditionalDependencies.append(.package(path: "\(kotlinSdkPath)/PowerSyncKotlin")) + conditionalDependencies.append(.package(path: "\(kotlinSdkPath)/internal/PowerSyncKotlin")) kotlinTargetDependency = .product(name: "PowerSyncKotlin", package: "PowerSyncKotlin") } else { @@ -81,7 +89,10 @@ let package = Package( name: packageName, dependencies: [ kotlinTargetDependency, - .product(name: "PowerSyncSQLiteCore", package: corePackageName) + .product(name: "PowerSyncSQLiteCore", package: corePackageName), + encryption ? + .product(name: "SQLCipher", package: "SQLCipher.swift") : + .product(name: "CSQLite", package: "CSQLite"), ] ), .testTarget( diff --git a/Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift b/Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift index 10bbd97..066733f 100644 --- a/Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift +++ b/Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift @@ -13,9 +13,10 @@ final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol, init( schema: Schema, dbFilename: String, - logger: DatabaseLogger + logger: DatabaseLogger, + initialStatements: [String] = [], ) { - let factory = PowerSyncKotlin.DatabaseDriverFactory() + let factory = sqlite3DatabaseFactory(initialStatements: initialStatements) kotlinDatabase = PowerSyncDatabase( factory: factory, schema: KotlinAdapter.Schema.toKotlin(schema), diff --git a/Tests/PowerSyncTests/EncryptionTests.swift b/Tests/PowerSyncTests/EncryptionTests.swift new file mode 100644 index 0000000..74c78e8 --- /dev/null +++ b/Tests/PowerSyncTests/EncryptionTests.swift @@ -0,0 +1,67 @@ +@testable import PowerSync +import XCTest + + +final class EncryptionTests: XCTestCase { + + func testLinksSqlcipher() async throws { + let database = KotlinPowerSyncDatabaseImpl( + schema: Schema(), + dbFilename: ":memory:", + logger: DatabaseLogger(DefaultLogger()) + ) + + let version = try await database.get("pragma cipher_version", mapper: {cursor in + try cursor.getString(index: 0) + }); + + XCTAssertEqual(version, "4.11.0 community") + try await database.close() + } + + func testEncryption() async throws { + let database = KotlinPowerSyncDatabaseImpl( + schema: Schema(tables: [ + Table( + name: "users", + columns: [ + .text("name") + ] + ), + ]), + dbFilename: "encrypted.db", + logger: DatabaseLogger(DefaultLogger()), + initialStatements: [ + "pragma key = 'foobar'" + ], + ) + + try await database.execute("INSERT INTO users (id, name) VALUES (uuid(), 'test')") + try await database.close() + + let another = KotlinPowerSyncDatabaseImpl( + schema: Schema(tables: [ + Table( + name: "users", + columns: [ + .text("name") + ] + ), + ]), + dbFilename: "encrypted.db", + logger: DatabaseLogger(DefaultLogger()), + initialStatements: [ + "pragma key = 'wrong password'" + ], + ) + + var hadError = false + do { + try await database.execute("DELETE FROM users") + } catch let error { + hadError = true + } + + XCTAssertTrue(hadError) + } +} From 82880a4b29d5f2f6464aa3d960ab617615bc41cf Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Thu, 23 Oct 2025 17:27:08 +0200 Subject: [PATCH 03/15] Prepare for Kotlin common module --- Package.resolved | 9 +++++++++ Package.swift | 15 +++++++++++---- .../Kotlin/KotlinPowerSyncDatabaseImpl.swift | 2 +- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/Package.resolved b/Package.resolved index a62a2d5..8bba077 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,14 @@ { "pins" : [ + { + "identity" : "csqlite", + "kind" : "remoteSourceControl", + "location" : "https://github.com/sbooth/CSQLite.git", + "state" : { + "revision" : "b1161e6c73fa68c25292f6bb697293d6c679f919", + "version" : "3.50.4" + } + }, { "identity" : "powersync-sqlite-core-swift", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index cece14d..438c8d9 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.7 +// swift-tools-version: 6.1 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -18,14 +18,20 @@ let localCoreExtension: String? = nil // a binary target. // With a local SDK, we point to a `Package.swift` within the Kotlin SDK containing a target pointing // towards a local framework build -var conditionalDependencies: [Package.Dependency] = [] +var conditionalDependencies: [Package.Dependency] = [ + .package( + url: "https://github.com/sbooth/CSQLite.git", + from: "3.50.4", + traits: ["ENABLE_SESSION"] + ) +] var conditionalTargets: [Target] = [] var kotlinTargetDependency = Target.Dependency.target(name: "PowerSyncKotlin") if let kotlinSdkPath = localKotlinSdkOverride { // We can't depend on local XCFrameworks outside of this project's root, so there's a Package.swift // in the PowerSyncKotlin project pointing towards a local build. - conditionalDependencies.append(.package(path: "\(kotlinSdkPath)/PowerSyncKotlin")) + conditionalDependencies.append(.package(path: "\(kotlinSdkPath)/internal/PowerSyncKotlin")) kotlinTargetDependency = .product(name: "PowerSyncKotlin", package: "PowerSyncKotlin") } else { @@ -81,7 +87,8 @@ let package = Package( name: packageName, dependencies: [ kotlinTargetDependency, - .product(name: "PowerSyncSQLiteCore", package: corePackageName) + .product(name: "PowerSyncSQLiteCore", package: corePackageName), + .product(name: "CSQLite", package: "CSQLite") ] ), .testTarget( diff --git a/Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift b/Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift index 10bbd97..efd6590 100644 --- a/Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift +++ b/Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift @@ -15,7 +15,7 @@ final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol, dbFilename: String, logger: DatabaseLogger ) { - let factory = PowerSyncKotlin.DatabaseDriverFactory() + let factory = sqlite3DatabaseFactory(initialStatements: []) kotlinDatabase = PowerSyncDatabase( factory: factory, schema: KotlinAdapter.Schema.toKotlin(schema), From ebacd302b30c71b38eb1d4436930147ae589e501 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 27 Oct 2025 11:58:02 +0100 Subject: [PATCH 04/15] Update PowerSync Kotlin and core extension --- Package.resolved | 7 +++--- Package.swift | 6 ++--- Sources/PowerSync/Kotlin/KotlinAdapter.swift | 3 ++- .../Kotlin/KotlinPowerSyncDatabaseImpl.swift | 3 ++- .../Protocol/PowerSyncDatabaseProtocol.swift | 22 +++++++++++++------ .../PowerSync/Protocol/Schema/RawTable.swift | 6 ++++- Tests/PowerSyncTests/CrudTests.swift | 15 +++++++++++++ 7 files changed, 46 insertions(+), 16 deletions(-) diff --git a/Package.resolved b/Package.resolved index 8bba077..d0dc9d8 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,4 +1,5 @@ { + "originHash" : "82539474c4bcedf04818c668b504533c49309a0dd3669f4ec2fb8e77b64439b3", "pins" : [ { "identity" : "csqlite", @@ -14,10 +15,10 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/powersync-ja/powersync-sqlite-core-swift.git", "state" : { - "revision" : "b2a81af14e9ad83393eb187bb02e62e6db8b5ad6", - "version" : "0.4.6" + "revision" : "9801f4aa0923c7f33fa479a01e643d00e7764f0b", + "version" : "0.4.8" } } ], - "version" : 2 + "version" : 3 } diff --git a/Package.swift b/Package.swift index 438c8d9..ce28be9 100644 --- a/Package.swift +++ b/Package.swift @@ -38,8 +38,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.7.0/PowersyncKotlinRelease.zip", - checksum: "836ac106c26a184c10373c862745d9af195737ad01505bb965f197797aa88535" + url: "https://github.com/powersync-ja/powersync-kotlin/releases/download/v1.8.0/PowersyncKotlinRelease.zip", + checksum: "31ac7c5e11d747e11bceb0b34f30438d37033e700c621b0a468aa308d887587f" )) } @@ -51,7 +51,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.6" + exact: "0.4.8" )) } diff --git a/Sources/PowerSync/Kotlin/KotlinAdapter.swift b/Sources/PowerSync/Kotlin/KotlinAdapter.swift index 8e85220..2d73ab3 100644 --- a/Sources/PowerSync/Kotlin/KotlinAdapter.swift +++ b/Sources/PowerSync/Kotlin/KotlinAdapter.swift @@ -49,7 +49,8 @@ enum KotlinAdapter { return PowerSyncKotlin.RawTable( name: table.name, put: translateStatement(table.put), - delete: translateStatement(table.delete) + delete: translateStatement(table.delete), + clear: table.clear, ); } diff --git a/Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift b/Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift index efd6590..ba91fd7 100644 --- a/Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift +++ b/Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift @@ -89,7 +89,8 @@ final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol, func disconnectAndClear(clearLocal: Bool = true) async throws { try await kotlinDatabase.disconnectAndClear( - clearLocal: clearLocal + clearLocal: clearLocal, + soft: false ) } diff --git a/Sources/PowerSync/Protocol/PowerSyncDatabaseProtocol.swift b/Sources/PowerSync/Protocol/PowerSyncDatabaseProtocol.swift index cd112a5..74fd57a 100644 --- a/Sources/PowerSync/Protocol/PowerSyncDatabaseProtocol.swift +++ b/Sources/PowerSync/Protocol/PowerSyncDatabaseProtocol.swift @@ -216,12 +216,20 @@ public protocol PowerSyncDatabaseProtocol: Queries, Sendable { func disconnect() async throws /// Disconnect and clear the database. - /// Use this when logging out. - /// The database can still be queried after this is called, but the tables - /// would be empty. /// - /// - Parameter clearLocal: Set to false to preserve data in local-only tables. Defaults to `true`. - func disconnectAndClear(clearLocal: Bool) async throws + /// Clearing the database is useful when a user logs out, to ensure another user logging in later would not see + /// previous data. + /// + /// The database can still be queried after this is called, but the tables would be empty. + /// + /// To perserve data in local-only tables, set `clearLocal` to `false`. + /// + /// A `soft` clear deletes publicly visible data, but keeps internal copies of data synced in the database. This + /// usually means that if the same user logs out and back in again, the first sync is very fast because all internal + /// data is still available. When a different user logs in, no old data would be visible at any point. + /// Using soft clears is recommended where it's not a security issue that old data could be reconstructed from + /// the database. + func disconnectAndClear(clearLocal: Bool, soft: Bool) async throws /// Close the database, releasing resources. /// Also disconnects any active connection. @@ -289,8 +297,8 @@ public extension PowerSyncDatabaseProtocol { ) } - func disconnectAndClear() async throws { - try await disconnectAndClear(clearLocal: true) + func disconnectAndClear(clearLocal: Bool = true, soft: Bool = false) async throws { + try await disconnectAndClear(clearLocal: clearLocal, soft: soft) } func getCrudBatch(limit: Int32 = 100) async throws -> CrudBatch? { diff --git a/Sources/PowerSync/Protocol/Schema/RawTable.swift b/Sources/PowerSync/Protocol/Schema/RawTable.swift index b209583..f8fbcd0 100644 --- a/Sources/PowerSync/Protocol/Schema/RawTable.swift +++ b/Sources/PowerSync/Protocol/Schema/RawTable.swift @@ -24,11 +24,15 @@ public struct RawTable: BaseTableProtocol { /// The statement to run when the sync client has to delete a row. public let delete: PendingStatement + + /// An optional statement to run when the database is cleared. + public let clear: String? - public init(name: String, put: PendingStatement, delete: PendingStatement) { + public init(name: String, put: PendingStatement, delete: PendingStatement, clear: String? = nil) { self.name = name self.put = put self.delete = delete + self.clear = clear } } diff --git a/Tests/PowerSyncTests/CrudTests.swift b/Tests/PowerSyncTests/CrudTests.swift index c315b38..d3a508f 100644 --- a/Tests/PowerSyncTests/CrudTests.swift +++ b/Tests/PowerSyncTests/CrudTests.swift @@ -238,4 +238,19 @@ final class CrudTests: XCTestCase { let finalTx = try await database.getNextCrudTransaction() XCTAssertEqual(finalTx!.crud.count, 15) } + + func testSoftClear() async throws { + try await database.execute(sql: "INSERT INTO users (id, name) VALUES (uuid(), ?)", parameters: ["test"]); + try await database.execute(sql: "INSERT INTO ps_buckets (name, last_applied_op) VALUES (?, ?)", parameters: ["bkt", 10]) + + // Doing a soft-clear should delete data but keep the bucket around. + try await database.disconnectAndClear(soft: true) + let entries = try await database.getAll("SELECT name FROM ps_buckets", mapper: { cursor in try cursor.getString(index: 0) }) + XCTAssertEqual(entries.count, 1) + + // Doing a default clear also deletes buckets. + try await database.disconnectAndClear(); + let newEntries = try await database.getAll("SELECT name FROM ps_buckets", mapper: { cursor in try cursor.getString(index: 0) }) + XCTAssertEqual(newEntries.count, 0) + } } From 858640fb93313c61d59f38d9fd7f6dd38c0cbbe4 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 27 Oct 2025 12:19:59 +0100 Subject: [PATCH 05/15] Fix tests --- .../Kotlin/KotlinPowerSyncDatabaseImpl.swift | 4 ++-- .../Protocol/PowerSyncDatabaseProtocol.swift | 12 ++++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift b/Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift index ba91fd7..142fc78 100644 --- a/Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift +++ b/Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift @@ -87,10 +87,10 @@ final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol, try await kotlinDatabase.disconnect() } - func disconnectAndClear(clearLocal: Bool = true) async throws { + func disconnectAndClear(clearLocal: Bool, soft: Bool) async throws { try await kotlinDatabase.disconnectAndClear( clearLocal: clearLocal, - soft: false + soft: soft ) } diff --git a/Sources/PowerSync/Protocol/PowerSyncDatabaseProtocol.swift b/Sources/PowerSync/Protocol/PowerSyncDatabaseProtocol.swift index 74fd57a..d0a7161 100644 --- a/Sources/PowerSync/Protocol/PowerSyncDatabaseProtocol.swift +++ b/Sources/PowerSync/Protocol/PowerSyncDatabaseProtocol.swift @@ -297,8 +297,16 @@ public extension PowerSyncDatabaseProtocol { ) } - func disconnectAndClear(clearLocal: Bool = true, soft: Bool = false) async throws { - try await disconnectAndClear(clearLocal: clearLocal, soft: soft) + func disconnectAndClear() async throws { + try await disconnectAndClear(clearLocal: true, soft: false) + } + + func disconnectAndClear(clearLocal: Bool) async throws { + try await disconnectAndClear(clearLocal: clearLocal, soft: false) + } + + func disconnectAndClear(soft: Bool) async throws { + try await disconnectAndClear(clearLocal: true, soft: soft) } func getCrudBatch(limit: Int32 = 100) async throws -> CrudBatch? { From 8300d80e36a356cdf49d1c37b14b57258f31cd6b Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 27 Oct 2025 12:24:25 +0100 Subject: [PATCH 06/15] Remove explicit Swift 6 test --- .github/workflows/build_and_test.yaml | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/.github/workflows/build_and_test.yaml b/.github/workflows/build_and_test.yaml index f13117d..9a8ff59 100644 --- a/.github/workflows/build_and_test.yaml +++ b/.github/workflows/build_and_test.yaml @@ -14,25 +14,13 @@ jobs: uses: maxim-lobanov/setup-xcode@v1 with: xcode-version: latest-stable - - name: Build and Test + + - name: Test on iOS simulator run: | xcodebuild test -scheme PowerSync-Package -destination "platform=iOS Simulator,name=iPhone 16" - xcodebuild test -scheme PowerSync-Package -destination "platform=macOS,arch=arm64,name=My Mac" - xcodebuild test -scheme PowerSync-Package -destination "platform=watchOS Simulator,arch=arm64,name=Apple Watch Ultra 2 (49mm)" - - buildSwift6: - name: Build and test with Swift 6 - runs-on: macos-latest - steps: - - uses: actions/checkout@v4 - - name: Set up XCode - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: latest-stable - - name: Use Swift 6 + - name: Test on macOS run: | - sed -i '' 's|^// swift-tools-version:.*$|// swift-tools-version:6.1|' Package.swift - - name: Build and Test + xcodebuild test -scheme PowerSync-Package -destination "platform=macOS,arch=arm64,name=My Mac" + - name: Test on watchOS simulator run: | - swift build -Xswiftc -strict-concurrency=complete - swift test -Xswiftc -strict-concurrency=complete + xcodebuild test -scheme PowerSync-Package -destination "platform=watchOS Simulator,arch=arm64,name=Apple Watch Ultra 2 (49mm)" From 3a4fb66f83f4070579371ea830de6cd08707d30f Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 27 Oct 2025 13:52:48 +0100 Subject: [PATCH 07/15] Try initializing SQLite --- Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift b/Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift index 142fc78..2e61cef 100644 --- a/Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift +++ b/Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift @@ -1,5 +1,6 @@ import Foundation import PowerSyncKotlin +import CSQLite final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol, // `PowerSyncKotlin.PowerSyncDatabase` cannot be marked as Sendable @@ -15,6 +16,11 @@ final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol, dbFilename: String, logger: DatabaseLogger ) { + let rc = sqlite3_initialize(); + if (rc != 0) { + fatalError("Call to sqlite3_initialize() failed with \(rc)") + } + let factory = sqlite3DatabaseFactory(initialStatements: []) kotlinDatabase = PowerSyncDatabase( factory: factory, From e1aca1a2cfbcf31a2ff6a479f4c74e94b79fe275 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Tue, 28 Oct 2025 15:03:20 +0100 Subject: [PATCH 08/15] Explicitly pass threadsafe 2 --- Package.swift | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index ce28be9..f502a95 100644 --- a/Package.swift +++ b/Package.swift @@ -22,7 +22,22 @@ var conditionalDependencies: [Package.Dependency] = [ .package( url: "https://github.com/sbooth/CSQLite.git", from: "3.50.4", - traits: ["ENABLE_SESSION"] + traits: [ + .defaults, + // CSQLite uses THREADSAFE=0 by default, which breaks PowerSync because we're using SQLite on + // multiple threads (it can lead to race conditions when closing connecting sharing resources + // like shared memory, causing crashes). + // THREADSAFE=2 overrides the default, and is safe to use as long as a single SQLite connection + // is not shared between threads. + // TODO: Technically, we should not use .defaults because there's a logical conflict between + // the threadsafe options. Instead, we should spell out all defaults again and remove that + // thread-safety option. + // However, despite the docs explicitly saying something else, it looks like there's no way to + // disable default traits anyway (XCode compiles sqlite3.c with the default option even without + // .defaults being included here). + "THREADSAFE_2", + "ENABLE_SESSION" + ] ) ] var conditionalTargets: [Target] = [] From 691395859cd877c551f03d161dab6360a5ee4360 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Tue, 28 Oct 2025 15:18:19 +0100 Subject: [PATCH 09/15] typo --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index f502a95..86b91d6 100644 --- a/Package.swift +++ b/Package.swift @@ -25,7 +25,7 @@ var conditionalDependencies: [Package.Dependency] = [ traits: [ .defaults, // CSQLite uses THREADSAFE=0 by default, which breaks PowerSync because we're using SQLite on - // multiple threads (it can lead to race conditions when closing connecting sharing resources + // multiple threads (it can lead to race conditions when closing connections sharing resources // like shared memory, causing crashes). // THREADSAFE=2 overrides the default, and is safe to use as long as a single SQLite connection // is not shared between threads. From 06486f6b31e817739e88de8eddec18e3e98341c0 Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Wed, 29 Oct 2025 11:26:00 +0200 Subject: [PATCH 10/15] init csqlite --- .../xcshareddata/swiftpm/Package.resolved | 13 +++++++++++-- .../Config/Configuration+PowerSync.swift | 10 ++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/Demo/GRDB Demo/GRDB Demo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Demo/GRDB Demo/GRDB Demo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index a9e2dc7..ca52990 100644 --- a/Demo/GRDB Demo/GRDB Demo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Demo/GRDB Demo/GRDB Demo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,6 +1,15 @@ { "originHash" : "84d25347b5249e7ab78894935a203b13d9d55e5a01b7fe00745bdf028062df42", "pins" : [ + { + "identity" : "csqlite", + "kind" : "remoteSourceControl", + "location" : "https://github.com/sbooth/CSQLite.git", + "state" : { + "revision" : "b1161e6c73fa68c25292f6bb697293d6c679f919", + "version" : "3.50.4" + } + }, { "identity" : "grdb.swift", "kind" : "remoteSourceControl", @@ -24,8 +33,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/powersync-ja/powersync-sqlite-core-swift.git", "state" : { - "revision" : "b2a81af14e9ad83393eb187bb02e62e6db8b5ad6", - "version" : "0.4.6" + "revision" : "9801f4aa0923c7f33fa479a01e643d00e7764f0b", + "version" : "0.4.8" } }, { diff --git a/Sources/PowerSyncGRDB/Config/Configuration+PowerSync.swift b/Sources/PowerSyncGRDB/Config/Configuration+PowerSync.swift index ec92093..13ab07f 100644 --- a/Sources/PowerSyncGRDB/Config/Configuration+PowerSync.swift +++ b/Sources/PowerSyncGRDB/Config/Configuration+PowerSync.swift @@ -2,6 +2,7 @@ import Foundation import GRDB import PowerSync import SQLite3 +import CSQLite public extension Configuration { /// Configures GRDB to work with PowerSync by registering required extensions and schema sources. @@ -23,6 +24,15 @@ public extension Configuration { mutating func configurePowerSync( schema: Schema ) { + + // This is a bit of a hack. We need to initialize CSQLite before we can use it. + // This needs to happen before the GRDB pool is created. + // Typically the config will be created before the GRDB pool is created, so we need to do this here. + let rc = sqlite3_initialize() + if rc != 0 { + fatalError("Call to sqlite3_initialize() failed with \(rc)") + } + // Register the PowerSync core extension prepareDatabase { database in #if os(watchOS) From 36a3b76b895a3327aeafcb3b9d762b590884dd62 Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Wed, 29 Oct 2025 14:39:18 +0200 Subject: [PATCH 11/15] Test: use SQLCipher --- .../xcshareddata/swiftpm/Package.resolved | 18 ++++---- Package.resolved | 20 ++++----- Package.swift | 44 ++++++++++--------- .../Kotlin/KotlinPowerSyncDatabaseImpl.swift | 2 +- .../Config/Configuration+PowerSync.swift | 2 +- .../Connections/GRDBConnectionPool.swift | 4 ++ 6 files changed, 48 insertions(+), 42 deletions(-) diff --git a/Demo/GRDB Demo/GRDB Demo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Demo/GRDB Demo/GRDB Demo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index ca52990..9d69379 100644 --- a/Demo/GRDB Demo/GRDB Demo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Demo/GRDB Demo/GRDB Demo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,15 +1,6 @@ { "originHash" : "84d25347b5249e7ab78894935a203b13d9d55e5a01b7fe00745bdf028062df42", "pins" : [ - { - "identity" : "csqlite", - "kind" : "remoteSourceControl", - "location" : "https://github.com/sbooth/CSQLite.git", - "state" : { - "revision" : "b1161e6c73fa68c25292f6bb697293d6c679f919", - "version" : "3.50.4" - } - }, { "identity" : "grdb.swift", "kind" : "remoteSourceControl", @@ -37,6 +28,15 @@ "version" : "0.4.8" } }, + { + "identity" : "sqlcipher.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/sqlcipher/SQLCipher.swift.git", + "state" : { + "revision" : "247b042574b4838c7eefdb95d6a250a843dcf1ad", + "version" : "4.11.0" + } + }, { "identity" : "supabase-swift", "kind" : "remoteSourceControl", diff --git a/Package.resolved b/Package.resolved index 6218469..0233827 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,15 +1,6 @@ { - "originHash" : "5fb9d75ebc419b20c046f3e982168b26c1977a575859515e1c79df3a34d6aec4", + "originHash" : "1547a031c0ecbbaab0fa533f54fbbbc7215881529928d0ad6ca6873e71e7415c", "pins" : [ - { - "identity" : "csqlite", - "kind" : "remoteSourceControl", - "location" : "https://github.com/sbooth/CSQLite.git", - "state" : { - "revision" : "b1161e6c73fa68c25292f6bb697293d6c679f919", - "version" : "3.50.4" - } - }, { "identity" : "grdb.swift", "kind" : "remoteSourceControl", @@ -27,6 +18,15 @@ "revision" : "9801f4aa0923c7f33fa479a01e643d00e7764f0b", "version" : "0.4.8" } + }, + { + "identity" : "sqlcipher.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/sqlcipher/SQLCipher.swift.git", + "state" : { + "revision" : "247b042574b4838c7eefdb95d6a250a843dcf1ad", + "version" : "4.11.0" + } } ], "version" : 3 diff --git a/Package.swift b/Package.swift index 9b90d5f..cf43d49 100644 --- a/Package.swift +++ b/Package.swift @@ -18,26 +18,27 @@ let localCoreExtension: String? = nil // With a local SDK, we point to a `Package.swift` within the Kotlin SDK containing a target pointing // towards a local framework build var conditionalDependencies: [Package.Dependency] = [ - .package( - url: "https://github.com/sbooth/CSQLite.git", - from: "3.50.4", - traits: [ - .defaults, - // CSQLite uses THREADSAFE=0 by default, which breaks PowerSync because we're using SQLite on - // multiple threads (it can lead to race conditions when closing connections sharing resources - // like shared memory, causing crashes). - // THREADSAFE=2 overrides the default, and is safe to use as long as a single SQLite connection - // is not shared between threads. - // TODO: Technically, we should not use .defaults because there's a logical conflict between - // the threadsafe options. Instead, we should spell out all defaults again and remove that - // thread-safety option. - // However, despite the docs explicitly saying something else, it looks like there's no way to - // disable default traits anyway (XCode compiles sqlite3.c with the default option even without - // .defaults being included here). - "THREADSAFE_2", - "ENABLE_SESSION" - ] - ) + // .package( + // url: "https://github.com/sbooth/CSQLite.git", + // from: "3.50.4", + // traits: [ + // .defaults, + // // CSQLite uses THREADSAFE=0 by default, which breaks PowerSync because we're using SQLite on + // // multiple threads (it can lead to race conditions when closing connections sharing resources + // // like shared memory, causing crashes). + // // THREADSAFE=2 overrides the default, and is safe to use as long as a single SQLite connection + // // is not shared between threads. + // // TODO: Technically, we should not use .defaults because there's a logical conflict between + // // the threadsafe options. Instead, we should spell out all defaults again and remove that + // // thread-safety option. + // // However, despite the docs explicitly saying something else, it looks like there's no way to + // // disable default traits anyway (XCode compiles sqlite3.c with the default option even without + // // .defaults being included here). + // "THREADSAFE_2", + // "ENABLE_SESSION" + // ] + // ) + .package(url: "https://github.com/sqlcipher/SQLCipher.swift.git", from: "4.10.0") ] var conditionalTargets: [Target] = [] var kotlinTargetDependency = Target.Dependency.target(name: "PowerSyncKotlin") @@ -108,7 +109,8 @@ let package = Package( dependencies: [ kotlinTargetDependency, .product(name: "PowerSyncSQLiteCore", package: corePackageName), - .product(name: "CSQLite", package: "CSQLite") + // .product(name: "CSQLite", package: "CSQLite") + .product(name: "SQLCipher", package: "SQLCipher.swift") ] ), .target( diff --git a/Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift b/Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift index a259bc2..40001fd 100644 --- a/Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift +++ b/Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift @@ -1,4 +1,4 @@ -import CSQLite +import SQLCipher import Foundation import PowerSyncKotlin diff --git a/Sources/PowerSyncGRDB/Config/Configuration+PowerSync.swift b/Sources/PowerSyncGRDB/Config/Configuration+PowerSync.swift index 13ab07f..f7cf4d8 100644 --- a/Sources/PowerSyncGRDB/Config/Configuration+PowerSync.swift +++ b/Sources/PowerSyncGRDB/Config/Configuration+PowerSync.swift @@ -2,7 +2,7 @@ import Foundation import GRDB import PowerSync import SQLite3 -import CSQLite +import SQLCipher public extension Configuration { /// Configures GRDB to work with PowerSync by registering required extensions and schema sources. diff --git a/Sources/PowerSyncGRDB/Connections/GRDBConnectionPool.swift b/Sources/PowerSyncGRDB/Connections/GRDBConnectionPool.swift index 6b9cabe..5707e78 100644 --- a/Sources/PowerSyncGRDB/Connections/GRDBConnectionPool.swift +++ b/Sources/PowerSyncGRDB/Connections/GRDBConnectionPool.swift @@ -75,6 +75,10 @@ actor GRDBConnectionPool: SQLiteConnectionPoolProtocol { try database.notifyChanges(in: Table(table)) } } + + if case .failure(let error) = result.blockResult { + throw error + } } func withAllConnections( From da4fc8c94d66b9dead4e5f4d1f08246f8c4f8593 Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Fri, 31 Oct 2025 11:18:41 +0200 Subject: [PATCH 12/15] close pool between tests --- Tests/PowerSyncGRDBTests/BasicTest.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/PowerSyncGRDBTests/BasicTest.swift b/Tests/PowerSyncGRDBTests/BasicTest.swift index adfdd10..b157406 100644 --- a/Tests/PowerSyncGRDBTests/BasicTest.swift +++ b/Tests/PowerSyncGRDBTests/BasicTest.swift @@ -82,6 +82,7 @@ final class GRDBTests: XCTestCase { override func tearDown() async throws { try await database.disconnectAndClear() database = nil + try pool.close() try await super.tearDown() } From 9e347cf8ede83d6e14176b9a34324a0b199d5ea2 Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Fri, 31 Oct 2025 13:22:37 +0200 Subject: [PATCH 13/15] cleanup docs --- Package.swift | 5 +++++ .../PowerSyncGRDB/Config/Configuration+PowerSync.swift | 10 ---------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/Package.swift b/Package.swift index cf43d49..8428a23 100644 --- a/Package.swift +++ b/Package.swift @@ -18,6 +18,9 @@ let localCoreExtension: String? = nil // With a local SDK, we point to a `Package.swift` within the Kotlin SDK containing a target pointing // towards a local framework build var conditionalDependencies: [Package.Dependency] = [ + // We can't currently build with GRDB using this package + // We could use traits for this + // .package( // url: "https://github.com/sbooth/CSQLite.git", // from: "3.50.4", @@ -38,6 +41,8 @@ var conditionalDependencies: [Package.Dependency] = [ // "ENABLE_SESSION" // ] // ) + + // Using SQLCipher here since GRDB doesn't compile with CSQLite .package(url: "https://github.com/sqlcipher/SQLCipher.swift.git", from: "4.10.0") ] var conditionalTargets: [Target] = [] diff --git a/Sources/PowerSyncGRDB/Config/Configuration+PowerSync.swift b/Sources/PowerSyncGRDB/Config/Configuration+PowerSync.swift index f7cf4d8..ec92093 100644 --- a/Sources/PowerSyncGRDB/Config/Configuration+PowerSync.swift +++ b/Sources/PowerSyncGRDB/Config/Configuration+PowerSync.swift @@ -2,7 +2,6 @@ import Foundation import GRDB import PowerSync import SQLite3 -import SQLCipher public extension Configuration { /// Configures GRDB to work with PowerSync by registering required extensions and schema sources. @@ -24,15 +23,6 @@ public extension Configuration { mutating func configurePowerSync( schema: Schema ) { - - // This is a bit of a hack. We need to initialize CSQLite before we can use it. - // This needs to happen before the GRDB pool is created. - // Typically the config will be created before the GRDB pool is created, so we need to do this here. - let rc = sqlite3_initialize() - if rc != 0 { - fatalError("Call to sqlite3_initialize() failed with \(rc)") - } - // Register the PowerSync core extension prepareDatabase { database in #if os(watchOS) From 41aafe1c61d809c4d57bbd2f228a42be93e6939e Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Tue, 4 Nov 2025 11:29:07 +0200 Subject: [PATCH 14/15] Export resolvePowerSyncLoadableExtensionPath from PowerSync SDK. Use resolvePowerSyncLoadableExtensionPath for GRDB integration. Fix WatchOS tests. --- ...esolvePowerSyncLoadableExtensionPath.swift | 9 +++ ...esolvePowerSyncLoadableExtensionPath.swift | 61 +++++++++++++++++++ .../Config/Configuration+PowerSync.swift | 50 ++++++++------- Tests/PowerSyncGRDBTests/BasicTest.swift | 16 ++++- 4 files changed, 107 insertions(+), 29 deletions(-) create mode 100644 Sources/PowerSync/Kotlin/kotlinResolvePowerSyncLoadableExtensionPath.swift create mode 100644 Sources/PowerSync/resolvePowerSyncLoadableExtensionPath.swift diff --git a/Sources/PowerSync/Kotlin/kotlinResolvePowerSyncLoadableExtensionPath.swift b/Sources/PowerSync/Kotlin/kotlinResolvePowerSyncLoadableExtensionPath.swift new file mode 100644 index 0000000..12af0ef --- /dev/null +++ b/Sources/PowerSync/Kotlin/kotlinResolvePowerSyncLoadableExtensionPath.swift @@ -0,0 +1,9 @@ +import PowerSyncKotlin + +func kotlinResolvePowerSyncLoadableExtensionPath() throws -> String? { + do { + return try PowerSyncKotlin.resolvePowerSyncLoadableExtensionPath() + } catch { + throw PowerSyncError.operationFailed(message: "Failed to resolve PowerSync loadable extension path: \(error.localizedDescription)") + } +} \ No newline at end of file diff --git a/Sources/PowerSync/resolvePowerSyncLoadableExtensionPath.swift b/Sources/PowerSync/resolvePowerSyncLoadableExtensionPath.swift new file mode 100644 index 0000000..ac28035 --- /dev/null +++ b/Sources/PowerSync/resolvePowerSyncLoadableExtensionPath.swift @@ -0,0 +1,61 @@ +/// Resolves the PowerSync SQLite extension path. +/// +/// This function returns the file system path to the PowerSync SQLite extension library. +/// For use with extension loading APIs or SQLite queries. +/// +/// ## Platform Behavior +/// +/// ### watchOS +/// On watchOS, the extension needs to be loaded statically. This function returns `nil` +/// on watchOS because the extension is statically linked and doesn't require a path. +/// Calling this function will auto-register the extension in watchOS. The static +/// initialization ensures the extension is available without requiring dynamic loading. +/// +/// ### Other Platforms +/// In other environments (iOS, macOS, tvOS, etc.), the extension needs to be loaded +/// dynamically using the path returned by this function. You'll need to: +/// 1. Enable extension loading on your SQLite connection +/// 2. Load the extension using the returned path +/// +/// ## Example Usage +/// +/// ### Loading with SQLite API +/// ```swift +/// guard let extensionPath = try resolvePowerSyncLoadableExtensionPath() else { +/// // On watchOS, extension is statically loaded, no path needed +/// return +/// } +/// +/// // Enable extension loading +/// sqlite3_enable_load_extension(db, 1) +/// +/// // Load the extension +/// var errorMsg: UnsafeMutablePointer? +/// let result = sqlite3_load_extension( +/// db, +/// extensionPath, +/// "sqlite3_powersync_init", +/// &errorMsg +/// ) +/// if result != SQLITE_OK { +/// // Handle error +/// } +/// ``` +/// +/// ### Loading with SQL Query +/// ```swift +/// guard let extensionPath = try resolvePowerSyncLoadableExtensionPath() else { +/// // On watchOS, extension is statically loaded, no path needed +/// return +/// } +/// let escapedPath = extensionPath.replacingOccurrences(of: "'", with: "''") +/// let query = "SELECT load_extension('\(escapedPath)', 'sqlite3_powersync_init')" +/// try db.execute(sql: query) +/// ``` +/// +/// - Returns: The file system path to the PowerSync SQLite extension, or `nil` on watchOS +/// (where the extension is statically loaded and doesn't require a path) +/// - Throws: An error if the extension path cannot be resolved on platforms that require it +public func resolvePowerSyncLoadableExtensionPath() throws -> String? { + return try kotlinResolvePowerSyncLoadableExtensionPath() +} diff --git a/Sources/PowerSyncGRDB/Config/Configuration+PowerSync.swift b/Sources/PowerSyncGRDB/Config/Configuration+PowerSync.swift index ec92093..42bdbd0 100644 --- a/Sources/PowerSyncGRDB/Config/Configuration+PowerSync.swift +++ b/Sources/PowerSyncGRDB/Config/Configuration+PowerSync.swift @@ -25,37 +25,35 @@ public extension Configuration { ) { // Register the PowerSync core extension prepareDatabase { database in - #if os(watchOS) - // Use static initialization on watchOS + let initResult = sqlite3_powersync_init(database.sqliteConnection, nil, nil) + if initResult != SQLITE_OK { + throw PowerSyncGRDBError.extensionLoadFailed("Could not initialize PowerSync") + } + guard let extensionPath = try resolvePowerSyncLoadableExtensionPath() else { + // We get the extension path for non WatchOS platforms. + // The Kotlin registration for automatically loading the extension does not seem to work. + // We explicitly initialize the extension here. let initResult = sqlite3_powersync_init(database.sqliteConnection, nil, nil) if initResult != SQLITE_OK { throw PowerSyncGRDBError.extensionLoadFailed("Could not initialize PowerSync statically") } - #else - // Dynamic loading on other platforms - guard let bundle = Bundle(identifier: "co.powersync.sqlitecore") else { - throw PowerSyncGRDBError.coreBundleNotFound + return + } + let extensionLoadResult = sqlite3_enable_load_extension(database.sqliteConnection, 1) + if extensionLoadResult != SQLITE_OK { + throw PowerSyncGRDBError.extensionLoadFailed("Could not enable extension loading") + } + var errorMsg: UnsafeMutablePointer? + let loadResult = sqlite3_load_extension(database.sqliteConnection, extensionPath, "sqlite3_powersync_init", &errorMsg) + if loadResult != SQLITE_OK { + if let errorMsg = errorMsg { + let message = String(cString: errorMsg) + sqlite3_free(errorMsg) + throw PowerSyncGRDBError.extensionLoadFailed(message) + } else { + throw PowerSyncGRDBError.unknownExtensionLoadError } - - // Construct the full path to the shared library inside the bundle - let fullPath = bundle.bundlePath + "/powersync-sqlite-core" - - let extensionLoadResult = sqlite3_enable_load_extension(database.sqliteConnection, 1) - if extensionLoadResult != SQLITE_OK { - throw PowerSyncGRDBError.extensionLoadFailed("Could not enable extension loading") - } - var errorMsg: UnsafeMutablePointer? - let loadResult = sqlite3_load_extension(database.sqliteConnection, fullPath, "sqlite3_powersync_init", &errorMsg) - if loadResult != SQLITE_OK { - if let errorMsg = errorMsg { - let message = String(cString: errorMsg) - sqlite3_free(errorMsg) - throw PowerSyncGRDBError.extensionLoadFailed(message) - } else { - throw PowerSyncGRDBError.unknownExtensionLoadError - } - } - #endif + } } // Supply the PowerSync views as a SchemaSource diff --git a/Tests/PowerSyncGRDBTests/BasicTest.swift b/Tests/PowerSyncGRDBTests/BasicTest.swift index b157406..41f1176 100644 --- a/Tests/PowerSyncGRDBTests/BasicTest.swift +++ b/Tests/PowerSyncGRDBTests/BasicTest.swift @@ -63,7 +63,16 @@ final class GRDBTests: XCTestCase { schema: schema ) - let documentsDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! + guard let documentsDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { + throw XCTestError( + .failureWhileWaiting, + userInfo: [NSLocalizedDescriptionKey: "Could not access documents directory"] + ) + } + + // Ensure the documents directory exists + try FileManager.default.createDirectory(at: documentsDir, withIntermediateDirectories: true, attributes: nil) + let dbURL = documentsDir.appendingPathComponent("test.sqlite") pool = try DatabasePool( path: dbURL.path, @@ -80,9 +89,10 @@ final class GRDBTests: XCTestCase { } override func tearDown() async throws { - try await database.disconnectAndClear() + try? await database?.disconnectAndClear() database = nil - try pool.close() + try? pool?.close() + pool = nil try await super.tearDown() } From fe8d65c344036e4890e0c8cf3a0e5a0eeb611af8 Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Tue, 4 Nov 2025 11:42:05 +0200 Subject: [PATCH 15/15] delete duplicate code block --- Sources/PowerSyncGRDB/Config/Configuration+PowerSync.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Sources/PowerSyncGRDB/Config/Configuration+PowerSync.swift b/Sources/PowerSyncGRDB/Config/Configuration+PowerSync.swift index 42bdbd0..f1ce149 100644 --- a/Sources/PowerSyncGRDB/Config/Configuration+PowerSync.swift +++ b/Sources/PowerSyncGRDB/Config/Configuration+PowerSync.swift @@ -25,10 +25,6 @@ public extension Configuration { ) { // Register the PowerSync core extension prepareDatabase { database in - let initResult = sqlite3_powersync_init(database.sqliteConnection, nil, nil) - if initResult != SQLITE_OK { - throw PowerSyncGRDBError.extensionLoadFailed("Could not initialize PowerSync") - } guard let extensionPath = try resolvePowerSyncLoadableExtensionPath() else { // We get the extension path for non WatchOS platforms. // The Kotlin registration for automatically loading the extension does not seem to work.