Skip to content

Commit 5a2b131

Browse files
committed
Add raw connection API
1 parent 9884eb0 commit 5a2b131

File tree

13 files changed

+168
-90
lines changed

13 files changed

+168
-90
lines changed

core/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ kotlin {
181181
all {
182182
languageSettings {
183183
optIn("kotlinx.cinterop.ExperimentalForeignApi")
184+
optIn("kotlin.experimental.ExperimentalObjCRefinement")
184185
}
185186
}
186187

@@ -202,6 +203,7 @@ kotlin {
202203

203204
dependencies {
204205
api(libs.kermit)
206+
api(libs.androidx.sqlite)
205207

206208
implementation(libs.uuid)
207209
implementation(libs.kotlin.stdlib)

core/src/androidMain/kotlin/com/powersync/DatabaseDriverFactory.android.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public actual class DatabaseDriverFactory(
1515
dbFilename: String,
1616
dbDirectory: String?,
1717
readOnly: Boolean,
18-
listener: ConnectionListener?
18+
listener: ConnectionListener?,
1919
): SQLiteConnection {
2020
val dbPath =
2121
if (dbDirectory != null) {

core/src/appleMain/kotlin/com/powersync/DatabaseDriverFactory.apple.kt

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public actual class DatabaseDriverFactory {
3333
dbFilename: String,
3434
dbDirectory: String?,
3535
readOnly: Boolean,
36-
listener: ConnectionListener?
36+
listener: ConnectionListener?,
3737
): SQLiteConnection {
3838
val directory = dbDirectory ?: defaultDatabaseDirectory()
3939
val path = Path(directory, dbFilename).toString()
@@ -61,15 +61,16 @@ public actual class DatabaseDriverFactory {
6161
@OptIn(UnsafeNumber::class)
6262
private fun defaultDatabaseDirectory(search: String = "databases"): String {
6363
// 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;
64+
val paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, true)
65+
val documentsDirectory = paths[0] as String
6666

6767
val databaseDirectory = "$documentsDirectory/$search"
6868

6969
val fileManager = NSFileManager.defaultManager()
7070

71-
if (!fileManager.fileExistsAtPath(databaseDirectory))
72-
fileManager.createDirectoryAtPath(databaseDirectory, true, null, null); //Create folder
71+
if (!fileManager.fileExistsAtPath(databaseDirectory)) {
72+
fileManager.createDirectoryAtPath(databaseDirectory, true, null, null)
73+
}; // Create folder
7374

7475
return databaseDirectory
7576
}

core/src/commonIntegrationTest/kotlin/com/powersync/DatabaseTest.kt

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.powersync
22

3+
import androidx.sqlite.SQLiteConnection
4+
import androidx.sqlite.execSQL
35
import app.cash.turbine.turbineScope
46
import co.touchlab.kermit.ExperimentalKermitApi
57
import com.powersync.db.ActiveDatabaseGroup
@@ -13,6 +15,7 @@ import io.kotest.assertions.throwables.shouldThrow
1315
import io.kotest.matchers.collections.shouldHaveSize
1416
import io.kotest.matchers.shouldBe
1517
import io.kotest.matchers.string.shouldContain
18+
import io.kotest.matchers.throwable.shouldHaveMessage
1619
import kotlinx.coroutines.CompletableDeferred
1720
import kotlinx.coroutines.Dispatchers
1821
import kotlinx.coroutines.async
@@ -459,4 +462,38 @@ class DatabaseTest {
459462

460463
database.getCrudBatch() shouldBe null
461464
}
465+
466+
@Test
467+
fun testRawConnection() =
468+
databaseTest {
469+
database.execute(
470+
"INSERT INTO users (id, name, email) VALUES (uuid(), ?, ?)",
471+
listOf("a", "[email protected]"),
472+
)
473+
var capturedConnection: SQLiteConnection? = null
474+
475+
database.readLock {
476+
it.rawConnection.prepare("SELECT * FROM users").use { stmt ->
477+
stmt.step() shouldBe true
478+
stmt.getText(1) shouldBe "a"
479+
stmt.getText(2) shouldBe "[email protected]"
480+
}
481+
482+
capturedConnection = it.rawConnection
483+
}
484+
485+
// When we exit readLock, the connection should no longer be usable
486+
shouldThrow<IllegalStateException> { capturedConnection!!.execSQL("DELETE FROM users") } shouldHaveMessage
487+
"Connection lease already closed"
488+
489+
capturedConnection = null
490+
database.writeLock {
491+
it.rawConnection.execSQL("DELETE FROM users")
492+
capturedConnection = it.rawConnection
493+
}
494+
495+
// Same thing for writes
496+
shouldThrow<IllegalStateException> { capturedConnection!!.prepare("SELECT * FROM users") } shouldHaveMessage
497+
"Connection lease already closed"
498+
}
462499
}

core/src/commonIntegrationTest/kotlin/com/powersync/sync/SyncIntegrationTest.kt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -597,10 +597,12 @@ abstract class BaseSyncIntegrationTest(
597597
val turbine = database.currentStatus.asFlow().testIn(scope)
598598
turbine.waitFor { it.connected }
599599

600-
val query = database.watch("SELECT name FROM users") {
601-
println("interpreting results: ${it.getString(0)}")
602-
it.getString(0)!!
603-
}.testIn(scope)
600+
val query =
601+
database
602+
.watch("SELECT name FROM users") {
603+
println("interpreting results: ${it.getString(0)}")
604+
it.getString(0)!!
605+
}.testIn(scope)
604606
query.awaitItem() shouldBe listOf("local write")
605607

606608
syncLines.send(SyncLine.KeepAlive(tokenExpiresIn = 1234))

core/src/commonMain/kotlin/com/powersync/db/PowerSyncDatabaseImpl.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.powersync.db
22

3+
import androidx.sqlite.SQLiteConnection
34
import co.touchlab.kermit.Logger
45
import com.powersync.DatabaseDriverFactory
56
import com.powersync.PowerSyncDatabase
@@ -46,7 +47,6 @@ import kotlinx.datetime.Instant
4647
import kotlinx.datetime.LocalDateTime
4748
import kotlinx.datetime.TimeZone
4849
import kotlinx.datetime.toInstant
49-
import kotlin.math.log
5050
import kotlin.time.Duration.Companion.milliseconds
5151

5252
/**

core/src/commonMain/kotlin/com/powersync/db/SqlCursor.kt

Lines changed: 25 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -31,62 +31,54 @@ private inline fun <T> SqlCursor.getColumnValue(
3131
return getValue(index) ?: throw IllegalArgumentException("Null value found for column '$name'")
3232
}
3333

34-
internal class StatementBasedCursor(private val stmt: SQLiteStatement): SqlCursor {
35-
override fun getBoolean(index: Int): Boolean? {
36-
return getNullable(index) { index -> stmt.getLong(index) != 0L }
37-
}
34+
internal class StatementBasedCursor(
35+
private val stmt: SQLiteStatement,
36+
) : SqlCursor {
37+
override fun getBoolean(index: Int): Boolean? = getNullable(index) { index -> stmt.getLong(index) != 0L }
3838

39-
override fun getBytes(index: Int): ByteArray? {
40-
return getNullable(index, SQLiteStatement::getBlob)
41-
}
39+
override fun getBytes(index: Int): ByteArray? = getNullable(index, SQLiteStatement::getBlob)
4240

43-
override fun getDouble(index: Int): Double? {
44-
return getNullable(index, SQLiteStatement::getDouble)
45-
}
41+
override fun getDouble(index: Int): Double? = getNullable(index, SQLiteStatement::getDouble)
4642

47-
override fun getLong(index: Int): Long? {
48-
return getNullable(index, SQLiteStatement::getLong)
49-
}
43+
override fun getLong(index: Int): Long? = getNullable(index, SQLiteStatement::getLong)
5044

51-
override fun getString(index: Int): String? {
52-
return getNullable(index, SQLiteStatement::getText)
53-
}
45+
override fun getString(index: Int): String? = getNullable(index, SQLiteStatement::getText)
5446

55-
private inline fun <T> getNullable(index: Int, read: SQLiteStatement.(Int) -> T): T? {
56-
return if (stmt.isNull(index)) {
47+
private inline fun <T> getNullable(
48+
index: Int,
49+
read: SQLiteStatement.(Int) -> T,
50+
): T? =
51+
if (stmt.isNull(index)) {
5752
null
5853
} else {
5954
stmt.read(index)
6055
}
61-
}
6256

63-
override fun columnName(index: Int): String? {
64-
return stmt.getColumnName(index)
65-
}
57+
override fun columnName(index: Int): String? = stmt.getColumnName(index)
6658

6759
override val columnCount: Int
6860
get() = stmt.getColumnCount()
6961

7062
override val columnNames: Map<String, Int> by lazy {
7163
buildMap {
7264
stmt.getColumnNames().forEachIndexed { index, key ->
73-
val finalKey = if (containsKey(key)) {
74-
var index = 1
75-
val basicKey = "$key&JOIN"
76-
var finalKey = basicKey + index
77-
while (containsKey(finalKey)) {
78-
finalKey = basicKey + ++index
65+
val finalKey =
66+
if (containsKey(key)) {
67+
var index = 1
68+
val basicKey = "$key&JOIN"
69+
var finalKey = basicKey + index
70+
while (containsKey(finalKey)) {
71+
finalKey = basicKey + ++index
72+
}
73+
finalKey
74+
} else {
75+
key
7976
}
80-
finalKey
81-
} else {
82-
key
83-
}
8477

8578
put(finalKey, index)
8679
}
8780
}
8881
}
89-
9082
}
9183

9284
private inline fun <T> SqlCursor.getColumnValueOptional(

core/src/commonMain/kotlin/com/powersync/db/internal/ConnectionContext.kt

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,12 @@ import androidx.sqlite.SQLiteStatement
55
import com.powersync.PowerSyncException
66
import com.powersync.db.SqlCursor
77
import com.powersync.db.StatementBasedCursor
8+
import kotlin.native.HiddenFromObjC
89

910
public interface ConnectionContext {
11+
@HiddenFromObjC
12+
public val rawConnection: SQLiteConnection
13+
1014
@Throws(PowerSyncException::class)
1115
public fun execute(
1216
sql: String,
@@ -35,10 +39,12 @@ public interface ConnectionContext {
3539
): RowType
3640
}
3741

38-
internal class ConnectionContextImplementation(val connection: SQLiteConnection): ConnectionContext {
42+
internal class ConnectionContextImplementation(
43+
override val rawConnection: SQLiteConnection,
44+
) : ConnectionContext {
3945
override fun execute(
4046
sql: String,
41-
parameters: List<Any?>?
47+
parameters: List<Any?>?,
4248
): Long {
4349
withStatement(sql, parameters) {
4450
while (it.step()) {
@@ -53,46 +59,47 @@ internal class ConnectionContextImplementation(val connection: SQLiteConnection)
5359
override fun <RowType : Any> getOptional(
5460
sql: String,
5561
parameters: List<Any?>?,
56-
mapper: (SqlCursor) -> RowType
57-
): RowType? {
58-
return withStatement(sql, parameters) { stmt ->
62+
mapper: (SqlCursor) -> RowType,
63+
): RowType? =
64+
withStatement(sql, parameters) { stmt ->
5965
if (stmt.step()) {
6066
mapper(StatementBasedCursor(stmt))
6167
} else {
6268
null
6369
}
6470
}
65-
}
6671

6772
override fun <RowType : Any> getAll(
6873
sql: String,
6974
parameters: List<Any?>?,
70-
mapper: (SqlCursor) -> RowType
71-
): List<RowType> {
72-
return withStatement(sql, parameters) { stmt ->
75+
mapper: (SqlCursor) -> RowType,
76+
): List<RowType> =
77+
withStatement(sql, parameters) { stmt ->
7378
buildList {
7479
val cursor = StatementBasedCursor(stmt)
7580
while (stmt.step()) {
7681
add(mapper(cursor))
7782
}
7883
}
7984
}
80-
}
8185

8286
override fun <RowType : Any> get(
8387
sql: String,
8488
parameters: List<Any?>?,
85-
mapper: (SqlCursor) -> RowType
86-
): RowType {
87-
return getOptional(sql, parameters, mapper) ?: throw PowerSyncException("get() called with query that returned no rows", null)
88-
}
89+
mapper: (SqlCursor) -> RowType,
90+
): RowType = getOptional(sql, parameters, mapper) ?: throw PowerSyncException("get() called with query that returned no rows", null)
8991

90-
private inline fun <T> withStatement(sql: String, parameters: List<Any?>?, block: (SQLiteStatement) -> T): T {
91-
return prepareStmt(sql, parameters).use(block)
92-
}
92+
private inline fun <T> withStatement(
93+
sql: String,
94+
parameters: List<Any?>?,
95+
block: (SQLiteStatement) -> T,
96+
): T = prepareStmt(sql, parameters).use(block)
9397

94-
private fun prepareStmt(sql: String, parameters: List<Any?>?): SQLiteStatement {
95-
return connection.prepare(sql).apply {
98+
private fun prepareStmt(
99+
sql: String,
100+
parameters: List<Any?>?,
101+
): SQLiteStatement =
102+
rawConnection.prepare(sql).apply {
96103
try {
97104
parameters?.forEachIndexed { i, parameter ->
98105
// SQLite parameters are 1-indexed
@@ -117,5 +124,4 @@ internal class ConnectionContextImplementation(val connection: SQLiteConnection)
117124
throw e
118125
}
119126
}
120-
}
121127
}

0 commit comments

Comments
 (0)