Skip to content
Draft
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
136e809
New unified driver interfaces
simolus3 Jul 22, 2025
435c7c5
Fix leaking statements
simolus3 Jul 24, 2025
522f4c6
Add raw connection API
simolus3 Jul 25, 2025
2359b48
Add changelog entry
simolus3 Jul 25, 2025
f4d98d5
Lease API that works better with Room
simolus3 Jul 25, 2025
eccb7f7
Notify updates from raw statements
simolus3 Jul 25, 2025
54829bc
Delete more driver stuff
simolus3 Aug 22, 2025
f532ba9
Fix deadlock in initialization
simolus3 Aug 22, 2025
c7adbab
Make addPowerSyncExtension public
simolus3 Aug 22, 2025
011b1da
Actually, use callbacks
simolus3 Aug 22, 2025
c87e079
merge main
simolus3 Sep 2, 2025
cb1373d
Add docs
simolus3 Sep 2, 2025
aa4e7bc
Fix lints
simolus3 Sep 2, 2025
03796e7
Add native sqlite driver
simolus3 Sep 5, 2025
a0682c2
Bring back static sqlite linking
simolus3 Sep 5, 2025
8f5f8cd
Fix linter errors
simolus3 Sep 5, 2025
f41b0a4
Fix Swift tests
simolus3 Sep 5, 2025
51521d8
Delete proguard rules
simolus3 Sep 5, 2025
fd04adc
grdb drivers
stevensJourney Sep 7, 2025
b32f7bf
wip: lease all connections
stevensJourney Sep 7, 2025
9008648
revert databasegroup changes.
stevensJourney Sep 8, 2025
d6697d9
Merge branch 'main' into grdb-drivers
stevensJourney Sep 16, 2025
a92930a
update after merging
stevensJourney Sep 16, 2025
ef4160c
revert test change
stevensJourney Sep 16, 2025
068d8ed
Merge remote-tracking branch 'origin/main' into grdb-drivers
stevensJourney Sep 19, 2025
c0bdde9
improve error handling
stevensJourney Sep 23, 2025
937d452
Use SQLite Session API for Swift updates.
stevensJourney Sep 25, 2025
59408b0
Code cleanup. Fix lint error.
stevensJourney Sep 25, 2025
07267c1
Merge remote-tracking branch 'origin/main' into grdb-drivers
stevensJourney Sep 25, 2025
587934c
cleanup APIs for sessions
stevensJourney Sep 29, 2025
5434a5f
Merge remote-tracking branch 'origin/main' into grdb-drivers
stevensJourney Oct 3, 2025
9f855e4
move Swift pool logic
stevensJourney Oct 3, 2025
11add5c
Add changelog entry
stevensJourney Oct 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package com.powersync

import androidx.sqlite.SQLiteStatement
import cnames.structs.sqlite3
import co.touchlab.kermit.Logger
import com.powersync.db.driver.SQLiteConnectionLease
import com.powersync.db.driver.SQLiteConnectionPool
import com.powersync.db.schema.Schema
import com.powersync.sqlite.Database
import io.ktor.utils.io.CancellationException
import kotlinx.cinterop.CPointer
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.runBlocking

@OptIn(ExperimentalPowerSyncAPI::class)
internal class RawConnectionLease
@OptIn(ExperimentalForeignApi::class)
constructor(
connectionPointer: CPointer<sqlite3>,
) : SQLiteConnectionLease {
private var isCompleted = false

@OptIn(ExperimentalForeignApi::class)
private var db = Database(connectionPointer)

private fun checkNotCompleted() {
check(!isCompleted) { "Connection lease already closed" }
}

override suspend fun isInTransaction(): Boolean = isInTransactionSync()

override fun isInTransactionSync(): Boolean {
checkNotCompleted()
return db.inTransaction()
}

override suspend fun <R> usePrepared(
sql: String,
block: (SQLiteStatement) -> R,
): R = usePreparedSync(sql, block)

override fun <R> usePreparedSync(
sql: String,
block: (SQLiteStatement) -> R,
): R {
checkNotCompleted()
return db.prepare(sql).use(block)
}
}

/**
* We only allow synchronous callbacks on the Swift side for leased READ/WRITE connections.
* We also get a SQLite connection pointer (sqlite3*) from Swift side. which is used in a [Database]
*/

