Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
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