Skip to content

Commit 03796e7

Browse files
committed
Add native sqlite driver
1 parent aa4e7bc commit 03796e7

File tree

12 files changed

+466
-38
lines changed

12 files changed

+466
-38
lines changed

core/build.gradle.kts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,12 @@ kotlin {
118118
headers(file("src/watchosMain/powersync_static.h"))
119119
}
120120
}
121+
122+
cinterops.create("sqlite3") {
123+
packageName("com.powersync.internal.sqlite3")
124+
includeDirs.allHeaders("src/nativeMain/interop/")
125+
definitionFile.set(project.file("src/nativeMain/interop/sqlite3.def"))
126+
}
121127
}
122128
}
123129

@@ -158,27 +164,35 @@ kotlin {
158164
implementation(libs.kotlinx.datetime)
159165
implementation(libs.stately.concurrency)
160166
implementation(libs.configuration.annotations)
161-
implementation(libs.androidx.sqlite.bundled)
162167
api(libs.ktor.client.core)
163168
api(libs.kermit)
164169
}
165170
}
166171

167172
androidMain {
168173
dependsOn(commonJava)
169-
dependencies.implementation(libs.ktor.client.okhttp)
174+
dependencies {
175+
implementation(libs.ktor.client.okhttp)
176+
implementation(libs.androidx.sqlite.bundled)
177+
}
170178
}
171179

172180
jvmMain {
173181
dependsOn(commonJava)
174182

175183
dependencies {
176184
implementation(libs.ktor.client.okhttp)
185+
implementation(libs.androidx.sqlite.bundled)
177186
}
178187
}
179188

180189
appleMain.dependencies {
181190
implementation(libs.ktor.client.darwin)
191+
192+
// We're not using the bundled SQLite library for Apple platforms. Instead, we depend on
193+
// static-sqlite-driver to link SQLite and have our own bindings implementing the
194+
// driver. The reason for this is that androidx.sqlite-bundled causes linker errors for
195+
// our Swift SDK.
182196
}
183197

184198
// Common apple targets where we link the core extension dynamically
Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
11
package com.powersync
22

33
import android.content.Context
4+
import androidx.sqlite.SQLiteConnection
45
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
56

67
@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
78
public actual class DatabaseDriverFactory(
89
private val context: Context,
910
) {
11+
private val driver = BundledSQLiteDriver().also { it.addPowerSyncExtension() }
12+
1013
internal actual fun resolveDefaultDatabasePath(dbFilename: String): String = context.getDatabasePath(dbFilename).path
14+
15+
internal actual fun openConnection(path: String, openFlags: Int): SQLiteConnection {
16+
return driver.open(path, openFlags)
17+
}
1118
}
1219