public interface SwiftPoolAdapter {
@OptIn(ExperimentalForeignApi::class)
@Throws(PowerSyncException::class, CancellationException::class)
public suspend fun leaseRead(callback: (CPointer<sqlite3>) -> Unit)

@OptIn(ExperimentalForeignApi::class)
@Throws(PowerSyncException::class, CancellationException::class)
public suspend fun leaseWrite(callback: (CPointer<sqlite3>) -> Unit)

@OptIn(ExperimentalForeignApi::class)
@Throws(PowerSyncException::class, CancellationException::class)
public suspend fun leaseAll(callback: (CPointer<sqlite3>, List<CPointer<sqlite3>>) -> Unit)

public suspend fun closePool()
}

@OptIn(ExperimentalPowerSyncAPI::class)
public open class SwiftSQLiteConnectionPool
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since it looks like we wouldn't extend this class from Swift, I don't think it would have to be open?

@OptIn(ExperimentalForeignApi::class)
constructor(
private val adapter: SwiftPoolAdapter,
) : SQLiteConnectionPool {
private val _updates = MutableSharedFlow<Set<String>>(replay = 0)
override val updates: SharedFlow<Set<String>> get() = _updates

public fun pushUpdate(update: Set<String>) {
_updates.tryEmit(update)
}

@OptIn(ExperimentalForeignApi::class)
override suspend fun <T> read(callback: suspend (SQLiteConnectionLease) -> T): T {
var result: T? = null
adapter.leaseRead {
/**
* For GRDB, this should be running inside the callback
* ```swift
* db.write {
* // should be here
* }
* ```
*/
val lease = RawConnectionLease(it)
runBlocking {
result = callback(lease)
}
}
return result as T
}

@OptIn(ExperimentalForeignApi::class)
override suspend fun <T> write(callback: suspend (SQLiteConnectionLease) -> T): T {
var result: T? = null
adapter.leaseWrite {
val lease = RawConnectionLease(it)
runBlocking {
result = callback(lease)
}
}
return result as T
}

@OptIn(ExperimentalForeignApi::class)
override suspend fun <R> withAllConnections(action: suspend (SQLiteConnectionLease, List<SQLiteConnectionLease>) -> R) {
adapter.leaseAll { writerPtr, readerPtrs ->
runBlocking {
action(RawConnectionLease(writerPtr), readerPtrs.map { RawConnectionLease(it) })
}
}
}

override suspend fun close() {
adapter.closePool()
}
}

@OptIn(ExperimentalPowerSyncAPI::class, DelicateCoroutinesApi::class)
public fun openPowerSyncWithPool(
pool: SQLiteConnectionPool,
identifier: String,
schema: Schema,
logger: Logger,
): PowerSyncDatabase =
PowerSyncDatabase.opened(
pool = pool,
scope = GlobalScope,
schema = schema,
identifier = identifier,
logger = logger,
)
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ internal class ActiveDatabaseGroup(
}
}

