Skip to content
Merged
23 changes: 6 additions & 17 deletions .github/workflows/build_and_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,14 @@ jobs:
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable

- name: Test with strict concurrency
run: |
swift build -Xswiftc -strict-concurrency=complete
swift test -Xswiftc -strict-concurrency=complete

- name: Build and Test
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
run: |
sed -i '' 's|^// swift-tools-version:.*$|// swift-tools-version:6.1|' Package.swift
- name: Build and Test
run: |
swift build -Xswiftc -strict-concurrency=complete
swift test -Xswiftc -strict-concurrency=complete

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

13 changes: 11 additions & 2 deletions Package.resolved

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

38 changes: 22 additions & 16 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -25,16 +25,18 @@ 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"
))
conditionalTargets.append(
.binaryTarget(
name: "PowerSyncKotlin",
url:
"https://github.com/powersync-ja/powersync-kotlin/releases/download/v1.9.0/PowersyncKotlinRelease.zip",
checksum: "6d9847391ab2bbbca1f6a7abe163f0682ddca4a559ef5a1d2567b3e62e7d9979"
))
}

var corePackageName = "powersync-sqlite-core-swift"
Expand All @@ -43,18 +45,19 @@ if let corePath = localCoreExtension {
corePackageName = "powersync-sqlite-core"
} else {
// 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"
))
conditionalDependencies.append(
.package(
url: "https://github.com/powersync-ja/powersync-sqlite-core-swift.git",
exact: "0.4.10"
))
}

let package = Package(
name: packageName,
platforms: [
.iOS(.v15),
.macOS(.v12),
.watchOS(.v9)
.watchOS(.v9),
],
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
Expand All @@ -71,22 +74,25 @@ let package = Package(
// Dynamic linking is particularly important for XCode previews.
type: .dynamic,
targets: ["PowerSync"]
)
),
],
dependencies: conditionalDependencies + [
.package(url: "https://github.com/powersync-ja/CSQLite.git", revision: "3.51.1")
],
dependencies: conditionalDependencies,
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.
.target(
name: packageName,
dependencies: [
kotlinTargetDependency,
.product(name: "PowerSyncSQLiteCore", package: corePackageName)
.product(name: "PowerSyncSQLiteCore", package: corePackageName),
.product(name: "CSQLite", package: "CSQLite"),
]
),
.testTarget(
name: "PowerSyncTests",
dependencies: ["PowerSync"]
)
),
] + conditionalTargets
)
3 changes: 2 additions & 1 deletion Sources/PowerSync/Kotlin/KotlinAdapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
}

Expand Down
13 changes: 10 additions & 3 deletions Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Foundation
import PowerSyncKotlin
import CSQLite

final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol,
// `PowerSyncKotlin.PowerSyncDatabase` cannot be marked as Sendable
Expand All @@ -16,7 +17,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),
Expand Down Expand Up @@ -89,9 +95,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
)
}

Expand Down
28 changes: 22 additions & 6 deletions Sources/PowerSync/Protocol/PowerSyncDatabaseProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -301,7 +309,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? {
Expand Down
6 changes: 5 additions & 1 deletion Sources/PowerSync/Protocol/Schema/RawTable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

Expand Down
15 changes: 15 additions & 0 deletions Tests/PowerSyncTests/CrudTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}