Skip to content

Commit 245e3d2

Browse files
wip: grdb connection pool
1 parent 174282d commit 245e3d2

File tree

12 files changed

+374
-75
lines changed

12 files changed

+374
-75
lines changed

Package.resolved

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ let packageName = "PowerSync"
77

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

1212
// Set this to the absolute path of your powersync-sqlite-core checkout if you want to use a
1313
// local build of the core extension.
@@ -71,9 +71,15 @@ let package = Package(
7171
// Dynamic linking is particularly important for XCode previews.
7272
type: .dynamic,
7373
targets: ["PowerSync"]
74+
),
75+
.library(
76+
name: "PowerSyncGRDB",
77+
targets: ["PowerSyncGRDB"]
7478
)
7579
],
76-
dependencies: conditionalDependencies,
80+
dependencies: conditionalDependencies + [
81+
.package(url: "https://github.com/groue/GRDB.swift.git", from: "6.0.0")
82+
],
7783
targets: [
7884
// Targets are the basic building blocks of a package, defining a module or a test suite.
7985
// Targets can depend on other targets in this package and products from dependencies.
@@ -84,9 +90,20 @@ let package = Package(
8490
.product(name: "PowerSyncSQLiteCore", package: corePackageName)
8591
]
8692
),
93+
.target(
94+
name: "PowerSyncGRDB",
95+
dependencies: [
96+
.target(name: "PowerSync"),
97+
.product(name: "GRDB", package: "GRDB.swift")
98+
]
99+
),
87100
.testTarget(
88101
name: "PowerSyncTests",
89102
dependencies: ["PowerSync"]
103+
),
104+
.testTarget(
105+
name: "PowerSyncGRDBTests",
106+
dependencies: ["PowerSync", "PowerSyncGRDB"]
90107
)
91108
] + conditionalTargets
92109
)

Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,11 @@ final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol,
1111
let currentStatus: SyncStatus
1212

1313
init(
14-
schema: Schema,
15-
dbFilename: String,
14+
kotlinDatabase: PowerSyncKotlin.PowerSyncDatabase,
1615
logger: DatabaseLogger
1716
) {
18-
let factory = PowerSyncKotlin.DatabaseDriverFactory()
19-
kotlinDatabase = PowerSyncDatabase(
20-
factory: factory,
21-
schema: KotlinAdapter.Schema.toKotlin(schema),
22-
dbFilename: dbFilename,
23-
logger: logger.kLogger
24-
)
2517
self.logger = logger
18+
self.kotlinDatabase = kotlinDatabase
2619
currentStatus = KotlinSyncStatus(
2720
baseStatus: kotlinDatabase.currentStatus
2821
)
@@ -401,6 +394,39 @@ final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol,
401394
}
402395
}
403396

