Skip to content

Commit 68407a0

Browse files
committed
Room support
1 parent 7650661 commit 68407a0

File tree

20 files changed

+609
-48
lines changed

20 files changed

+609
-48
lines changed

build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ plugins {
1919
alias(libs.plugins.keeper) apply false
2020
alias(libs.plugins.kotlin.atomicfu) apply false
2121
alias(libs.plugins.cocoapods) apply false
22+
alias(libs.plugins.ksp) apply false
23+
alias(libs.plugins.androidx.room) apply false
2224
id("org.jetbrains.dokka") version libs.versions.dokkaBase
2325
id("dokka-convention")
2426
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,7 @@ public actual class DatabaseDriverFactory(
2121
public fun BundledSQLiteDriver.addPowerSyncExtension() {
2222
addExtension("libpowersync.so", "sqlite3_powersync_init")
2323
}
24+
25+
public actual fun resolvePowerSyncLoadableExtensionPath(): String? {
26+
return "libpowersync.so"
27+
}

core/src/appleNonWatchOsMain/kotlin/com/powersync/DatabaseDriverFactory.appleNonWatchOs.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,8 @@ public actual class DatabaseDriverFactory {
2121
return db
2222
}
2323
}
24+
25+
@ExperimentalPowerSyncAPI
26+
public actual fun resolvePowerSyncLoadableExtensionPath(): String? {
27+
return powerSyncExtensionPath
28+
}

core/src/commonMain/kotlin/com/powersync/DatabaseDriverFactory.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,19 @@ public expect class DatabaseDriverFactory {
1717
): SQLiteConnection
1818
}
1919

20+
/**
21+
* Resolves a path to the loadable PowerSync core extension library.
22+
*
23+
* This library must be loaded on all databases using the PowerSync SDK. On platforms where the
24+
* extension is linked statically (only watchOS at the moment), this returns `null`.
25+
*
26+
* When using the PowerSync SDK directly, there is no need to invoke this method. It is intended for
27+
* configuring external database connections not managed by PowerSync to work with the PowerSync
28+
* SDK.
29+
*/
30+
@ExperimentalPowerSyncAPI
31+
public expect fun resolvePowerSyncLoadableExtensionPath(): String?
32+
2033
@OptIn(ExperimentalPowerSyncAPI::class)
2134
internal fun openDatabase(
2235
factory: DatabaseDriverFactory,

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,8 @@ public fun BundledSQLiteDriver.addPowerSyncExtension() {
2020
}
2121

2222
private val powersyncExtension: String by lazy { extractLib("powersync") }
23+
24+
@ExperimentalPowerSyncAPI
25+
public actual fun resolvePowerSyncLoadableExtensionPath(): String? {
26+
return powersyncExtension
27+
}

core/src/watchosMain/kotlin/com/powersync/DatabaseDriverFactory.watchos.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,9 @@ private val didLoadExtension by lazy {
3131

3232
true
3333
}
34+
35+
@ExperimentalPowerSyncAPI
36+
public actual fun resolvePowerSyncLoadableExtensionPath(): String? {
37+
didLoadExtension
38+
return null
39+
}

gradle/libs.versions.toml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ java = "17"
1010

1111
# Dependencies
1212
kermit = "2.0.8"
13-
kotlin = "2.2.10"
13+
kotlin = "2.2.10" # Note: When updating, always update the first part of the ksp version too
14+
ksp = "2.2.10-2.0.2"
1415
coroutines = "1.10.2"
1516
kotlinx-datetime = "0.7.1"
17+
serialization = "1.9.0"
1618
kotlinx-io = "0.8.0"
1719
ktor = "3.2.3"
1820
uuid = "0.8.4"
@@ -30,6 +32,7 @@ compose-preview = "1.9.0"
3032
compose-lifecycle = "2.9.2"
3133
androidxSqlite = "2.6.0-rc02"
3234
androidxSplashscreen = "1.0.1"
35+
room = "2.8.0-rc02"
3336

3437
# plugins
3538
android-gradle-plugin = "8.12.1"
@@ -88,6 +91,7 @@ ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "kto
8891
ktor-client-contentnegotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
8992
ktor-client-mock = { module = "io.ktor:ktor-client-mock", version.ref = "ktor" }
9093
ktor-serialization-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
94+
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" }
9195
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
9296
kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "coroutines" }
9397

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

101107
# Sample - Android
102108
androidx-core = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core" }
@@ -143,3 +149,5 @@ keeper = { id = "com.slack.keeper", version.ref = "keeper" }
143149
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
144150
kotlin-atomicfu = { id = "org.jetbrains.kotlinx.atomicfu", version.ref = "atomicfu" }
145151
buildKonfig = { id = "com.codingfeline.buildkonfig", version.ref = "buildKonfig" }
152+
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
153+
androidx-room = { id = "androidx.room", version.ref = "room" }

