Skip to content

Commit 281558a

Browse files
Update README. Cleanup public APIs. WIP WatchOS.
1 parent 76aeb1c commit 281558a

File tree

10 files changed

+256
-87
lines changed

10 files changed

+256
-87
lines changed

README.md

Lines changed: 56 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ _[PowerSync](https://www.powersync.com) is a sync engine for building local-firs
88

99
This is the PowerSync SDK for Swift clients. The SDK reference is available [here](https://docs.powersync.com/client-sdk-references/swift), API references are [documented here](https://powersync-ja.github.io/powersync-swift/documentation/powersync/).
1010

11-
## Structure: Packages
11+
## Available Products
1212

13-
- [Sources](./Sources/)
13+
The SDK provides two main products:
1414

15-
- This is the Swift SDK implementation.
15+
- **PowerSync**: Core SDK with SQLite support for data synchronization.
16+
- **PowerSyncDynamic**: Forced dynamically linked version of `PowerSync` - useful for XCode previews.
17+
- **PowerSyncGRDB [ALPHA]**: GRDB integration allowing PowerSync to work with GRDB databases. This product is currently in an alpha release.
1618

1719
## Demo Apps / Example Projects
1820

@@ -38,6 +40,11 @@ Add
3840
name: "PowerSync",
3941
package: "powersync-swift"
4042
),
43+
// Optional: Add if using GRDB
44+
.product(
45+
name: "PowerSyncGRDB",
46+
package: "powersync-swift"
47+
)
4148
]
4249
)
4350
]
@@ -47,29 +54,59 @@ to your `Package.swift` file.
4754

4855
## Usage
4956

50-
Create a PowerSync client
57+
### Basic PowerSync Setup
5158

5259
```swift
5360
import PowerSync
5461

55-
let powersync = PowerSyncDatabase(
56-
schema: Schema(
57-
tables: [
58-
Table(
59-
name: "users",
60-
columns: [
61-
.text("count"),
62-
.integer("is_active"),
63-
.real("weight"),
64-
.text("description")
65-
]
66-
)
67-
]
68-
),
62+
let mySchema = Schema(
63+
tables: [
64+
Table(
65+
name: "users",
66+
columns: [
67+
.text("count"),
68+
.integer("is_active"),
69+
.real("weight"),
70+
.text("description")
71+
]
72+
)
73+
]
74+
)
75+
76+
let powerSync = PowerSyncDatabase(
77+
schema: mySchema,
6978
logger: DefaultLogger(minSeverity: .debug)
7079
)
7180
```
7281

82+
### GRDB Integration
83+
84+
If you're using [GRDB.swift](https://github.com/groue/GRDB.swift) by [Gwendal Roué](https://github.com/groue), you can integrate PowerSync with your existing database. Special thanks to Gwendal for their help in developing this integration.
85+
86+
**⚠️ Note:** The GRDB integration is currently in **alpha** release and the API may change significantly. While functional, it should be used with caution in production environments.
87+
88+
```swift
89+
import PowerSync
90+
import PowerSyncGRDB
91+
import GRDB
92+
93+
// Configure GRDB with PowerSync support
94+
var config = Configuration()
95+
config.configurePowerSync(schema: mySchema)
96+
97+
// Create database with PowerSync enabled
98+
let dbPool = try DatabasePool(
99+
path: dbPath,
100+
configuration: config
101+
)
102+
103+
let powerSync = try openPowerSyncWithGRDB(
104+
pool: dbPool,
105+
schema: mySchema,
106+
identifier: "app-db"
107+
)
108+
```
109+
73110
## Underlying Kotlin Dependency
74111

