Skip to content
Open
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
24 changes: 6 additions & 18 deletions .github/workflows/build_and_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
16 changes: 13 additions & 3 deletions Package.resolved

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

36 changes: 29 additions & 7 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 @@ -18,22 +18,43 @@ 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: [
.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"
]
)
]
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 {
// 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"
))
}

Expand All @@ -45,7 +66,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"
))
}

Expand Down Expand Up @@ -81,7 +102,8 @@ let package = Package(
name: packageName,
dependencies: [
kotlinTargetDependency,
.product(name: "PowerSyncSQLiteCore", package: corePackageName)
.product(name: "PowerSyncSQLiteCore", package: corePackageName),
.product(name: "CSQLite", package: "CSQLite")
]
),
.testTarget(
Expand Down
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 @@ -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),
Expand Down Expand Up @@ -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
)
}

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 @@ -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? {
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)
}
}