Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
- os: windows-latest
targets: jvmTest
runs-on: ${{ matrix.os }}
timeout-minutes: 20
timeout-minutes: 30

steps:
- uses: actions/checkout@v4
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* 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.
* Fix an issue where `watch()` would run queries more often than intended.
* Add an integration for the Room database library ([readme](integrations/room/README.md)).

## 1.5.1

Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ This is the PowerSync client SDK for Kotlin. This SDK currently supports the fol
- iOS
- macOS
- watchOS
- tvOS

If you need support for additional targets, please reach out!

Expand All @@ -32,6 +33,11 @@ and API documentation [here](https://powersync-ja.github.io/powersync-kotlin/).
1. Retrieve a token to connect to the PowerSync service.
2. Apply local changes on your backend application server (and from there, to your backend database).

- [integrations](./integrations/)
- [room](./integrations/room/README.md): Allows using the [Room database library](https://developer.android.com/jetpack/androidx/releases/room)
with PowerSync, making it easier to run typed queries on the database.


## Demo Apps / Example Projects

The easiest way to test the PowerSync KMP SDK is to run one of our demo applications.
Expand All @@ -41,12 +47,6 @@ Demo applications are located in the [`demos/`](./demos) directory. See their re
- [demos/supabase-todolist](./demos/supabase-todolist/README.md): A simple to-do list application demonstrating the use of the PowerSync Kotlin Multiplatform SDK and the Supabase connector.
- [demos/android-supabase-todolist](./demos/android-supabase-todolist/README.md): A simple to-do list application demonstrating the use of the PowerSync Kotlin Multiplatform SDK and the Supabase connector in an Android application.

## Current Limitations / Future work

Current limitations:

- Integration with SQLDelight schema and API generation (ORM) is not yet supported.

## Installation

Add the PowerSync Kotlin Multiplatform SDK to your project by adding the following to your `build.gradle.kts` file:
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 @@ -3,6 +3,7 @@ package com.powersync
import android.content.Context
import androidx.sqlite.SQLiteConnection
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
import kotlin.Throws

@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
public actual class DatabaseDriverFactory(
Expand All @@ -21,3 +22,7 @@ public actual class DatabaseDriverFactory(
public fun BundledSQLiteDriver.addPowerSyncExtension() {
addExtension("libpowersync.so", "sqlite3_powersync_init")
}

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

@ExperimentalPowerSyncAPI
@Throws(PowerSyncException::class)
public actual fun resolvePowerSyncLoadableExtensionPath(): String? = powerSyncExtensionPath
14 changes: 14 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,20 @@ 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
@Throws(PowerSyncException::class)
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 @@ -2,6 +2,7 @@ package com.powersync

import androidx.sqlite.SQLiteConnection
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
import com.powersync.db.runWrapped

@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING", "SqlNoDataSourceInspection")
public actual class DatabaseDriverFactory {
Expand All @@ -20,3 +21,7 @@ public fun BundledSQLiteDriver.addPowerSyncExtension() {
}

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

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

true
}

@ExperimentalPowerSyncAPI
@Throws(PowerSyncException::class)
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" }
59 changes: 59 additions & 0 deletions integrations/room/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# 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 which is backed by Room:

```Kotlin
// With Room, you need to use raw tables (https://docs.powersync.com/usage/use-case-examples/raw-tables).
// This is because Room verifies your schema at runtime, and PowerSync-managed views will not
// pass those checks.
val schema = Schema(...)
val pool = RoomConnectionPool(yourRoomDatabase, schema)
val powersync = PowerSyncDatabase.opened(
pool = pool,
scope = this,
schema = schema,
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. Pass the schema as a second parameter to the `RoomConnectionPool` constructor. This will make the
pool notify PowerSync on Room writes for every raw table mentioned in the schema.
Alternatively, call `transferPendingRoomUpdatesToPowerSync` after writes in Room.
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()
applyDefaultHierarchyTemplate()

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.
nativeTest.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
Loading