Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* Remove internal SQLDelight and SQLiter dependencies.
* Add `rawConnection` getter to `ConnectionContext`, which is a `SQLiteConnection` instance from
`androidx.sqlite` that can be used to step through statements in a custom way.
* Add an integration for the Room database library ([readme](integrations/room/README.md)).

## 1.5.1

Expand Down
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ plugins {
alias(libs.plugins.keeper) apply false
alias(libs.plugins.kotlin.atomicfu) apply false
alias(libs.plugins.cocoapods) apply false
alias(libs.plugins.ksp) apply false
alias(libs.plugins.androidx.room) apply false
id("org.jetbrains.dokka") version libs.versions.dokkaBase
id("dokka-convention")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,6 @@ public actual class DatabaseDriverFactory(
public fun BundledSQLiteDriver.addPowerSyncExtension() {
addExtension("libpowersync.so", "sqlite3_powersync_init")
}

@ExperimentalPowerSyncAPI
public actual fun resolvePowerSyncLoadableExtensionPath(): String? = "libpowersync.so"
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,6 @@ public actual class DatabaseDriverFactory {
return db
}
}

@ExperimentalPowerSyncAPI
public actual fun resolvePowerSyncLoadableExtensionPath(): String? = powerSyncExtensionPath
13 changes: 13 additions & 0 deletions core/src/commonMain/kotlin/com/powersync/DatabaseDriverFactory.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,19 @@ public expect class DatabaseDriverFactory {
): SQLiteConnection
}

/**
* Resolves a path to the loadable PowerSync core extension library.
*
* This library must be loaded on all databases using the PowerSync SDK. On platforms where the
* extension is linked statically (only watchOS at the moment), this returns `null`.
*
* When using the PowerSync SDK directly, there is no need to invoke this method. It is intended for
* configuring external database connections not managed by PowerSync to work with the PowerSync
* SDK.
*/
@ExperimentalPowerSyncAPI
public expect fun resolvePowerSyncLoadableExtensionPath(): String?

@OptIn(ExperimentalPowerSyncAPI::class)
internal fun openDatabase(
factory: DatabaseDriverFactory,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,6 @@ public fun BundledSQLiteDriver.addPowerSyncExtension() {
}

private val powersyncExtension: String by lazy { extractLib("powersync") }

@ExperimentalPowerSyncAPI
public actual fun resolvePowerSyncLoadableExtensionPath(): String? = powersyncExtension
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,9 @@ private val didLoadExtension by lazy {

true
}

@ExperimentalPowerSyncAPI
public actual fun resolvePowerSyncLoadableExtensionPath(): String? {
didLoadExtension
return null
}
10 changes: 9 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ java = "17"

# Dependencies
kermit = "2.0.8"
kotlin = "2.2.10"
kotlin = "2.2.10" # Note: When updating, always update the first part of the ksp version too
ksp = "2.2.10-2.0.2"
coroutines = "1.10.2"
kotlinx-datetime = "0.7.1"
serialization = "1.9.0"
kotlinx-io = "0.8.0"
ktor = "3.2.3"
uuid = "0.8.4"
Expand All @@ -30,6 +32,7 @@ compose-preview = "1.9.0"
compose-lifecycle = "2.9.2"
androidxSqlite = "2.6.0-rc02"
androidxSplashscreen = "1.0.1"
room = "2.8.0-rc02"

# plugins
android-gradle-plugin = "8.12.1"
Expand Down Expand Up @@ -88,6 +91,7 @@ ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "kto
ktor-client-contentnegotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
ktor-client-mock = { module = "io.ktor:ktor-client-mock", version.ref = "ktor" }
ktor-serialization-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "coroutines" }

Expand All @@ -97,6 +101,8 @@ supabase-auth = { module = "io.github.jan-tennert.supabase:auth-kt", version.ref
supabase-storage = { module = "io.github.jan-tennert.supabase:storage-kt", version.ref = "supabase" }
androidx-sqlite-sqlite = { module = "androidx.sqlite:sqlite", version.ref = "androidxSqlite" }
androidx-sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version.ref = "androidxSqlite" }
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }

