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
9 changes: 9 additions & 0 deletions Package.resolved

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

21 changes: 19 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ let packageName = "PowerSync"

// Set this to the absolute path of your Kotlin SDK checkout if you want to use a local Kotlin
// build. Also see docs/LocalBuild.md for details
let localKotlinSdkOverride: String? = nil
let localKotlinSdkOverride: String? = "/Users/stevenontong/Documents/platform_code/powersync/powersync-kotlin"

// Set this to the absolute path of your powersync-sqlite-core checkout if you want to use a
// local build of the core extension.
Expand Down Expand Up @@ -71,9 +71,15 @@ let package = Package(
// Dynamic linking is particularly important for XCode previews.
type: .dynamic,
targets: ["PowerSync"]
),
.library(
name: "PowerSyncGRDB",
targets: ["PowerSyncGRDB"]
)
],
dependencies: conditionalDependencies,
dependencies: conditionalDependencies + [
.package(url: "https://github.com/groue/GRDB.swift.git", from: "6.0.0")
],
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.
Expand All @@ -84,9 +90,20 @@ let package = Package(
.product(name: "PowerSyncSQLiteCore", package: corePackageName)
]
),
.target(
name: "PowerSyncGRDB",
dependencies: [
.target(name: "PowerSync"),
.product(name: "GRDB", package: "GRDB.swift")
]
),
.testTarget(
name: "PowerSyncTests",
dependencies: ["PowerSync"]
),
.testTarget(
name: "PowerSyncGRDBTests",
dependencies: ["PowerSync", "PowerSyncGRDB"]
)
] + conditionalTargets
)
44 changes: 35 additions & 9 deletions Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,11 @@ final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol,
let currentStatus: SyncStatus

init(
schema: Schema,
dbFilename: String,
kotlinDatabase: PowerSyncKotlin.PowerSyncDatabase,
logger: DatabaseLogger
) {
let factory = PowerSyncKotlin.DatabaseDriverFactory()
kotlinDatabase = PowerSyncDatabase(
factory: factory,
schema: KotlinAdapter.Schema.toKotlin(schema),
dbFilename: dbFilename,
logger: logger.kLogger
)
self.logger = logger
self.kotlinDatabase = kotlinDatabase
currentStatus = KotlinSyncStatus(
baseStatus: kotlinDatabase.currentStatus
)
Expand Down Expand Up @@ -401,6 +394,39 @@ final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol,
}
}

func openKotlinDBWithFactory(
schema: Schema,
dbFilename: String,
logger: DatabaseLogger
) -> PowerSyncDatabaseProtocol {
return KotlinPowerSyncDatabaseImpl(
kotlinDatabase: PowerSyncDatabase(
factory: PowerSyncKotlin.DatabaseDriverFactory(),
schema: KotlinAdapter.Schema.toKotlin(schema),
dbFilename: dbFilename,
logger: logger.kLogger
),
logger: logger
)
}

func openKotlinDBWithPool(
schema: Schema,
pool: SQLiteConnectionPoolProtocol,
identifier: String,
logger: DatabaseLogger
) -> PowerSyncDatabaseProtocol {
return KotlinPowerSyncDatabaseImpl(
kotlinDatabase: openPowerSyncWithPool(
pool: pool.toKotlin(),
identifier: identifier,
schema: KotlinAdapter.Schema.toKotlin(schema),
logger: logger.kLogger
),
logger: logger
)
}