integrations/room/README.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# PowerSync Room integration
2+
3+
This module provides the ability to use PowerSync with Room databases. This module aims for complete
4+
Room support, meaning that:
5+
6+
1. Changes synced from PowerSync automatically update your Room `Flow`s.
7+
2. Room and PowerSync cooperate on the write connection, avoiding "database is locked errors".
8+
3. Changes from Room trigger a CRUD upload.
9+
10+
## Setup
11+
12+
PowerSync can use an existing Room database, provided that the PowerSync core SQLite extension has
13+
been loaded. To do that:
14+
15+
1. Add a dependency on `androidx.sqlite:sqlite-bundled`. Using the SQLite version from the Android
16+
framework will not work as it doesn't support loading extensions.
17+
2. On your `RoomDatabase.Builder`, call `setDriver()` with a PowerSync-enabled driver:
18+
```Kotlin
19+
val driver = BundledSQLiteDriver().also {
20+
it.loadPowerSyncExtension() // Extension method by this module
21+
}
22+
23+
Room.databaseBuilder(...).setDriver(driver).build()
24+
```
25+
3. Configure raw tables for your Room databases.
26+
27+
After these steps, you can open your Room database like you normally would. Then, you can use the
28+
following method to obtain a `PowerSyncDatabase` instance that is backed by Room:
29+
30+
```Kotlin
31+
val pool = RoomConnectionPool(yourRoomDatabase)
32+
val powersync = PowerSyncDatabase.opened(
33+
pool = pool,
34+
scope = this,
35+
schema = Schema(...), // With Room, you need to use raw tables
36+
identifier = "databaseName", // Prefer to use the same path/name as your Room database
37+
logger = Logger,
38+
)
39+
40+
powersync.connect(...)
41+
```
42+
43+
Changes from PowerSync (regardless of whether they've been made with `powersync.execute` or from a
44+
sync operation) will automatically trigger updates in Room.
45+
46+
To also transfer local writes to PowerSync, you need to
47+
48+
1. Create triggers on your Room tables to insert into `ps_crud` (see the PowerSync documentation on
49+
raw tables for details).
50+
2. Listen for Room changes and invoke a helper method to transfer them to PowerSync:
51+
```Kotlin
52+
yourRoomDatabase.getCoroutineScope().launch {
53+
// list all your tables here
54+
yourRoomDatabase.invalidationTracker.createFlow("users", "groups", /*...*/).collect {
55+
pool.transferRoomUpdatesToPowerSync()
56+
}
57+
}
58+
```

integrations/room/build.gradle.kts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import com.powersync.plugins.sonatype.setupGithubRepository
2+
import com.powersync.plugins.utils.powersyncTargets
3+
4+
plugins {
5+
alias(libs.plugins.kotlinMultiplatform)
6+
alias(libs.plugins.android.library)
7+
alias(libs.plugins.kotlinter)
8+
alias(libs.plugins.ksp)
9+
alias(libs.plugins.kotlinSerialization)
10+
id("com.powersync.plugins.sonatype")
11+
id("dokka-convention")
12+
id("com.powersync.plugins.sharedbuild")
13+
}
14+
15+
kotlin {
16+
powersyncTargets()
17+
18+
explicitApi()
19+
20+
sourceSets {
21+
all {
22+
languageSettings {
23+
optIn("com.powersync.ExperimentalPowerSyncAPI")
24+
}
25+
}
26+
27+
commonMain.dependencies {
28+
api(project(":core"))
29+
api(libs.androidx.room.runtime)
30+
api(libs.androidx.sqlite.bundled)
31+
32+
implementation(libs.kotlinx.serialization.json)
33+
}
34+
35+
commonTest.dependencies {
36+
implementation(libs.kotlin.test)
37+
implementation(libs.kotlinx.io)
38+
implementation(libs.test.kotest.assertions)
39+
implementation(libs.test.coroutines)
40+
implementation(libs.test.turbine)
41+
42+
implementation(libs.androidx.sqlite.bundled)
43+
}
44+
}
45+
}
46+
47+
dependencies {
48+
// We use a room database for testing, so we apply the symbol processor on the test target.
49+
val targets = listOf(
50+
"jvm",
51+
"macosArm64",
52+
"macosX64",
53+
"iosSimulatorArm64",
54+
"iosX64",
55+
"tvosSimulatorArm64",
56+
"tvosX64",
57+
"watchosSimulatorArm64",
58+
"watchosX64"
59+
)
60+
61+
targets.forEach { target ->
62+
val capitalized = target.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }
63+
64+
add("ksp${capitalized}Test", libs.androidx.room.compiler)
65+
}
66+
}
67+
68+
android {
69+
namespace = "com.powersync.compose"
70+
compileSdk =
71+
libs.versions.android.compileSdk
72+
.get()
73+
.toInt()
74+
defaultConfig {
75+
minSdk =
76+
libs.versions.android.minSdk
77+
.get()
78+
.toInt()
79+
}
80+
kotlin {
81+
jvmToolchain(17)
82+
}
83+
}
84+
85+
setupGithubRepository()
86+
87+
dokka {
88+
moduleName.set("PowerSync Room Integration")
89+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.powersync.integrations.room
2+
3+
import androidx.room.RoomDatabase
4+
5+
actual fun createDatabaseBuilder(): RoomDatabase.Builder<TestDatabase> {
6+
TODO("Android unit tests are unsupported, we test on JVM instead")
7+
}

0 commit comments

Comments
 (0)