13-
public actual fun BundledSQLiteDriver.addPowerSyncExtension() {
20+
public fun BundledSQLiteDriver.addPowerSyncExtension() {
1421
addExtension("libpowersync.so", "sqlite3_powersync_init")
1522
}
Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
package com.powersync
22

3-
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
4-
import platform.Foundation.NSBundle
5-
import kotlin.getValue
3+
import androidx.sqlite.SQLiteConnection
4+
import com.powersync.sqlite.Database
5+
import com.powersync.sqlite.SqliteException
6+
67

78
@Suppress(names = ["EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING"])
89
public actual class DatabaseDriverFactory {
910
internal actual fun resolveDefaultDatabasePath(dbFilename: String): String = appleDefaultDatabasePath(dbFilename)
10-
}
1111

12-
public actual fun BundledSQLiteDriver.addPowerSyncExtension() {
13-
addExtension(powerSyncExtensionPath, "sqlite3_powersync_init")
12+
internal actual fun openConnection(path: String, openFlags: Int): SQLiteConnection {
13+
val db = Database.open(path, openFlags)
14+
try {
15+
db.loadExtension(powerSyncExtensionPath, "sqlite3_powersync_init")
16+
} catch (e: SqliteException) {
17+
db.close()
18+
throw e
19+
}
20+
return db
21+
}
1422
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.powersync.sqlite
2+
3+
import androidx.sqlite.SQLiteConnection
4+
import androidx.sqlite.execSQL
5+
import io.kotest.matchers.shouldBe
6+
import kotlin.test.Test
7+
8+
class DatabaseTest {
9+
@Test
10+
fun testInTransaction() = inMemoryDatabase().use {
11+
it.inTransaction() shouldBe false
12+
it.execSQL("BEGIN")
13+
it.inTransaction() shouldBe true
14+
it.execSQL("COMMIT")
15+
it.inTransaction() shouldBe false
16+
17+
Unit
18+
}
19+
20+
private companion object {
21+
private fun inMemoryDatabase(): SQLiteConnection {
22+
return Database.open(":memory", 0)
23+
}
24+
}
25+
}
Lines changed: 17 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,19 @@
11
package com.powersync
22

33
import androidx.sqlite.SQLiteConnection
4-
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
5-
import androidx.sqlite.driver.bundled.SQLITE_OPEN_CREATE
6-
import androidx.sqlite.driver.bundled.SQLITE_OPEN_READONLY
7-
import androidx.sqlite.driver.bundled.SQLITE_OPEN_READWRITE
4+
85

96
@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
107
public expect class DatabaseDriverFactory {
118
internal fun resolveDefaultDatabasePath(dbFilename: String): String
12-
}
139

14-
/**
15-
* Registers the PowerSync core extension on connections opened by this [BundledSQLiteDriver].
16-
*
17-
* This method will be invoked by the PowerSync SDK when creating new databases. When using
18-
* [PowerSyncDatabase.opened] with an existing connection pool, you should configure the driver
19-
* backing that pool to load the extension.
20-
*/
21-
@ExperimentalPowerSyncAPI()
22-
public expect fun BundledSQLiteDriver.addPowerSyncExtension()
10+
/**
11+
* Opens a SQLite connection on [path] with [openFlags].
12+
*
13+
* The connection should have the PowerSync core extension loaded.
14+
*/
15+
internal fun openConnection(path: String, openFlags: Int): SQLiteConnection
16+
}
2317

2418
@OptIn(ExperimentalPowerSyncAPI::class)
2519
internal fun openDatabase(
@@ -28,21 +22,20 @@ internal fun openDatabase(
2822
dbDirectory: String?,
2923
readOnly: Boolean = false,
3024
): SQLiteConnection {
31-
val driver = BundledSQLiteDriver()
3225
val dbPath =
3326
if (dbDirectory != null) {
3427
"$dbDirectory/$dbFilename"
3528
} else {
3629
factory.resolveDefaultDatabasePath(dbFilename)
3730
}
3831

39-
driver.addPowerSyncExtension()
40-
return driver.open(
41-
dbPath,
42-
if (readOnly) {
43-
SQLITE_OPEN_READONLY
44-
} else {
45-
SQLITE_OPEN_READWRITE or SQLITE_OPEN_CREATE
46-
},
47-
)
32+
return factory.openConnection(dbPath, if (readOnly) {
33+
SQLITE_OPEN_READONLY
34+
} else {
35+
SQLITE_OPEN_READWRITE or SQLITE_OPEN_CREATE
36+
},)
4837
}
38+
39+
private const val SQLITE_OPEN_READONLY = 0x01
40+
private const val SQLITE_OPEN_READWRITE = 0x02
41+
private const val SQLITE_OPEN_CREATE = 0x04

core/src/jvmMain/kotlin/com/powersync/DatabaseDriverFactory.jvm.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
package com.powersync
22

3+
import androidx.sqlite.SQLiteConnection
34
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
45

56
@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING", "SqlNoDataSourceInspection")
67
public actual class DatabaseDriverFactory {
8+
private val driver = BundledSQLiteDriver().also { it.addPowerSyncExtension() }
9+
710
internal actual fun resolveDefaultDatabasePath(dbFilename: String): String = dbFilename
11+
12+
internal actual fun openConnection(path: String, openFlags: Int): SQLiteConnection {
13+
return driver.open(path, openFlags)
14+
}
815
}
916

10-
public actual fun BundledSQLiteDriver.addPowerSyncExtension() {
17+
public fun BundledSQLiteDriver.addPowerSyncExtension() {
1118
addExtension(powersyncExtension, "sqlite3_powersync_init")
1219
}
1320

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
headers = sqlite3.h
2+
3+
noStringConversion = sqlite3_prepare_v3
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// A subset of sqlite3.h that only includes the symbols this Kotlin package needs.
2+
#include <stdint.h>
3+
4+
typedef struct sqlite3 sqlite3;
5+
typedef struct sqlite3_stmt sqlite3_stmt;
6+
7+
int sqlite3_initialize();
8+
9+
int sqlite3_open_v2(char *filename, sqlite3 **ppDb, int flags,
10+
char *zVfs);
11+
int sqlite3_close_v2(sqlite3 *db);
12+
13+
// Error handling
14+
int sqlite3_extended_result_codes(sqlite3 *db, int onoff);
15+
int sqlite3_extended_errcode(sqlite3 *db);
16+
char *sqlite3_errmsg(sqlite3 *db);
17+
char *sqlite3_errstr(int code);
18+
int sqlite3_error_offset(sqlite3 *db);
19+
void sqlite3_free(void *ptr);
20+
21+
// Versions
22+
char *sqlite3_libversion();
23+
char *sqlite3_sourceid();
24+
int sqlite3_libversion_number();
25+
26+
// Database
27+
int sqlite3_get_autocommit(sqlite3 *db);
28+
int sqlite3_db_config(sqlite3 *db, int op, ...);
29+
int sqlite3_load_extension(
30+
sqlite3 *db, /* Load the extension into this database connection */
31+
const char *zFile, /* Name of the shared library containing extension */
32+
const char *zProc, /* Entry point. Derived from zFile if 0 */
33+
char **pzErrMsg /* Put error message here if not 0 */
34+
);
35+
36+
// Statements
37+
int sqlite3_prepare_v3(sqlite3 *db, const char *zSql, int nByte,
38+
unsigned int prepFlags, sqlite3_stmt **ppStmt,
39+
const char **pzTail);
40+
int sqlite3_finalize(sqlite3_stmt *pStmt);
41+
int sqlite3_step(sqlite3_stmt *pStmt);
42+
int sqlite3_reset(sqlite3_stmt *pStmt);
43+
int sqlite3_clear_bindings(sqlite3_stmt*);
44+
45+
int sqlite3_column_count(sqlite3_stmt *pStmt);
46+
int sqlite3_bind_parameter_count(sqlite3_stmt *pStmt);
47+
char *sqlite3_column_name(sqlite3_stmt *pStmt, int N);
48+
49+
int sqlite3_bind_blob64(sqlite3_stmt *pStmt, int index, void *data,
50+
uint64_t length, void *destructor);
51+
int sqlite3_bind_double(sqlite3_stmt *pStmt, int index, double data);
52+
int sqlite3_bind_int64(sqlite3_stmt *pStmt, int index, int64_t data);
53+
int sqlite3_bind_null(sqlite3_stmt *pStmt, int index);
54+
int sqlite3_bind_text16(sqlite3_stmt *pStmt, int index, char *data,
55+
int length, void *destructor);
56+
57+
void *sqlite3_column_blob(sqlite3_stmt *pStmt, int iCol);
58+
double sqlite3_column_double(sqlite3_stmt *pStmt, int iCol);
59+
int64_t sqlite3_column_int64(sqlite3_stmt *pStmt, int iCol);
60+
char *sqlite3_column_text(sqlite3_stmt *pStmt, int iCol);
61+
int sqlite3_column_bytes(sqlite3_stmt *pStmt, int iCol);
62+
int sqlite3_column_bytes16(sqlite3_stmt *pStmt, int iCol);
63+
int sqlite3_column_type(sqlite3_stmt *pStmt, int iCol);
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package com.powersync.sqlite
2+
3+
import androidx.sqlite.SQLiteConnection
4+
import androidx.sqlite.SQLiteStatement
5+
import cnames.structs.sqlite3
6+
import cnames.structs.sqlite3_stmt
7+
import com.powersync.PowerSyncException
8+
import com.powersync.internal.sqlite3.sqlite3_close_v2
9+
import com.powersync.internal.sqlite3.sqlite3_get_autocommit
10+
import com.powersync.internal.sqlite3.sqlite3_initialize
11+
import com.powersync.internal.sqlite3.sqlite3_open_v2
12+
import com.powersync.internal.sqlite3.sqlite3_prepare_v3
13+
import com.powersync.internal.sqlite3.sqlite3_db_config
14+
import com.powersync.internal.sqlite3.sqlite3_free
15+
import com.powersync.internal.sqlite3.sqlite3_load_extension
16+
import kotlinx.cinterop.ByteVar
17+
import kotlinx.cinterop.CPointer
18+
import kotlinx.cinterop.CPointerVar
19+
import kotlinx.cinterop.alloc
20+
import kotlinx.cinterop.allocPointerTo
21+
import kotlinx.cinterop.cstr
22+
import kotlinx.cinterop.memScoped
23+
import kotlinx.cinterop.ptr
24+
import kotlinx.cinterop.reinterpret
25+
import kotlinx.cinterop.toKStringFromUtf8
26+
import kotlinx.cinterop.utf16
27+
import kotlinx.cinterop.value
28+
29+
internal class Database(private val ptr: CPointer<sqlite3>): SQLiteConnection {
30+
override fun inTransaction(): Boolean {
31+
// We're in a transaction if autocommit is disabled
32+
return sqlite3_get_autocommit(ptr) == 0
33+
}
34+
35+
override fun prepare(sql: String): SQLiteStatement = memScoped {
36+
val stmtPtr = allocPointerTo<sqlite3_stmt>()
37+
val asUtf16 = sql.utf16
38+
sqlite3_prepare_v3(ptr, asUtf16.ptr.reinterpret(), asUtf16.size, 0u, stmtPtr.ptr, null).checkResult()
39+
40+
Statement(sql, ptr, stmtPtr.value!!)
41+
}
42+
43+
fun loadExtension(filename: String, entrypoint: String) = memScoped {
44+
val errorMessagePointer = alloc<CPointerVar<ByteVar>>()
45+
val resultCode = sqlite3_load_extension(ptr, filename, entrypoint, errorMessagePointer.ptr)
46+
47+
if (resultCode != 0) {
48+
val errorMessage = errorMessagePointer.value?.toKStringFromUtf8()
49+
if (errorMessage != null) {
50+
sqlite3_free(errorMessagePointer.value)
51+
}
52+
53+
throw SqliteException(resultCode, errorMessage ?: "unknown error")
54+
}
55+
}
56+
57+
override fun close() {
58+
sqlite3_close_v2(ptr)
59+
}
60+
61+
private fun Int.checkResult() {
62+
if (this != 0) {
63+
throw PowerSyncException("SQLite error", SqliteException.createExceptionInDatabase(this, ptr))
64+
}
65+
}
66+
67+
companion object {
68+
fun open(path: String, flags: Int): Database = memScoped {
69+
var rc = sqlite3_initialize()
70+
if (rc != 0) {
71+
throw SqliteException.createExceptionOutsideOfDatabase(rc)
72+
}
73+
74+
val encodedPath = path.cstr.getPointer(this)
75+
val ptr = allocPointerTo<sqlite3>()
76+
rc = sqlite3_open_v2(encodedPath, ptr.ptr, flags, null)
77+
if (rc != 0) {
78+
throw SqliteException.createExceptionOutsideOfDatabase(rc)
79+
}
80+
81+
val db = ptr.value!!
82+
// Enable extensions via the C API
83+
sqlite3_db_config(db, DBCONFIG_ENABLE_LOAD_EXTENSION, 1, 0)
84+
85+
Database(db)
86+
}
87+
88+
private const val DBCONFIG_ENABLE_LOAD_EXTENSION = 1005
89+
}
90+
}

0 commit comments

Comments
 (0)