397+
func openKotlinDBWithFactory(
398+
schema: Schema,
399+
dbFilename: String,
400+
logger: DatabaseLogger
401+
) -> PowerSyncDatabaseProtocol {
402+
return KotlinPowerSyncDatabaseImpl(
403+
kotlinDatabase: PowerSyncDatabase(
404+
factory: PowerSyncKotlin.DatabaseDriverFactory(),
405+
schema: KotlinAdapter.Schema.toKotlin(schema),
406+
dbFilename: dbFilename,
407+
logger: logger.kLogger
408+
),
409+
logger: logger
410+
)
411+
}
412+
413+
func openKotlinDBWithPool(
414+
schema: Schema,
415+
pool: SQLiteConnectionPoolProtocol,
416+
identifier: String,
417+
logger: DatabaseLogger
418+
) -> PowerSyncDatabaseProtocol {
419+
return KotlinPowerSyncDatabaseImpl(
420+
kotlinDatabase: openPowerSyncWithPool(
421+
pool: pool.toKotlin(),
422+
identifier: identifier,
423+
schema: KotlinAdapter.Schema.toKotlin(schema),
424+
logger: logger.kLogger
425+
),
426+
logger: logger
427+
)
428+
}
429+
404430
private struct ExplainQueryResult {
405431
let addr: String
406432
let opcode: String
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import PowerSyncKotlin
2+
3+
final class SwiftSQLiteConnectionPoolAdapter: PowerSyncKotlin.SwiftPoolAdapter {
4+
let pool: SQLiteConnectionPoolProtocol
5+
6+
init(
7+
pool: SQLiteConnectionPoolProtocol
8+
) {
9+
self.pool = pool
10+
}
11+
12+
func __closePool() async throws {
13+
do {
14+
try pool.close()
15+
} catch {
16+
try? PowerSyncKotlin.throwPowerSyncException(
17+
exception: PowerSyncException(
18+
message: error.localizedDescription,
19+
cause: nil
20+
)
21+
)
22+
}
23+
}
24+
25+
func __leaseRead(callback: @escaping (Any) -> Void) async throws {
26+
do {
27+
try await pool.read { pointer in
28+
callback(pointer)
29+
}
30+
} catch {
31+
try? PowerSyncKotlin.throwPowerSyncException(
32+
exception: PowerSyncException(
33+
message: error.localizedDescription,
34+
cause: nil
35+
)
36+
)
37+
}
38+
}
39+
40+
func __leaseWrite(callback: @escaping (Any) -> Void) async throws {
41+
do {
42+
try await pool.write { pointer in
43+
callback(pointer)
44+
}
45+
} catch {
46+
try? PowerSyncKotlin.throwPowerSyncException(
47+
exception: PowerSyncException(
48+
message: error.localizedDescription,
49+
cause: nil
50+
)
51+
)
52+
}
53+
}
54+
}
55+
56+
extension SQLiteConnectionPoolProtocol {
57+
func toKotlin() -> PowerSyncKotlin.SwiftSQLiteConnectionPool {
58+
return PowerSyncKotlin.SwiftSQLiteConnectionPool(
59+
adapter: SwiftSQLiteConnectionPoolAdapter(pool: self)
60+
)
61+
}
62+
}

Sources/PowerSync/PowerSyncDatabase.swift

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,23 @@ public func PowerSyncDatabase(
1414
dbFilename: String = DEFAULT_DB_FILENAME,
1515
logger: (any LoggerProtocol) = DefaultLogger()
1616
) -> PowerSyncDatabaseProtocol {
17-
18-
return KotlinPowerSyncDatabaseImpl(
17+
return openKotlinDBWithFactory(
1918
schema: schema,
2019
dbFilename: dbFilename,
2120
logger: DatabaseLogger(logger)
2221
)
2322
}
23+
24+
public func OpenedPowerSyncDatabase(
25+
schema: Schema,
26+
pool: any SQLiteConnectionPoolProtocol,
27+
identifier: String,
28+
logger: (any LoggerProtocol) = DefaultLogger()
29+
) -> PowerSyncDatabaseProtocol {
30+
return openKotlinDBWithPool(
31+
schema: schema,
32+
pool: pool,
33+
identifier: identifier,
34+
logger: DatabaseLogger(logger)
35+
)
36+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import Foundation
2+
3+
/// An implementation of a connection pool providing asynchronous access to a single writer and multiple readers.
4+
/// This is the underlying pool implementation on which the higher-level PowerSync Swift SDK is built on.
5+
public protocol SQLiteConnectionPoolProtocol {
6+
/// Calls the callback with a read-only connection temporarily leased from the pool.
7+
func read(
8+
onConnection: @Sendable @escaping (OpaquePointer) -> Void,
9+
) async throws
10+
11+
/// Calls the callback with a read-write connection temporarily leased from the pool.
12+
func write(
13+
onConnection: @Sendable @escaping (OpaquePointer) -> Void,
14+
) async throws
15+
16+
/// Invokes the callback with all connections leased from the pool.
17+
func withAllConnections(
18+
onConnection: @escaping (
19+
_ writer: OpaquePointer,
20+
_ readers: [OpaquePointer]
21+
) -> Void,
22+
) async throws
23+
24+
/// Closes the connection pool and associated resources.
25+
func close() throws
26+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import Foundation
2+
import GRDB
3+
import PowerSync
4+
import SQLite3
5+
6+
// The system SQLite does not expose this,
7+
// linking PowerSync provides them
8+
// Declare the missing function manually
9+
@_silgen_name("sqlite3_enable_load_extension")
10+
func sqlite3_enable_load_extension(_ db: OpaquePointer?, _ onoff: Int32) -> Int32
11+
12+
// Similarly for sqlite3_load_extension if needed:
13+
@_silgen_name("sqlite3_load_extension")
14+
func sqlite3_load_extension(_ db: OpaquePointer?, _ fileName: UnsafePointer<Int8>?, _ procName: UnsafePointer<Int8>?, _ errMsg: UnsafeMutablePointer<UnsafeMutablePointer<Int8>?>?) -> Int32
15+
16+
enum PowerSyncGRDBConfigError: Error {
17+
case bundleNotFound
18+
case extensionLoadFailed(String)
19+
case unknownExtensionLoadError
20+
}
21+
22+
func configurePowerSync(_ config: inout Configuration) {
23+
config.prepareDatabase { database in
24+
guard let bundle = Bundle(identifier: "co.powersync.sqlitecore") else {
25+
throw PowerSyncGRDBConfigError.bundleNotFound
26+
}
27+
28+
// Construct the full path to the shared library inside the bundle
29+
let fullPath = bundle.bundlePath + "/powersync-sqlite-core"
30+
31+
let rc = sqlite3_enable_load_extension(database.sqliteConnection, 1)
32+
if rc != SQLITE_OK {
33+
throw PowerSyncGRDBConfigError.extensionLoadFailed("Could not enable extension loading")
34+
}
35+
var errorMsg: UnsafeMutablePointer<Int8>?
36+
let loadResult = sqlite3_load_extension(database.sqliteConnection, fullPath, "sqlite3_powersync_init", &errorMsg)
37+
if loadResult != SQLITE_OK {
38+
if let errorMsg = errorMsg {
39+
let message = String(cString: errorMsg)
40+
sqlite3_free(errorMsg)
41+
throw PowerSyncGRDBConfigError.extensionLoadFailed(message)
42+
} else {
43+
throw PowerSyncGRDBConfigError.unknownExtensionLoadError
44+
}
45+
}
46+
}
47+
}
48+
49+
class GRDBConnectionPool: SQLiteConnectionPoolProtocol {
50+
let pool: DatabasePool
51+
52+
init(
53+
pool: DatabasePool
54+
) {
55+
self.pool = pool
56+
}
57+
58+
func read(
59+
onConnection: @Sendable @escaping (OpaquePointer) -> Void
60+
) async throws {
61+
try await pool.read { database in
62+
guard let connection = database.sqliteConnection else {
63+
return
64+
}
65+
onConnection(connection)
66+
}
67+
}
68+
69+
func write(
70+
onConnection: @Sendable @escaping (OpaquePointer) -> Void
71+
) async throws {
72+
try await pool.write { database in
73+
guard let connection = database.sqliteConnection else {
74+
return
75+
}
76+
onConnection(connection)
77+
}
78+
}
79+
80+
func withAllConnections(
81+
onConnection _: @escaping (OpaquePointer, [OpaquePointer]) -> Void
82+
) async throws {
83+
// TODO:
84+
}
85+
86+
func close() throws {
87+
try pool.close()
88+
}
89+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
@testable import GRDB
2+
@testable import PowerSync
3+
@testable import PowerSyncGRDB
4+
5+
import XCTest
6+
7+
final class GRDBTests: XCTestCase {
8+
private var database: PowerSyncDatabaseProtocol!
9+
private var schema: Schema!
10+
11+
override func setUp() async throws {
12+
try await super.setUp()
13+
schema = Schema(tables: [
14+
Table(name: "users", columns: [
15+
.text("count"),
16+
.integer("is_active"),
17+
.real("weight"),
18+
.text("description")
19+
])
20+
])
21+
22+
var config = Configuration()
23+
configurePowerSync(&config)
24+
25+
let documentsDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
26+
let dbURL = documentsDir.appendingPathComponent("test.sqlite")
27+
let pool = try DatabasePool(
28+
path: dbURL.path,
29+
configuration: config
30+
)
31+
32+
database = OpenedPowerSyncDatabase(
33+
schema: schema,
34+
pool: GRDBConnectionPool(
35+
pool: pool
36+
),
37+
identifier: "test"
38+
)
39+
40+
try await database.disconnectAndClear()
41+
}
42+
43+
override func tearDown() async throws {
44+
try await database.disconnectAndClear()
45+
database = nil
46+
try await super.tearDown()
47+
}
48+
49+
func testValidValues() async throws {
50+
let result = try await database.get(
51+
"SELECT powersync_rs_version as r"
52+
) { cursor in
53+
try cursor.getString(index: 0)
54+
}
55+
print(result)
56+
}
57+
}

Tests/PowerSyncTests/ConnectTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ final class ConnectTests: XCTestCase {
1818
),
1919
])
2020

21-
database = KotlinPowerSyncDatabaseImpl(
21+
database = openKotlinDBWithFactory(
2222
schema: schema,
2323
dbFilename: ":memory:",
2424
logger: DatabaseLogger(DefaultLogger())

0 commit comments

Comments
 (0)