Skip to content

Commit 27512a8

Browse files
Export resolvePowerSyncLoadableExtensionPath from PowerSync SDK.
Use resolvePowerSyncLoadableExtensionPath for GRDB integration. Fix WatchOS tests.
1 parent b4d9cbf commit 27512a8

File tree

4 files changed

+107
-28
lines changed

4 files changed

+107
-28
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import PowerSyncKotlin
2+
3+
func kotlinResolvePowerSyncLoadableExtensionPath() throws -> String? {
4+
do {
5+
return try PowerSyncKotlin.resolvePowerSyncLoadableExtensionPath()
6+
} catch {
7+
throw PowerSyncError.operationFailed(message: "Failed to resolve PowerSync loadable extension path: \(error.localizedDescription)")
8+
}
9+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/// Resolves the PowerSync SQLite extension path.
2+
///
3+
/// This function returns the file system path to the PowerSync SQLite extension library.
4+
/// For use with extension loading APIs or SQLite queries.
5+
///
6+
/// ## Platform Behavior
7+
///
8+
/// ### watchOS
9+
/// On watchOS, the extension needs to be loaded statically. This function returns `nil`
10+
/// on watchOS because the extension is statically linked and doesn't require a path.
11+
/// Calling this function will auto-register the extension in watchOS. The static
12+
/// initialization ensures the extension is available without requiring dynamic loading.
13+
///
14+
/// ### Other Platforms
15+
/// In other environments (iOS, macOS, tvOS, etc.), the extension needs to be loaded
16+
/// dynamically using the path returned by this function. You'll need to:
17+
/// 1. Enable extension loading on your SQLite connection
18+
/// 2. Load the extension using the returned path
19+
///
20+
/// ## Example Usage
21+
///
22+
/// ### Loading with SQLite API
23+
/// ```swift
24+
/// guard let extensionPath = try resolvePowerSyncLoadableExtensionPath() else {
25+
/// // On watchOS, extension is statically loaded, no path needed
26+
/// return
27+
/// }
28+
///
29+
/// // Enable extension loading
30+
/// sqlite3_enable_load_extension(db, 1)
31+
///
32+
/// // Load the extension
33+
/// var errorMsg: UnsafeMutablePointer<Int8>?
34+
/// let result = sqlite3_load_extension(
35+
/// db,
36+
/// extensionPath,
37+
/// "sqlite3_powersync_init",
38+
/// &errorMsg
39+
/// )
40+
/// if result != SQLITE_OK {
41+
/// // Handle error
42+
/// }
43+
/// ```
44+
///
45+
/// ### Loading with SQL Query
46+
/// ```swift
47+
/// guard let extensionPath = try resolvePowerSyncLoadableExtensionPath() else {
48+
/// // On watchOS, extension is statically loaded, no path needed
49+
/// return
50+
/// }
51+
/// let escapedPath = extensionPath.replacingOccurrences(of: "'", with: "''")
52+
/// let query = "SELECT load_extension('\(escapedPath)', 'sqlite3_powersync_init')"
53+
/// try db.execute(sql: query)
54+
/// ```
55+
///
56+
/// - Returns: The file system path to the PowerSync SQLite extension, or `nil` on watchOS
57+
/// (where the extension is statically loaded and doesn't require a path)
58+
/// - Throws: An error if the extension path cannot be resolved on platforms that require it
59+
public func resolvePowerSyncLoadableExtensionPath() throws -> String? {
60+
return try kotlinResolvePowerSyncLoadableExtensionPath()
61+
}

Sources/PowerSyncGRDB/Config/Configuration+PowerSync.swift

Lines changed: 24 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -25,37 +25,35 @@ public extension Configuration {
2525
) {
2626
// Register the PowerSync core extension
2727
prepareDatabase { database in
28-
#if os(watchOS)
29-
// Use static initialization on watchOS
28+
let initResult = sqlite3_powersync_init(database.sqliteConnection, nil, nil)
29+
if initResult != SQLITE_OK {
30+
throw PowerSyncGRDBError.extensionLoadFailed("Could not initialize PowerSync")
31+
}
32+
guard let extensionPath = try resolvePowerSyncLoadableExtensionPath() else {
33+
// We get the extension path for non WatchOS platforms.
34+
// The Kotlin registration for automatically loading the extension does not seem to work.
35+
// We explicitly initialize the extension here.
3036
let initResult = sqlite3_powersync_init(database.sqliteConnection, nil, nil)
3137
if initResult != SQLITE_OK {
3238
throw PowerSyncGRDBError.extensionLoadFailed("Could not initialize PowerSync statically")
3339
}
34-
#else
35-
// Dynamic loading on other platforms
36-
guard let bundle = Bundle(identifier: "co.powersync.sqlitecore") else {
37-
throw PowerSyncGRDBError.coreBundleNotFound
40+
return
41+
}
42+
let extensionLoadResult = sqlite3_enable_load_extension(database.sqliteConnection, 1)
43+
if extensionLoadResult != SQLITE_OK {
44+
throw PowerSyncGRDBError.extensionLoadFailed("Could not enable extension loading")
45+
}
46+
var errorMsg: UnsafeMutablePointer<Int8>?
47+
let loadResult = sqlite3_load_extension(database.sqliteConnection, extensionPath, "sqlite3_powersync_init", &errorMsg)
48+
if loadResult != SQLITE_OK {
49+
if let errorMsg = errorMsg {
50+
let message = String(cString: errorMsg)
51+
sqlite3_free(errorMsg)
52+
throw PowerSyncGRDBError.extensionLoadFailed(message)
53+
} else {
54+
throw PowerSyncGRDBError.unknownExtensionLoadError
3855
}
39-
40-
// Construct the full path to the shared library inside the bundle
41-
let fullPath = bundle.bundlePath + "/powersync-sqlite-core"
42-
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-
}
57-
}
58-
#endif
56+
}
5957
}
6058

6159
// Supply the PowerSync views as a SchemaSource

Tests/PowerSyncGRDBTests/BasicTest.swift

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,16 @@ final class GRDBTests: XCTestCase {
6363
schema: schema
6464
)
6565

66-
let documentsDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
66+
guard let documentsDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
67+
throw XCTestError(
68+
.failureWhileWaiting,
69+
userInfo: [NSLocalizedDescriptionKey: "Could not access documents directory"]
70+
)
71+
}
72+
73+
// Ensure the documents directory exists
74+
try FileManager.default.createDirectory(at: documentsDir, withIntermediateDirectories: true, attributes: nil)
75+
6776
let dbURL = documentsDir.appendingPathComponent("test.sqlite")
6877
pool = try DatabasePool(
6978
path: dbURL.path,
@@ -80,8 +89,10 @@ final class GRDBTests: XCTestCase {
8089
}
8190

8291
override func tearDown() async throws {
83-
try await database.disconnectAndClear()
92+
try? await database?.disconnectAndClear()
8493
database = nil
94+
try? pool?.close()
95+
pool = nil
8596
try await super.tearDown()
8697
}
8798

0 commit comments

Comments
 (0)