diff --git a/Package.resolved b/Package.resolved index a62a2d5..06bbfdb 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,14 +1,15 @@ { + "originHash" : "07cca32b599d2d8e93fb0f48a44643e0cac92225db615070dba76b849ec0ded7", "pins" : [ { "identity" : "powersync-sqlite-core-swift", "kind" : "remoteSourceControl", "location" : "https://github.com/powersync-ja/powersync-sqlite-core-swift.git", "state" : { - "revision" : "b2a81af14e9ad83393eb187bb02e62e6db8b5ad6", - "version" : "0.4.6" + "revision" : "9743740980cd488a11f50cffe62ed34a9739a135", + "version" : "0.4.10" } } ], - "version" : 2 + "version" : 3 } diff --git a/Package.swift b/Package.swift index cece14d..97986b4 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 @@ -25,15 +25,15 @@ 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 { // 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.9.0/PowersyncKotlinRelease.zip", + checksum: "6d9847391ab2bbbca1f6a7abe163f0682ddca4a559ef5a1d2567b3e62e7d9979" )) } @@ -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.6" + exact: "0.4.10" )) } @@ -73,7 +73,9 @@ let package = Package( targets: ["PowerSync"] ) ], - dependencies: conditionalDependencies, + dependencies: conditionalDependencies + [ + .package(path: "/Users/simon/src/CSQLite") + ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. // Targets can depend on other targets in this package and products from dependencies. @@ -81,7 +83,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/KotlinAdapter.swift b/Sources/PowerSync/Kotlin/KotlinAdapter.swift index 8e85220..a294b81 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 10bbd97..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,7 +16,12 @@ final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol, dbFilename: String, logger: DatabaseLogger ) { - let factory = PowerSyncKotlin.DatabaseDriverFactory() + let rc = sqlite3_initialize(); + if (rc != 0) { + fatalError("Call to sqlite3_initialize() failed with \(rc)") + } + + let factory = sqlite3DatabaseFactory(initialStatements: []) kotlinDatabase = PowerSyncDatabase( factory: factory, schema: KotlinAdapter.Schema.toKotlin(schema), @@ -87,9 +93,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 + clearLocal: clearLocal, + soft: soft ) } diff --git a/Sources/PowerSync/Protocol/PowerSyncDatabaseProtocol.swift b/Sources/PowerSync/Protocol/PowerSyncDatabaseProtocol.swift index cd112a5..d0a7161 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. @@ -290,7 +298,15 @@ public extension PowerSyncDatabaseProtocol { } func disconnectAndClear() async throws { - try await disconnectAndClear(clearLocal: true) + 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? { 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) + } }