75112
The PowerSync Swift SDK makes use of the [PowerSync Kotlin Multiplatform SDK](https://github.com/powersync-ja/powersync-kotlin) and the API tool [SKIE](https://skie.touchlab.co/) under the hood to implement the Swift package.
@@ -89,4 +126,4 @@ XCode previews can be enabled by either:
89126

90127
Enabling `Editor -> Canvas -> Use Legacy Previews Execution` in XCode.
91128

92-
Or adding the `PowerSyncDynamic` product when adding PowerSync to your project. This product will assert that PowerSync should be dynamically linked, which restores XCode previews.
129+
Or adding the `PowerSyncDynamic` product when adding PowerSync to your project. This product will assert that PowerSync should be dynamically linked, which restores XCode previews.

Sources/PowerSync/Kotlin/KotlinSQLiteConnectionPool.swift

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,6 @@ final class SwiftSQLiteConnectionPoolAdapter: PowerSyncKotlin.SwiftPoolAdapter {
3232
}
3333
}
3434

35-
func __processPowerSyncUpdates(updates: Set<String>) async throws {
36-
return try await wrapExceptions {
37-
try await pool.processPowerSyncUpdates(updates)
38-
}
39-
}
40-
4135
func __dispose() async throws {
4236
return try await wrapExceptions {
4337
updateTrackingTask?.cancel()
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import PowerSyncKotlin
2+
3+
func kotlinWithSession<ReturnType>(
4+
db: OpaquePointer,
5+
action: @escaping () throws -> ReturnType,
6+
onComplete: @escaping (Result<ReturnType, Error>, Set<String>) -> Void,
7+
) throws {
8+
try withSession(
9+
db: UnsafeMutableRawPointer(db),
10+
onComplete: { powerSyncResult, updates in
11+
let result: Result<ReturnType, Error>
12+
switch powerSyncResult {
13+
case let success as PowerSyncResult.Success:
14+
do {
15+
let casted = try safeCast(success.value, to: ReturnType.self)
16+
result = .success(casted)
17+
} catch {
18+
result = .failure(error)
19+
}
20+
21+
case let failure as PowerSyncResult.Failure:
22+
result = .failure(failure.exception.asError())
23+
24+
default:
25+
result = .failure(PowerSyncError.operationFailed(message: "Unknown error encountered when processing session"))
26+
}
27+
onComplete(result, updates)
28+
},
29+
block: {
30+
do {
31+
return try PowerSyncResult.Success(value: action())
32+
} catch {
33+
return PowerSyncResult.Failure(exception: error.toPowerSyncError())
34+
}
35+
}
36+
)
37+
}

Sources/PowerSync/Protocol/SQLiteConnectionPool.swift

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,6 @@ public protocol SQLiteConnectionLease {
1111
public protocol SQLiteConnectionPoolProtocol {
1212
var tableUpdates: AsyncStream<Set<String>> { get }
1313

14-
/// Processes updates from PowerSync, notifying any active leases of changes
15-
/// (made by PowerSync) to tracked tables.
16-
func processPowerSyncUpdates(_ updates: Set<String>) async throws
17-
1814
/// Calls the callback with a read-only connection temporarily leased from the pool.
1915
func read(
2016
onConnection: @Sendable @escaping (SQLiteConnectionLease) throws -> Void,
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import Foundation
2+
3+
/// Executes an action within a SQLite database connection session and handles its result.
4+
///
5+
/// The Raw SQLite connection is only available in some niche scenarios.
6+
///
7+
/// - Executes the provided action in a SQLite session
8+
/// - Handles success/failure results
9+
/// - Tracks table updates during execution
10+
/// - Provides type-safe result handling
11+
///
12+
/// Example usage:
13+
/// ```swift
14+
/// try withSession(db: database) {
15+
/// return try someOperation()
16+
/// } onComplete: { result, updates in
17+
/// switch result {
18+
/// case .success(let value):
19+
/// print("Operation succeeded with: \(value)")
20+
/// case .failure(let error):
21+
/// print("Operation failed: \(error)")
22+
/// }
23+
/// }
24+
/// ```
25+
///
26+
/// - Parameters:
27+
/// - db: The database connection pointer
28+
/// - action: The operation to execute within the session
29+
/// - onComplete: Callback that receives the operation result and set of updated tables
30+
/// - Throws: Errors from session initialization or execution
31+
public func withSession<ReturnType>(
32+
db: OpaquePointer,
33+
action: @escaping () throws -> ReturnType,
34+
onComplete: @escaping (Result<ReturnType, Error>, Set<String>) -> Void,
35+
) throws {
36+
return try kotlinWithSession(
37+
db: db,
38+
action: action,
39+
onComplete: onComplete,
40+
)
41+
}

Sources/PowerSyncGRDB/Config/Configuration+PowerSync.swift

Lines changed: 45 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,58 +3,67 @@ import GRDB
33
import PowerSync
44
import SQLite3
55

6-
/// Extension for GRDB `Configuration` to add PowerSync support.
7-
///
8-
/// Call `configurePowerSync(schema:)` on your existing GRDB `Configuration` to:
9-
/// - Register the PowerSync SQLite core extension (required for PowerSync features).
10-
/// - Add PowerSync schema views to your database schema source.
11-
///
12-
/// This enables PowerSync replication and view management in your GRDB database.
13-
///
14-
/// Example usage:
15-
/// ```swift
16-
/// var config = Configuration()
17-
/// config.configurePowerSync(schema: mySchema)
18-
/// let dbQueue = try DatabaseQueue(path: dbPath, configuration: config)
19-
/// ```
20-
///
21-
/// - Parameter schema: The PowerSync `Schema` describing your sync views.
226
public extension Configuration {
7+
/// Configures GRDB to work with PowerSync by registering required extensions and schema sources.
8+
///
9+
/// Call this method on your existing GRDB `Configuration` to:
10+
/// - Register the PowerSync SQLite core extension (required for PowerSync features).
11+
/// - Add PowerSync schema views to your database schema source.
12+
///
13+
/// This enables PowerSync replication and view management in your GRDB database.
14+
///
15+
/// Example usage:
16+
/// ```swift
17+
/// var config = Configuration()
18+
/// config.configurePowerSync(schema: mySchema)
19+
/// let dbQueue = try DatabaseQueue(path: dbPath, configuration: config)
20+
/// ```
21+
///
22+
/// - Parameter schema: The PowerSync `Schema` describing your sync views.
2323
mutating func configurePowerSync(
2424
schema: Schema
2525
) {
2626
// Register the PowerSync core extension
2727
prepareDatabase { database in
28-
guard let bundle = Bundle(identifier: "co.powersync.sqlitecore") else {
29-
throw PowerSyncGRDBError.coreBundleNotFound
30-
}
28+
#if os(watchOS)
29+
// Use static initialization on watchOS
30+
let initResult = sqlite3_powersync_init_static(database.sqliteConnection, nil)
31+
if initResult != SQLITE_OK {
32+
throw PowerSyncGRDBError.extensionLoadFailed("Could not initialize PowerSync statically")
33+
}
34+
#else
35+
// Dynamic loading on other platforms
36+
guard let bundle = Bundle(identifier: "co.powersync.sqlitecore") else {
37+
throw PowerSyncGRDBError.coreBundleNotFound
38+
}
3139

32-
// Construct the full path to the shared library inside the bundle
33-
let fullPath = bundle.bundlePath + "/powersync-sqlite-core"
40+
// Construct the full path to the shared library inside the bundle
41+
let fullPath = bundle.bundlePath + "/powersync-sqlite-core"
3442

35-
let extensionLoadResult = sqlite3_enable_load_extension(database.sqliteConnection, 1)
36-
if extensionLoadResult != SQLITE_OK {
37-
throw PowerSyncGRDBError.extensionLoadFailed("Could not enable extension loading")
38-
}
39-
var errorMsg: UnsafeMutablePointer<Int8>?
40-
let loadResult = sqlite3_load_extension(database.sqliteConnection, fullPath, "sqlite3_powersync_init", &errorMsg)
41-
if loadResult != SQLITE_OK {
42-
if let errorMsg = errorMsg {
43-
let message = String(cString: errorMsg)
44-
sqlite3_free(errorMsg)
45-
throw PowerSyncGRDBError.extensionLoadFailed(message)
46-
} else {
47-
throw PowerSyncGRDBError.unknownExtensionLoadError
43+
let extensionLoadResult = sqlite3_enable_load_extension(database.sqliteConnection, 1)
44+
if extensionLoadResult != SQLITE_OK {
45+
throw PowerSyncGRDBError.extensionLoadFailed("Could not enable extension loading")
46+
}
47+
var errorMsg: UnsafeMutablePointer<Int8>?
48+
let loadResult = sqlite3_load_extension(database.sqliteConnection, fullPath, "sqlite3_powersync_init", &errorMsg)
49+
if loadResult != SQLITE_OK {
50+
if let errorMsg = errorMsg {
51+
let message = String(cString: errorMsg)
52+
sqlite3_free(errorMsg)
53+
throw PowerSyncGRDBError.extensionLoadFailed(message)
54+
} else {
55+
throw PowerSyncGRDBError.unknownExtensionLoadError
56+
}
4857
}
49-
}
58+
#endif
5059
}
5160

5261
// Supply the PowerSync views as a SchemaSource
5362
let powerSyncSchemaSource = PowerSyncSchemaSource(
5463
schema: schema
5564
)
5665
if let schemaSource = schemaSource {
57-
self.schemaSource = schemaSource.then(powerSyncSchemaSource)
66+
self.schemaSource = powerSyncSchemaSource.then(schemaSource)
5867
} else {
5968
schemaSource = powerSyncSchemaSource
6069
}

Sources/PowerSyncGRDB/Connections/GRDBConnectionPool.swift

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import SQLite3
1212
/// - Provides async streams of table updates for replication.
1313
/// - Bridges GRDB's managed connections to PowerSync's lease abstraction.
1414
/// - Allows both read and write access to raw SQLite connections.
15-
public final class GRDBConnectionPool: SQLiteConnectionPoolProtocol {
15+
final class GRDBConnectionPool: SQLiteConnectionPoolProtocol {
1616
let pool: DatabasePool
1717

1818
public private(set) var tableUpdates: AsyncStream<Set<String>>
@@ -41,13 +41,6 @@ public final class GRDBConnectionPool: SQLiteConnectionPoolProtocol {
4141
try await pool.write { database in
4242
for table in updates {
4343
try database.notifyChanges(in: Table(table))
44-
if table.hasPrefix("ps_data__") {
45-
let stripped = String(table.dropFirst("ps_data__".count))
46-
try database.notifyChanges(in: Table(stripped))
47-
} else if table.hasPrefix("ps_data_local__") {
48-
let stripped = String(table.dropFirst("ps_data_local__".count))
49-
try database.notifyChanges(in: Table(stripped))
50-
}
5144
}
5245
}
5346
// Pass the updates to the output stream
@@ -69,16 +62,31 @@ public final class GRDBConnectionPool: SQLiteConnectionPoolProtocol {
6962
) async throws {
7063
// Don't start an explicit transaction, we do this internally
7164
try await pool.writeWithoutTransaction { database in
72-
try onConnection(
73-
GRDBConnectionLease(database: database)
74-
)
65+
guard let pointer = database.sqliteConnection else {
66+
throw PowerSyncGRDBError.connectionUnavailable
67+
}
68+
69+
try withSession(
70+
db: pointer,
71+
) {
72+
try onConnection(
73+
GRDBConnectionLease(database: database)
74+
)
75+
} onComplete: { _, changes in
76+
self.tableUpdatesContinuation?.yield(changes)
77+
}
7578
}
7679
}
7780

7881
public func withAllConnections(
79-
onConnection _: @escaping (SQLiteConnectionLease, [SQLiteConnectionLease]) throws -> Void
82+
onConnection: @Sendable @escaping (SQLiteConnectionLease, [SQLiteConnectionLease]) throws -> Void
8083
) async throws {
81-
// TODO:
84+
// FIXME, we currently don't support updating the schema
85+
try await pool.write { database in
86+
let lease = try GRDBConnectionLease(database: database)
87+
try onConnection(lease, [])
88+
}
89+
pool.invalidateReadOnlyConnections()
8290
}
8391

8492
public func close() throws {

0 commit comments

Comments
 (0)