internal class ActiveDatabaseResource(
internal class ActiveDatabaseResource constructor(
val group: ActiveDatabaseGroup,
) {
val disposed = AtomicBoolean(false)
Expand Down
32 changes: 18 additions & 14 deletions core/src/nativeMain/kotlin/com/powersync/sqlite/Database.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import kotlinx.cinterop.value
* [com.powersync.db.driver.InternalConnectionPool] and called from [kotlinx.coroutines.Dispatchers.IO]
* to make these APIs asynchronous.
*/
internal class Database(
public class Database(
private val ptr: CPointer<sqlite3>,
) : SQLiteConnection {
override fun inTransaction(): Boolean {
Expand All @@ -52,22 +52,26 @@ internal class Database(
Statement(sql, ptr, stmtPtr.value!!)
}

fun loadExtension(
public fun loadExtension(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the intention is to call this method from Swift, should it have a @Throws annotation?

filename: String,
entrypoint: String,
) = memScoped {
val errorMessagePointer = alloc<CPointerVar<ByteVar>>()
val resultCode = sqlite3_load_extension(ptr, filename, entrypoint, errorMessagePointer.ptr)

if (resultCode != 0) {
val errorMessage = errorMessagePointer.value?.toKStringFromUtf8()
if (errorMessage != null) {
sqlite3_free(errorMessagePointer.value)
}
): Unit =
memScoped {
val errorMessagePointer = alloc<CPointerVar<ByteVar>>()
val resultCode = sqlite3_load_extension(ptr, filename, entrypoint, errorMessagePointer.ptr)

if (resultCode != 0) {
val errorMessage = errorMessagePointer.value?.toKStringFromUtf8()
if (errorMessage != null) {
sqlite3_free(errorMessagePointer.value)
}

throw PowerSyncException("Could not load extension ($resultCode): ${errorMessage ?: "unknown error"}", null)
throw PowerSyncException(
"Could not load extension ($resultCode): ${errorMessage ?: "unknown error"}",
null,
)
}
}
}

override fun close() {
sqlite3_close_v2(ptr)
Expand All @@ -79,7 +83,7 @@ internal class Database(
}
}

companion object {
internal companion object {
fun open(
path: String,
flags: Int,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
package com.powersync.compile

import kotlin.io.path.Path
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.provider.Provider
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.provider.ProviderFactory
import org.gradle.api.tasks.CacheableTask
import org.gradle.api.tasks.Input
Expand All @@ -18,11 +17,12 @@ import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction
import org.jetbrains.kotlin.konan.target.KonanTarget
import javax.inject.Inject
import kotlin.io.path.Path
import kotlin.io.path.absolutePathString
import kotlin.io.path.name

@CacheableTask
abstract class ClangCompile: DefaultTask() {
abstract class ClangCompile : DefaultTask() {
@get:InputFile
@get:PathSensitive(PathSensitivity.NONE)
abstract val inputFile: RegularFileProperty
Expand All @@ -41,10 +41,13 @@ abstract class ClangCompile: DefaultTask() {
protected abstract val providers: ProviderFactory

@get:Input
val xcodeInstallation: Provider<String> get() = providers.exec {
executable("xcode-select")
args("-p")
}.standardOutput.asText
val xcodeInstallation: Provider<String>
get() =
providers
.exec {
executable("xcode-select")
args("-p")
}.standardOutput.asText

@TaskAction
fun run() {
Expand All @@ -55,60 +58,68 @@ abstract class ClangCompile: DefaultTask() {
}

val xcode = Path(xcodePath)
val toolchain = xcode.resolve("Toolchains/XcodeDefault.xctoolchain/usr/bin").absolutePathString()
val toolchain =
xcode.resolve("Toolchains/XcodeDefault.xctoolchain/usr/bin").absolutePathString()

val (llvmTarget, sysRoot) = when (target) {
KonanTarget.IOS_X64 -> "x86_64-apple-ios12.0-simulator" to IOS_SIMULATOR_SDK
KonanTarget.IOS_ARM64 -> "arm64-apple-ios12.0" to IOS_SDK
KonanTarget.IOS_SIMULATOR_ARM64 -> "arm64-apple-ios14.0-simulator" to IOS_SIMULATOR_SDK
KonanTarget.MACOS_ARM64 -> "aarch64-apple-macos" to MACOS_SDK
KonanTarget.MACOS_X64 -> "x86_64-apple-macos" to MACOS_SDK
KonanTarget.WATCHOS_DEVICE_ARM64 -> "aarch64-apple-watchos" to WATCHOS_SDK
KonanTarget.WATCHOS_ARM32 -> "armv7k-apple-watchos" to WATCHOS_SDK
KonanTarget.WATCHOS_ARM64 -> "arm64_32-apple-watchos" to WATCHOS_SDK
KonanTarget.WATCHOS_SIMULATOR_ARM64 -> "aarch64-apple-watchos-simulator" to WATCHOS_SIMULATOR_SDK
KonanTarget.WATCHOS_X64 -> "x86_64-apple-watchos-simulator" to WATCHOS_SIMULATOR_SDK
KonanTarget.TVOS_ARM64 -> "aarch64-apple-tvos" to TVOS_SDK
KonanTarget.TVOS_X64 -> "x86_64-apple-tvos-simulator" to TVOS_SIMULATOR_SDK
KonanTarget.TVOS_SIMULATOR_ARM64 -> "aarch64-apple-tvos-simulator" to TVOS_SIMULATOR_SDK
else -> error("Unexpected target $target")
}
val (llvmTarget, sysRoot) =
when (target) {
KonanTarget.IOS_X64 -> "x86_64-apple-ios12.0-simulator" to IOS_SIMULATOR_SDK
KonanTarget.IOS_ARM64 -> "arm64-apple-ios12.0" to IOS_SDK
KonanTarget.IOS_SIMULATOR_ARM64 -> "arm64-apple-ios14.0-simulator" to IOS_SIMULATOR_SDK
KonanTarget.MACOS_ARM64 -> "aarch64-apple-macos" to MACOS_SDK
KonanTarget.MACOS_X64 -> "x86_64-apple-macos" to MACOS_SDK
KonanTarget.WATCHOS_DEVICE_ARM64 -> "aarch64-apple-watchos" to WATCHOS_SDK
KonanTarget.WATCHOS_ARM32 -> "armv7k-apple-watchos" to WATCHOS_SDK
KonanTarget.WATCHOS_ARM64 -> "arm64_32-apple-watchos" to WATCHOS_SDK
KonanTarget.WATCHOS_SIMULATOR_ARM64 -> "aarch64-apple-watchos-simulator" to WATCHOS_SIMULATOR_SDK
KonanTarget.WATCHOS_X64 -> "x86_64-apple-watchos-simulator" to WATCHOS_SIMULATOR_SDK
KonanTarget.TVOS_ARM64 -> "aarch64-apple-tvos" to TVOS_SDK
KonanTarget.TVOS_X64 -> "x86_64-apple-tvos-simulator" to TVOS_SIMULATOR_SDK
KonanTarget.TVOS_SIMULATOR_ARM64 -> "aarch64-apple-tvos-simulator" to TVOS_SIMULATOR_SDK
else -> error("Unexpected target $target")
}

val output = objectFile.get()

providers.exec {
executable = "clang"
args(
"-B${toolchain}",
"-fno-stack-protector",
"-target",
llvmTarget,
"-isysroot",
xcode.resolve(sysRoot).absolutePathString(),
"-fPIC",
"--compile",
"-I${include.get().asFile.absolutePath}",
inputFile.get().asFile.absolutePath,
"-DHAVE_GETHOSTUUID=0",
"-DSQLITE_ENABLE_DBSTAT_VTAB",
"-DSQLITE_ENABLE_FTS5",
"-DSQLITE_ENABLE_RTREE",
"-O3",
"-o",
output.asFile.toPath().name,
)
providers
.exec {
executable = "clang"
args(
"-B$toolchain",
"-fno-stack-protector",
"-target",
llvmTarget,
"-isysroot",
xcode.resolve(sysRoot).absolutePathString(),
"-fPIC",
"--compile",
"-I${include.get().asFile.absolutePath}",
inputFile.get().asFile.absolutePath,
"-DHAVE_GETHOSTUUID=0",
"-DSQLITE_ENABLE_DBSTAT_VTAB",
"-DSQLITE_ENABLE_FTS5",
"-DSQLITE_ENABLE_RTREE",
"-DSQLITE_ENABLE_SNAPSHOT",
"-O3",
"-o",
output.asFile.toPath().name,
)

workingDir = output.asFile.parentFile
}.result.get()
workingDir = output.asFile.parentFile
}.result
.get()
}

companion object {
const val WATCHOS_SDK = "Platforms/WatchOS.platform/Developer/SDKs/WatchOS.sdk"
const val WATCHOS_SIMULATOR_SDK = "Platforms/WatchSimulator.platform/Developer/SDKs/WatchSimulator.sdk/"
const val WATCHOS_SIMULATOR_SDK =
"Platforms/WatchSimulator.platform/Developer/SDKs/WatchSimulator.sdk/"
const val IOS_SDK = "Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk"
const val IOS_SIMULATOR_SDK = "Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk"
const val IOS_SIMULATOR_SDK =
"Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk"
const val TVOS_SDK = "Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS.sdk"
const val TVOS_SIMULATOR_SDK = "Platforms/AppleTVSimulator.platform/Developer/SDKs/AppleTVSimulator.sdk"
const val TVOS_SIMULATOR_SDK =
"Platforms/AppleTVSimulator.platform/Developer/SDKs/AppleTVSimulator.sdk"
const val MACOS_SDK = "Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/"
}
}
Loading