private struct ExplainQueryResult {
let addr: String
let opcode: String
Expand Down
78 changes: 78 additions & 0 deletions Sources/PowerSync/Kotlin/KotlinSQLiteConnectionPool.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import PowerSyncKotlin

final class SwiftSQLiteConnectionPoolAdapter: PowerSyncKotlin.SwiftPoolAdapter {
let pool: SQLiteConnectionPoolProtocol

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

func __closePool() async throws {
do {
try pool.close()
} catch {
try? PowerSyncKotlin.throwPowerSyncException(
exception: PowerSyncException(
message: error.localizedDescription,
cause: nil
)
)
}
}

func __leaseRead(callback: @escaping (Any) -> Void) async throws {
do {
try await pool.read { pointer in
callback(UInt(bitPattern: pointer))
}
} catch {
try? PowerSyncKotlin.throwPowerSyncException(
exception: PowerSyncException(
message: error.localizedDescription,
cause: nil
)
)
}
}

func __leaseWrite(callback: @escaping (Any) -> Void) async throws {
do {
try await pool.write { pointer in
callback(UInt(bitPattern: pointer))
}
} catch {
try? PowerSyncKotlin.throwPowerSyncException(
exception: PowerSyncException(
message: error.localizedDescription,
cause: nil
)
)
}
}

func __leaseAll(callback: @escaping (Any, [Any]) -> Void) async throws {
// TODO, actually use all connections
do {
try await pool.write { pointer in
callback(UInt(bitPattern: pointer), [])
}
} catch {
try? PowerSyncKotlin.throwPowerSyncException(
exception: PowerSyncException(
message: error.localizedDescription,
cause: nil
)
)
}
}
}

extension SQLiteConnectionPoolProtocol {
func toKotlin() -> PowerSyncKotlin.SwiftSQLiteConnectionPool {
return PowerSyncKotlin.SwiftSQLiteConnectionPool(
adapter: SwiftSQLiteConnectionPoolAdapter(pool: self)
)
}
}
17 changes: 15 additions & 2 deletions Sources/PowerSync/PowerSyncDatabase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,23 @@ public func PowerSyncDatabase(
dbFilename: String = DEFAULT_DB_FILENAME,
logger: (any LoggerProtocol) = DefaultLogger()
) -> PowerSyncDatabaseProtocol {

return KotlinPowerSyncDatabaseImpl(
return openKotlinDBWithFactory(
schema: schema,
dbFilename: dbFilename,
logger: DatabaseLogger(logger)
)
}

public func OpenedPowerSyncDatabase(
schema: Schema,
pool: any SQLiteConnectionPoolProtocol,
identifier: String,
logger: (any LoggerProtocol) = DefaultLogger()
) -> PowerSyncDatabaseProtocol {
return openKotlinDBWithPool(
schema: schema,
pool: pool,
identifier: identifier,
logger: DatabaseLogger(logger)
)
}
26 changes: 26 additions & 0 deletions Sources/PowerSync/Protocol/SQLiteConnectionPool.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Foundation

/// 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 {
/// Calls the callback with a read-only connection temporarily leased from the pool.
func read(
onConnection: @Sendable @escaping (OpaquePointer) -> Void,
) async throws

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

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

/// Closes the connection pool and associated resources.
func close() throws
}
90 changes: 90 additions & 0 deletions Sources/PowerSyncGRDB/GRDBPool.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import Foundation
import GRDB
import PowerSync
import SQLite3

// The system SQLite does not expose this,
// linking PowerSync provides them
// Declare the missing function manually
@_silgen_name("sqlite3_enable_load_extension")
func sqlite3_enable_load_extension(_ db: OpaquePointer?, _ onoff: Int32) -> Int32

// Similarly for sqlite3_load_extension if needed:
@_silgen_name("sqlite3_load_extension")
func sqlite3_load_extension(_ db: OpaquePointer?, _ fileName: UnsafePointer<Int8>?, _ procName: UnsafePointer<Int8>?, _ errMsg: UnsafeMutablePointer<UnsafeMutablePointer<Int8>?>?) -> Int32

enum PowerSyncGRDBConfigError: Error {
case bundleNotFound
case extensionLoadFailed(String)
case unknownExtensionLoadError
}

func configurePowerSync(_ config: inout Configuration) {
config.prepareDatabase { database in
guard let bundle = Bundle(identifier: "co.powersync.sqlitecore") else {
throw PowerSyncGRDBConfigError.bundleNotFound
}

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

let rc = sqlite3_enable_load_extension(database.sqliteConnection, 1)
if rc != SQLITE_OK {
throw PowerSyncGRDBConfigError.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 PowerSyncGRDBConfigError.extensionLoadFailed(message)
} else {
throw PowerSyncGRDBConfigError.unknownExtensionLoadError
}
}
}
}

class GRDBConnectionPool: SQLiteConnectionPoolProtocol {
let pool: DatabasePool

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

func read(
onConnection: @Sendable @escaping (OpaquePointer) -> Void
) async throws {
try await pool.read { database in
guard let connection = database.sqliteConnection else {
return
}
onConnection(connection)
}
}

func write(
onConnection: @Sendable @escaping (OpaquePointer) -> Void
) async throws {
// Don't start an explicit transaction
try await pool.writeWithoutTransaction { database in
guard let connection = database.sqliteConnection else {
return
}
onConnection(connection)
}
}

func withAllConnections(
onConnection _: @escaping (OpaquePointer, [OpaquePointer]) -> Void
) async throws {
// TODO:
}

func close() throws {
try pool.close()
}
}
Loading
Loading