11package com.powersync
22
3- import app.cash.sqldelight.db.QueryResult
4- import co.touchlab.sqliter.DatabaseConfiguration
5- import co.touchlab.sqliter.DatabaseConfiguration.Logging
6- import co.touchlab.sqliter.DatabaseConnection
7- import co.touchlab.sqliter.NO_VERSION_CHECK
8- import co.touchlab.sqliter.interop.Logger
9- import co.touchlab.sqliter.interop.SqliteErrorType
10- import co.touchlab.sqliter.sqlite3.sqlite3_commit_hook
11- import co.touchlab.sqliter.sqlite3.sqlite3_enable_load_extension
12- import co.touchlab.sqliter.sqlite3.sqlite3_load_extension
13- import co.touchlab.sqliter.sqlite3.sqlite3_rollback_hook
14- import co.touchlab.sqliter.sqlite3.sqlite3_update_hook
3+ import androidx.sqlite.SQLiteConnection
154import com.powersync.DatabaseDriverFactory.Companion.powerSyncExtensionPath
16- import com.powersync.db. internal.InternalSchema
17- import com.powersync.persistence .driver.NativeSqliteDriver
18- import com.powersync.persistence .driver.wrapConnection
5+ import com.powersync.internal.driver.ConnectionListener
6+ import com.powersync.internal .driver.NativeConnection
7+ import com.powersync.internal .driver.NativeDriver
198import kotlinx.cinterop.ByteVar
209import kotlinx.cinterop.CPointerVar
2110import kotlinx.cinterop.ExperimentalForeignApi
2211import kotlinx.cinterop.MemScope
23- import kotlinx.cinterop.StableRef
12+ import kotlinx.cinterop.UnsafeNumber
2413import kotlinx.cinterop.alloc
25- import kotlinx.cinterop.asStableRef
2614import kotlinx.cinterop.free
2715import kotlinx.cinterop.nativeHeap
2816import kotlinx.cinterop.ptr
29- import kotlinx.cinterop.staticCFunction
3017import kotlinx.cinterop.toKString
3118import kotlinx.cinterop.value
32- import kotlinx.coroutines.CoroutineScope
19+ import kotlinx.io.files.Path
20+ import platform.Foundation.NSApplicationSupportDirectory
3321import platform.Foundation.NSBundle
22+ import platform.Foundation.NSFileManager
23+ import platform.Foundation.NSSearchPathForDirectoriesInDomains
24+ import platform.Foundation.NSUserDomainMask
25+ import sqlite3.SQLITE_OK
26+ import sqlite3.sqlite3_enable_load_extension
27+ import sqlite3.sqlite3_load_extension
3428
3529@Suppress(" EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING" )
3630@OptIn(ExperimentalForeignApi ::class )
3731public actual class DatabaseDriverFactory {
38- internal actual fun createDriver (
39- scope : CoroutineScope ,
32+ internal actual fun openDatabase (
4033 dbFilename : String ,
4134 dbDirectory : String? ,
4235 readOnly : Boolean ,
43- ): PsSqlDriver {
44- val schema = InternalSchema
45- val sqlLogger =
46- object : Logger {
47- override val eActive: Boolean
48- get() = false
49- override val vActive: Boolean
50- get() = false
51-
52- override fun eWrite (
53- message : String ,
54- exception : Throwable ? ,
55- ) {
56- }
57-
58- override fun trace (message : String ) {}
59-
60- override fun vWrite (message : String ) {}
61- }
62-
63- // Create a deferred driver reference for hook registrations
64- // This must exist before we create the driver since we require
65- // a pointer for C hooks
66- val deferredDriver = DeferredDriver ()
67-
68- val driver =
69- PsSqlDriver (
70- driver =
71- NativeSqliteDriver (
72- configuration =
73- DatabaseConfiguration (
74- name = dbFilename,
75- version =
76- if (! readOnly) {
77- schema.version.toInt()
78- } else {
79- // Don't do migrations on read only connections
80- NO_VERSION_CHECK
81- },
82- create = { connection ->
83- wrapConnection(connection) {
84- schema.create(
85- it,
86- )
87- }
88- },
89- loggingConfig = Logging (logger = sqlLogger),
90- lifecycleConfig =
91- DatabaseConfiguration .Lifecycle (
92- onCreateConnection = { connection ->
93- setupSqliteBinding(connection, deferredDriver)
94- wrapConnection(connection) { driver ->
95- schema.create(driver)
96- }
97- },
98- onCloseConnection = { connection ->
99- deregisterSqliteBinding(connection)
100- },
101- ),
102- ),
103- ),
104- )
105-
106- // The iOS driver implementation generates 1 write and 1 read connection internally
107- // It uses the read connection for all queries and the write connection for all
108- // execute statements. Unfortunately the driver does not seem to respond to query
109- // calls if the read connection count is set to zero.
110- // We'd like to ensure a driver is set to read-only. Ideally we could do this in the
111- // onCreateConnection lifecycle hook, but this runs before driver internal migrations.
112- // Setting the connection to read only there breaks migrations.
113- // We explicitly execute this pragma to reflect and guard the "write" connection.
114- // The read connection already has this set.
115- if (readOnly) {
116- driver.execute(" PRAGMA query_only=true" )
117- }
118-
119- // Ensure internal read pool has created a connection at this point. This makes connection
120- // initialization a bit more deterministic.
121- driver.executeQuery(
122- identifier = null ,
123- sql = " SELECT 1" ,
124- mapper = { QueryResult .Value (it.getLong(0 )) },
125- parameters = 0 ,
126- )
127-
128- deferredDriver.setDriver(driver)
129-
130- return driver
131- }
132-
133- private fun setupSqliteBinding (
134- connection : DatabaseConnection ,
135- driver : DeferredDriver ,
136- ) {
137- connection.loadPowerSyncSqliteCoreExtension()
138-
139- val ptr = connection.getDbPointer().getPointer(MemScope ())
140- val driverRef = StableRef .create(driver)
141-
142- sqlite3_update_hook(
143- ptr,
144- staticCFunction { usrPtr, updateType, dbName, tableName, rowId ->
145- usrPtr!!
146- .asStableRef<DeferredDriver >()
147- .get()
148- .updateTableHook(tableName!! .toKString())
149- },
150- driverRef.asCPointer(),
151- )
152-
153- sqlite3_commit_hook(
154- ptr,
155- staticCFunction { usrPtr ->
156- usrPtr!! .asStableRef<DeferredDriver >().get().onTransactionCommit(true )
157- 0
158- },
159- driverRef.asCPointer(),
160- )
161-
162- sqlite3_rollback_hook(
163- ptr,
164- staticCFunction { usrPtr ->
165- usrPtr!! .asStableRef<DeferredDriver >().get().onTransactionCommit(false )
166- },
167- driverRef.asCPointer(),
168- )
169- }
170-
171- private fun deregisterSqliteBinding (connection : DatabaseConnection ) {
172- val basePtr = connection.getDbPointer().getPointer(MemScope ())
173-
174- sqlite3_update_hook(
175- basePtr,
176- null ,
177- null ,
178- )
36+ listener : ConnectionListener ?
37+ ): SQLiteConnection {
38+ val directory = dbDirectory ? : defaultDatabaseDirectory()
39+ val path = Path (directory, dbFilename).toString()
40+ val db = NativeDriver ().openNativeDatabase(path, readOnly, listener)
41+
42+ db.loadPowerSyncSqliteCoreExtension()
43+ return db
17944 }
18045
18146 internal companion object {
@@ -192,18 +57,34 @@ public actual class DatabaseDriverFactory {
19257 // Construct full path to the shared library inside the bundle
19358 bundlePath.let { " $it /powersync-sqlite-core" }
19459 }
60+
61+ @OptIn(UnsafeNumber ::class )
62+ private fun defaultDatabaseDirectory (search : String = "databases"): String {
63+ // This needs to be compatible with https://github.com/touchlab/SQLiter/blob/a37bbe7e9c65e6a5a94c5bfcaccdaae55ad2bac9/sqliter-driver/src/appleMain/kotlin/co/touchlab/sqliter/DatabaseFileContext.kt#L36-L51
64+ val paths = NSSearchPathForDirectoriesInDomains (NSApplicationSupportDirectory , NSUserDomainMask , true );
65+ val documentsDirectory = paths[0 ] as String ;
66+
67+ val databaseDirectory = " $documentsDirectory /$search "
68+
69+ val fileManager = NSFileManager .defaultManager()
70+
71+ if (! fileManager.fileExistsAtPath(databaseDirectory))
72+ fileManager.createDirectoryAtPath(databaseDirectory, true , null , null ); // Create folder
73+
74+ return databaseDirectory
75+ }
19576 }
19677}
19778
198- internal fun DatabaseConnection .loadPowerSyncSqliteCoreExtensionDynamically () {
199- val ptr = getDbPointer() .getPointer(MemScope ())
79+ internal fun NativeConnection .loadPowerSyncSqliteCoreExtensionDynamically () {
80+ val ptr = sqlite .getPointer(MemScope ())
20081 val extensionPath = powerSyncExtensionPath
20182
20283 // Enable extension loading
20384 // We don't disable this after the fact, this should allow users to load their own extensions
20485 // in future.
20586 val enableResult = sqlite3_enable_load_extension(ptr, 1 )
206- if (enableResult != SqliteErrorType . SQLITE_OK .code ) {
87+ if (enableResult != SQLITE_OK ) {
20788 throw PowerSyncException (
20889 " Could not dynamically load the PowerSync SQLite core extension" ,
20990 cause =
@@ -219,7 +100,7 @@ internal fun DatabaseConnection.loadPowerSyncSqliteCoreExtensionDynamically() {
219100 sqlite3_load_extension(ptr, extensionPath, " sqlite3_powersync_init" , errMsg.ptr)
220101 val resultingError = errMsg.value
221102 nativeHeap.free(errMsg)
222- if (result != SqliteErrorType . SQLITE_OK .code ) {
103+ if (result != SQLITE_OK ) {
223104 val errorMessage = resultingError?.toKString() ? : " Unknown error"
224105 throw PowerSyncException (
225106 " Could not load the PowerSync SQLite core extension" ,
@@ -231,4 +112,4 @@ internal fun DatabaseConnection.loadPowerSyncSqliteCoreExtensionDynamically() {
231112 }
232113}
233114
234- internal expect fun DatabaseConnection .loadPowerSyncSqliteCoreExtension ()
115+ internal expect fun NativeConnection .loadPowerSyncSqliteCoreExtension ()
0 commit comments