Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
245e3d2
wip: grdb connection pool
stevensJourney Sep 7, 2025
8428135
wip: grdb
stevensJourney Sep 7, 2025
dbd9c09
Use latest GRDB package. Update tests and queries.
stevensJourney Sep 8, 2025
6f32934
wip: table update hooks
stevensJourney Sep 8, 2025
e2681f6
Add test for GRDB updates triggered by GRDB
stevensJourney Sep 16, 2025
a79ec5c
add join test
stevensJourney Sep 17, 2025
8a278a1
WIP: Add GRDB demo app
stevensJourney Sep 18, 2025
4002d85
demo improvements
stevensJourney Sep 19, 2025
6f0e630
Table updates from PowerSync side
stevensJourney Sep 23, 2025
41174b1
Use SQLite Session API for PowerSync updates.
stevensJourney Sep 25, 2025
7aae6cf
Update GRDB dependency
stevensJourney Sep 25, 2025
e970fd4
Merge remote-tracking branch 'origin/main' into grdb
stevensJourney Sep 25, 2025
76aeb1c
demo update
stevensJourney Sep 25, 2025
281558a
Update README. Cleanup public APIs. WIP WatchOS.
stevensJourney Sep 29, 2025
7beff16
Merge remote-tracking branch 'origin/main' into grdb
stevensJourney Oct 3, 2025
a81986e
Update READMEs
stevensJourney Oct 3, 2025
1c1f2bb
Register extension on WatchOS
stevensJourney Oct 3, 2025
6afa299
Swift Strict Concurrency
stevensJourney Oct 22, 2025
b4d9cbf
Demo and Docs cleanup
stevensJourney Oct 23, 2025
27512a8
Export resolvePowerSyncLoadableExtensionPath from PowerSync SDK.
stevensJourney Nov 4, 2025
fa38d31
delete unused demo tests
stevensJourney Nov 4, 2025
c520836
delete duplicate code block
stevensJourney Nov 4, 2025
6a54ef1
Update README.md
stevensJourney Nov 5, 2025
5efbef4
Update README.md
stevensJourney Nov 6, 2025
4f112b2
Avoid casting in kotlinWithSession
stevensJourney Nov 6, 2025
81074db
Update WatchOS extension loading
stevensJourney Nov 6, 2025
f08484e
Fix withAllConnections issue
stevensJourney Nov 6, 2025
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
20 changes: 20 additions & 0 deletions Demo/GRDB Demo/GRDB Demo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,17 @@
DEVELOPMENT_TEAM = ZGT7463CVJ;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
ENABLE_PREVIEWS = YES;
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
ENABLE_RESOURCE_ACCESS_PRINTING = NO;
ENABLE_RESOURCE_ACCESS_USB = NO;
ENABLE_USER_SELECTED_FILES = readonly;
GENERATE_INFOPLIST_FILE = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
Expand Down Expand Up @@ -481,7 +491,17 @@
DEVELOPMENT_TEAM = ZGT7463CVJ;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
ENABLE_PREVIEWS = YES;
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
ENABLE_RESOURCE_ACCESS_PRINTING = NO;
ENABLE_RESOURCE_ACCESS_USB = NO;
ENABLE_USER_SELECTED_FILES = readonly;
GENERATE_INFOPLIST_FILE = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
Expand Down
8 changes: 4 additions & 4 deletions Demo/GRDB Demo/GRDB Demo/Data/Todo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import PowerSync
let todosTable = Table(
name: "todos",
columns: [
.text("name"),
.text("description"),
.text("list_id"),
// Conversion should automatically be handled by GRDB
.integer("completed"),
Expand All @@ -16,7 +16,7 @@ let todosTable = Table(

struct Todo: Codable, Equatable, Identifiable, FetchableRecord, PersistableRecord {
var id: String
var name: String
var description: String
var listId: String
var isCompleted: Bool
var completedAt: Date?
Expand All @@ -25,15 +25,15 @@ struct Todo: Codable, Equatable, Identifiable, FetchableRecord, PersistableRecor

enum CodingKeys: String, CodingKey {
case id
case name
case description
case listId = "list_id"
case isCompleted = "completed"
case completedAt = "completed_at"
}

enum Columns {
static let id = Column(CodingKeys.id)
static let name = Column(CodingKeys.name)
static let description = Column(CodingKeys.description)
static let listId = Column(CodingKeys.listId)
static let isCompleted = Column(CodingKeys.isCompleted)
static let completedAt = Column(CodingKeys.completedAt)
Expand Down
4 changes: 1 addition & 3 deletions Demo/GRDB Demo/GRDB Demo/GRDB_DemoApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,7 @@ func openDatabase()
.appendingPathComponent("test.sqlite")

var config = Configuration()

configurePowerSync(
config: &config,
config.configurePowerSync(
schema: schema
)

Expand Down
4 changes: 2 additions & 2 deletions Demo/GRDB Demo/GRDB Demo/Models/TodosViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ struct ListsTodosRequest: ValueObservationQueryable {
func fetch(_ database: Database) throws -> [Todo] {
try Todo
.filter(Todo.Columns.listId == list.id)
.order(Todo.Columns.name)
.order(Todo.Columns.description)
.order(Todo.Columns.isCompleted)
.fetchAll(database)
}
Expand All @@ -36,7 +36,7 @@ class TodoViewModel {
try grdb.write { database in
try Todo(
id: UUID().uuidString,
name: name,
description: name,
listId: listId,
isCompleted: false
).insert(database)
Expand Down
32 changes: 20 additions & 12 deletions Demo/GRDB Demo/GRDB Demo/Screens/StatusIndicatorView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,27 @@ struct StatusIndicatorView<Content: View>: View {
}

@State var statusImageName: String = "wifi.slash"
@State private var showErrorAlert = false
@State var directionStatusImageName: String?

let content: () -> Content

var body: some View {
content()
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
ToolbarItem(placement: .automatic) {
Button {
if powerSync.currentStatus.anyError != nil {
showErrorAlert = true
if let error = powerSync.currentStatus.anyError {
viewModels.errorViewModel.report("\(error)")
}
} label: {
Image(systemName: statusImageName)
ZStack {
// Network status
Image(systemName: statusImageName)
// Upload/Download status
if let name = directionStatusImageName {
Image(systemName: name)
}
}
}
.contextMenu {
if powerSync.currentStatus.connected || powerSync.currentStatus.connecting {
Expand All @@ -43,13 +50,6 @@ struct StatusIndicatorView<Content: View>: View {
}
}
}
.alert(isPresented: $showErrorAlert) {
Alert(
title: Text("Error"),
message: Text(String("\(powerSync.currentStatus.anyError ?? "Unknown error")")),
dismissButton: .default(Text("OK"))
)
}
.task {
do {
for try await status in powerSync.currentStatus.asFlow() {
Expand All @@ -62,6 +62,14 @@ struct StatusIndicatorView<Content: View>: View {
} else {
statusImageName = "wifi.slash"
}

if status.downloading {
directionStatusImageName = "chevron.down.2"
} else if status.uploading {
directionStatusImageName = "chevron.up.2"
} else {
directionStatusImageName = nil
}
}
} catch {
print("Could not monitor status")
Expand Down
2 changes: 2 additions & 0 deletions Demo/GRDB Demo/GRDB Demo/Screens/signin/SigninScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,10 @@ struct SigninScreen: View {
VStack(spacing: 16) {
TextField("Email", text: $email)
.textFieldStyle(RoundedBorderTextFieldStyle())
#if os (iOS) || os (tvOS) || targetEnvironment(macCatalyst)
.autocapitalization(.none)
.keyboardType(.emailAddress)
#endif
.focused($emailFieldFocused)

SecureField("Password", text: $password)
Expand Down
2 changes: 1 addition & 1 deletion Demo/GRDB Demo/GRDB Demo/Screens/todos/TodoItemView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ struct TodoItemView: View {
var body: some View {
VStack {
HStack {
Text(todo.name).font(.title)
Text(todo.description).font(.title)
Spacer()
Button {
try? viewModels.todoViewModel.toggleCompleted(todo: todo)
Expand Down
72 changes: 61 additions & 11 deletions Sources/PowerSync/Kotlin/KotlinSQLiteConnectionPool.swift
Original file line number Diff line number Diff line change
@@ -1,20 +1,41 @@
import PowerSyncKotlin

class KotlinLeaseAdapter: PowerSyncKotlin.SwiftLeaseAdapter {
let pointer: UnsafeMutableRawPointer

init(
lease: SQLiteConnectionLease
) {
pointer = UnsafeMutableRawPointer(lease.pointer)
}
}

final class SwiftSQLiteConnectionPoolAdapter: PowerSyncKotlin.SwiftPoolAdapter {
let pool: SQLiteConnectionPoolProtocol
var updateTrackingTask: Task<Void, Never>?

init(
pool: SQLiteConnectionPoolProtocol
) {
self.pool = pool
}

func getPendingUpdates() -> Set<String> {
return pool.getPendingUpdates()
func linkUpdates(callback: any KotlinSuspendFunction1) {
updateTrackingTask = Task {
do {
for try await updates in pool.tableUpdates {
_ = try await callback.invoke(p1: updates)
}
} catch {
// none of these calls should actually throw
}
}
}

func __closePool() async throws {
do {
updateTrackingTask?.cancel()
updateTrackingTask = nil
try pool.close()
} catch {
try? PowerSyncKotlin.throwPowerSyncException(
Expand All @@ -26,10 +47,22 @@ final class SwiftSQLiteConnectionPoolAdapter: PowerSyncKotlin.SwiftPoolAdapter {
}
}

func __leaseRead(callback: @escaping (Any) -> Void) async throws {
func __leaseRead(callback: any LeaseCallback) async throws {
do {
try await pool.read { pointer in
callback(UInt(bitPattern: pointer))
var errorToThrow: Error?
try await pool.read { lease in
do {
try callback.execute(
lease: KotlinLeaseAdapter(
lease: lease
)
)
} catch {
errorToThrow = error
}
}
if let errorToThrow {
throw errorToThrow
}
} catch {
try? PowerSyncKotlin.throwPowerSyncException(
Expand All @@ -41,10 +74,22 @@ final class SwiftSQLiteConnectionPoolAdapter: PowerSyncKotlin.SwiftPoolAdapter {
}
}

func __leaseWrite(callback: @escaping (Any) -> Void) async throws {
func __leaseWrite(callback: any LeaseCallback) async throws {
do {
try await pool.write { pointer in
callback(UInt(bitPattern: pointer))
var errorToThrow: Error?
try await pool.write { lease in
do {
try callback.execute(
lease: KotlinLeaseAdapter(
lease: lease
)
)
} catch {
errorToThrow = error
}
}
if let errorToThrow {
throw errorToThrow
}
} catch {
try? PowerSyncKotlin.throwPowerSyncException(
Expand All @@ -56,11 +101,16 @@ final class SwiftSQLiteConnectionPoolAdapter: PowerSyncKotlin.SwiftPoolAdapter {
}
}

func __leaseAll(callback: @escaping (Any, [Any]) -> Void) async throws {
func __leaseAll(callback: any AllLeaseCallback) async throws {
// TODO, actually use all connections
do {
try await pool.write { pointer in
callback(UInt(bitPattern: pointer), [])
try await pool.write { lease in
try? callback.execute(
writeLease: KotlinLeaseAdapter(
lease: lease
),
readLeases: []
)
}
} catch {
try? PowerSyncKotlin.throwPowerSyncException(
Expand Down
14 changes: 9 additions & 5 deletions Sources/PowerSync/Protocol/SQLiteConnectionPool.swift
Original file line number Diff line number Diff line change
@@ -1,25 +1,29 @@
import Foundation

public protocol SQLiteConnectionLease {
var pointer: OpaquePointer { get }
}

/// An implementation of a connection pool providing asynchronous access to a single writer and multiple readers.
/// This is the underlying pool implementation on which the higher-level PowerSync Swift SDK is built on.
public protocol SQLiteConnectionPoolProtocol {
func getPendingUpdates() -> Set<String>
var tableUpdates: AsyncStream<Set<String>> { get }

/// Calls the callback with a read-only connection temporarily leased from the pool.
func read(
onConnection: @Sendable @escaping (OpaquePointer) -> Void,
onConnection: @Sendable @escaping (SQLiteConnectionLease) -> Void,
) async throws

/// Calls the callback with a read-write connection temporarily leased from the pool.
func write(
onConnection: @Sendable @escaping (OpaquePointer) -> Void,
onConnection: @Sendable @escaping (SQLiteConnectionLease) -> Void,
) async throws

/// Invokes the callback with all connections leased from the pool.
func withAllConnections(
onConnection: @Sendable @escaping (
_ writer: OpaquePointer,
_ readers: [OpaquePointer]
_ writer: SQLiteConnectionLease,
_ readers: [SQLiteConnectionLease]
) -> Void,
) async throws

Expand Down
62 changes: 62 additions & 0 deletions Sources/PowerSyncGRDB/Config/Configuration+PowerSync.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import Foundation
import GRDB
import PowerSync
import SQLite3

/// Extension for GRDB `Configuration` to add PowerSync support.
///
/// Call `configurePowerSync(schema:)` on your existing GRDB `Configuration` to:
/// - Register the PowerSync SQLite core extension (required for PowerSync features).
/// - Add PowerSync schema views to your database schema source.
///
/// This enables PowerSync replication and view management in your GRDB database.
///
/// Example usage:
/// ```swift
/// var config = Configuration()
/// config.configurePowerSync(schema: mySchema)
/// let dbQueue = try DatabaseQueue(path: dbPath, configuration: config)
/// ```
///
/// - Parameter schema: The PowerSync `Schema` describing your sync views.
public extension Configuration {
mutating func configurePowerSync(
schema: Schema
) {
// Register the PowerSync core extension
prepareDatabase { database in
guard let bundle = Bundle(identifier: "co.powersync.sqlitecore") else {
throw PowerSyncGRDBError.coreBundleNotFound
}

// 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<Int8>?
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
}
}
}

// Supply the PowerSync views as a SchemaSource
let powerSyncSchemaSource = PowerSyncSchemaSource(
schema: schema
)
if let schemaSource = schemaSource {
self.schemaSource = schemaSource.then(powerSyncSchemaSource)
} else {
schemaSource = powerSyncSchemaSource
}
}
}
Loading
Loading