# Sample - Android
androidx-core = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core" }
Expand Down Expand Up @@ -143,3 +149,5 @@ keeper = { id = "com.slack.keeper", version.ref = "keeper" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-atomicfu = { id = "org.jetbrains.kotlinx.atomicfu", version.ref = "atomicfu" }
buildKonfig = { id = "com.codingfeline.buildkonfig", version.ref = "buildKonfig" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
androidx-room = { id = "androidx.room", version.ref = "room" }
62 changes: 62 additions & 0 deletions integrations/room/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# PowerSync Room integration

This module provides the ability to use PowerSync with Room databases. This module aims for complete
Room support, meaning that:

1. Changes synced from PowerSync automatically update your Room `Flow`s.
2. Room and PowerSync cooperate on the write connection, avoiding "database is locked errors".
3. Changes from Room trigger a CRUD upload.

## Setup

Add a dependency on `com.powersync:integration-room` with the same version you use for the main
PowerSync SDK.

PowerSync can use an existing Room database, provided that the PowerSync core SQLite extension has
been loaded. To do that:

1. Add a dependency on `androidx.sqlite:sqlite-bundled`. Using the SQLite version from the Android
framework will not work as it doesn't support loading extensions.
2. On your `RoomDatabase.Builder`, call `setDriver()` with a PowerSync-enabled driver:
```Kotlin
val driver = BundledSQLiteDriver().also {
it.loadPowerSyncExtension() // Extension method by this module
}

Room.databaseBuilder(...).setDriver(driver).build()
```
3. Configure raw tables for your Room databases.

After these steps, you can open your Room database like you normally would. Then, you can use the
following method to obtain a `PowerSyncDatabase` instance that is backed by Room:

```Kotlin
val pool = RoomConnectionPool(yourRoomDatabase)
val powersync = PowerSyncDatabase.opened(
pool = pool,
scope = this,
schema = Schema(...), // With Room, you need to use raw tables
identifier = "databaseName", // Prefer to use the same path/name as your Room database
logger = Logger,
)

powersync.connect(...)
```

Changes from PowerSync (regardless of whether they've been made with `powersync.execute` or from a
sync operation) will automatically trigger updates in Room.

To also transfer local writes to PowerSync, you need to

1. Create triggers on your Room tables to insert into `ps_crud` (see the
[PowerSync documentation on raw tables](https://docs.powersync.com/usage/use-case-examples/raw-tables#capture-local-writes-with-triggers)
for details).
2. Listen for Room changes and invoke a helper method to transfer them to PowerSync:
```Kotlin
yourRoomDatabase.getCoroutineScope().launch {
// list all your tables here
yourRoomDatabase.invalidationTracker.createFlow("users", "groups", /*...*/).collect {
pool.transferRoomUpdatesToPowerSync()
}
}
```
111 changes: 111 additions & 0 deletions integrations/room/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import com.powersync.plugins.sonatype.setupGithubRepository
import com.powersync.plugins.utils.powersyncTargets
import org.jmailen.gradle.kotlinter.tasks.FormatTask
import org.jmailen.gradle.kotlinter.tasks.LintTask

plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.android.library)
alias(libs.plugins.kotlinter)
alias(libs.plugins.ksp)
alias(libs.plugins.kotlinSerialization)
id("com.powersync.plugins.sonatype")
id("dokka-convention")
id("com.powersync.plugins.sharedbuild")
}

kotlin {
powersyncTargets()

explicitApi()

sourceSets {
all {
languageSettings {
optIn("com.powersync.ExperimentalPowerSyncAPI")
}
}

commonMain.dependencies {
api(project(":core"))
api(libs.androidx.room.runtime)
api(libs.androidx.sqlite.bundled)

implementation(libs.kotlinx.serialization.json)
}

commonTest.dependencies {
implementation(libs.kotlin.test)
implementation(libs.kotlinx.io)
implementation(libs.test.kotest.assertions)
implementation(libs.test.coroutines)
implementation(libs.test.turbine)

implementation(libs.androidx.sqlite.bundled)
}

val commonIntegrationTest by creating {
dependsOn(commonTest.get())
}

// We're putting the native libraries into our JAR, so integration tests for the JVM can run as part of the unit
// tests.
jvmTest.get().dependsOn(commonIntegrationTest)

// We have special setup in this build configuration to make these tests link the PowerSync extension, so they
// can run integration tests along with the executable for unit testing.
appleTest.orNull?.dependsOn(commonIntegrationTest)
}
}

dependencies {
// We use a room database for testing, so we apply the symbol processor on the test target.
val targets = listOf(
"jvm",
"macosArm64",
"macosX64",
"iosSimulatorArm64",
"iosX64",
"tvosSimulatorArm64",
"tvosX64",
"watchosSimulatorArm64",
"watchosX64"
)

targets.forEach { target ->
val capitalized = target.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }

add("ksp${capitalized}Test", libs.androidx.room.compiler)
}
}

android {
namespace = "com.powersync.compose"
compileSdk =
libs.versions.android.compileSdk
.get()
.toInt()
defaultConfig {
minSdk =
libs.versions.android.minSdk
.get()
.toInt()
}
kotlin {
jvmToolchain(17)
}
}

setupGithubRepository()

dokka {
moduleName.set("PowerSync Room Integration")
}

tasks.withType<LintTask> {
exclude { it.file.path.contains("build/generated") }
}

tasks.withType<FormatTask> {
exclude { it.file.path.contains("build/generated") }
}
3 changes: 3 additions & 0 deletions integrations/room/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
POM_ARTIFACT_ID=integration-room
POM_NAME=Room integration for PowerSync
POM_DESCRIPTION=Use PowerSync to sync data from Room databases.
Loading