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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## 1.7.0 (unreleased)

- Add `PowerSyncDatabase.inMemory` to create an in-memory SQLite database with PowerSync.
This may be useful for testing.

## 1.6.1

* Fix `dlopen failed: library "libpowersync.so.so" not found` errors on Android.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,5 @@ public fun BundledSQLiteDriver.addPowerSyncExtension() {
@ExperimentalPowerSyncAPI
@Throws(PowerSyncException::class)
public actual fun resolvePowerSyncLoadableExtensionPath(): String? = "libpowersync.so"

internal actual fun openInMemoryConnection(): SQLiteConnection = BundledSQLiteDriver().also { it.addPowerSyncExtension() }.open(":memory:")
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,5 @@ public actual class DatabaseDriverFactory {
@ExperimentalPowerSyncAPI
@Throws(PowerSyncException::class)
public actual fun resolvePowerSyncLoadableExtensionPath(): String? = powerSyncExtensionPath

internal actual fun openInMemoryConnection(): SQLiteConnection = DatabaseDriverFactory().openConnection(":memory:", 0x02)
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.powersync.db

import app.cash.turbine.turbineScope
import co.touchlab.kermit.ExperimentalKermitApi
import co.touchlab.kermit.Logger
import co.touchlab.kermit.Severity
import co.touchlab.kermit.TestConfig
import co.touchlab.kermit.TestLogWriter
import com.powersync.PowerSyncDatabase
import com.powersync.db.schema.Column
import com.powersync.db.schema.Schema
import com.powersync.db.schema.Table
import io.kotest.matchers.collections.shouldHaveSize
import io.kotest.matchers.shouldBe
import kotlinx.coroutines.test.runTest
import kotlin.test.Test

@OptIn(ExperimentalKermitApi::class)
class InMemoryTest {
private val logWriter =
TestLogWriter(
loggable = Severity.Debug,
)

private val logger =
Logger(
TestConfig(
minSeverity = Severity.Debug,
logWriterList = listOf(logWriter),
),
)

@Test
fun createsSchema() =
runTest {
val db = PowerSyncDatabase.Companion.inMemory(schema, this, logger)
try {
db.getAll("SELECT * FROM users") { } shouldHaveSize 0
} finally {
db.close()
}
}

@Test
fun watch() =
runTest {
val db = PowerSyncDatabase.Companion.inMemory(schema, this, logger)
try {
turbineScope {
val turbine =
db.watch("SELECT name FROM users", mapper = { it.getString(0)!! }).testIn(this)

turbine.awaitItem() shouldBe listOf()

db.execute("INSERT INTO users (id, name) VALUES (uuid(), ?)", listOf("test user"))
turbine.awaitItem() shouldBe listOf("test user")
turbine.cancelAndIgnoreRemainingEvents()
}
} finally {
db.close()
}
}

companion object {
private val schema =
Schema(
Table(
name = "users",
columns =
listOf(
Column.Companion.text("name"),
),
),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ public expect class DatabaseDriverFactory {
): SQLiteConnection
}

internal expect fun openInMemoryConnection(): SQLiteConnection

/**
* Resolves a path to the loadable PowerSync core extension library.
*
Expand Down
25 changes: 25 additions & 0 deletions core/src/commonMain/kotlin/com/powersync/PowerSyncDatabase.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ import com.powersync.db.Queries
import com.powersync.db.crud.CrudBatch
import com.powersync.db.crud.CrudTransaction
import com.powersync.db.driver.SQLiteConnectionPool
import com.powersync.db.driver.SingleConnectionPool
import com.powersync.db.schema.Schema
import com.powersync.sync.SyncOptions
import com.powersync.sync.SyncStatus
import com.powersync.utils.JsonParam
import com.powersync.utils.generateLogger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
Expand Down Expand Up @@ -233,6 +235,29 @@ public interface PowerSyncDatabase : Queries {
return openedWithGroup(pool, scope, schema, logger, group)
}

/**
* Creates an in-memory PowerSync database instance, useful for testing.
*/
@OptIn(ExperimentalPowerSyncAPI::class)
public fun inMemory(
schema: Schema,
scope: CoroutineScope,
logger: Logger? = null,
): PowerSyncDatabase {
val logger = generateLogger(logger)
// Since this returns a fresh in-memory database every time, use a fresh group to avoid warnings about the
// same database being opened multiple times.
val collection = ActiveDatabaseGroup.GroupsCollection().referenceDatabase(logger, "test")

return openedWithGroup(
SingleConnectionPool(openInMemoryConnection()),
scope,
schema,
logger,
collection,
)
}

@ExperimentalPowerSyncAPI
internal fun openedWithGroup(
pool: SQLiteConnectionPool,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,33 +36,7 @@ internal class InternalConnectionPool(
readOnly = false,
)

connection.execSQL("pragma journal_mode = WAL")
connection.execSQL("pragma journal_size_limit = ${6 * 1024 * 1024}")
connection.execSQL("pragma busy_timeout = 30000")
connection.execSQL("pragma cache_size = ${50 * 1024}")

if (readOnly) {
connection.execSQL("pragma query_only = TRUE")
}

// Older versions of the SDK used to set up an empty schema and raise the user version to 1.
// Keep doing that for consistency.
if (!readOnly) {
val version =
connection.prepare("pragma user_version").use {
require(it.step())
if (it.isNull(0)) 0L else it.getLong(0)
}
if (version < 1L) {
connection.execSQL("pragma user_version = 1")
}

// Also install a commit, rollback and update hooks in the core extension to implement
// the updates flow here (not all our driver implementations support hooks, so this is
// a more reliable fallback).
connection.execSQL("select powersync_update_hooks('install');")
}

connection.setupDefaultPragmas(readOnly)
return connection
}

Expand All @@ -75,13 +49,10 @@ internal class InternalConnectionPool(
} finally {
// When we've leased a write connection, we may have to update table update flows
// after users ran their custom statements.
writeConnection.prepare("SELECT powersync_update_hooks('get')").use {
check(it.step())
val updatedTables = JsonUtil.json.decodeFromString<Set<String>>(it.getText(0))
if (updatedTables.isNotEmpty()) {
scope.launch {
tableUpdatesFlow.emit(updatedTables)
}
val updatedTables = writeConnection.readPendingUpdates()
if (updatedTables.isNotEmpty()) {
scope.launch {
tableUpdatesFlow.emit(updatedTables)
}
}
}
Expand All @@ -106,3 +77,39 @@ internal class InternalConnectionPool(
readPool.close()
}
}

internal fun SQLiteConnection.setupDefaultPragmas(readOnly: Boolean) {
execSQL("pragma journal_mode = WAL")
execSQL("pragma journal_size_limit = ${6 * 1024 * 1024}")
execSQL("pragma busy_timeout = 30000")
execSQL("pragma cache_size = ${50 * 1024}")

if (readOnly) {
execSQL("pragma query_only = TRUE")
}

// Older versions of the SDK used to set up an empty schema and raise the user version to 1.
// Keep doing that for consistency.
if (!readOnly) {
val version =
prepare("pragma user_version").use {
require(it.step())
if (it.isNull(0)) 0L else it.getLong(0)
}
if (version < 1L) {
execSQL("pragma user_version = 1")
}

// Also install a commit, rollback and update hooks in the core extension to implement
// the updates flow here (not all our driver implementations support hooks, so this is
// a more reliable fallback).
execSQL("select powersync_update_hooks('install');")
}
}

internal fun SQLiteConnection.readPendingUpdates(): Set<String> =
prepare("SELECT powersync_update_hooks('get')").use {
check(it.step())
val updatedTables = JsonUtil.json.decodeFromString<Set<String>>(it.getText(0))
updatedTables
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.powersync.db.driver

import androidx.sqlite.SQLiteConnection
import com.powersync.ExperimentalPowerSyncAPI
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock

/**
* A [SQLiteConnectionPool] backed by a single database connection.
*
* This does not provide any concurrency, but is still a reasonable implementation to use for e.g. tests.
*/
@OptIn(ExperimentalPowerSyncAPI::class)
internal class SingleConnectionPool(
private val conn: SQLiteConnection,
) : SQLiteConnectionPool {
private val mutex: Mutex = Mutex()
private var closed = false
private val tableUpdatesFlow = MutableSharedFlow<Set<String>>(replay = 0)

init {
conn.setupDefaultPragmas(false)
}

override suspend fun <T> read(callback: suspend (SQLiteConnectionLease) -> T): T = write(callback)

override suspend fun <T> write(callback: suspend (SQLiteConnectionLease) -> T): T =
mutex.withLock {
check(!closed) { "Connection closed" }

try {
callback(RawConnectionLease(conn))
} finally {
val updates = conn.readPendingUpdates()
if (updates.isNotEmpty()) {
tableUpdatesFlow.emit(updates)
}
}
}

override suspend fun <R> withAllConnections(
action: suspend (writer: SQLiteConnectionLease, readers: List<SQLiteConnectionLease>) -> R,
) = write { writer ->
action(writer, emptyList())
Unit
}

override val updates: SharedFlow<Set<String>>
get() = tableUpdatesFlow

override suspend fun close() {
mutex.withLock {
conn.close()
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.powersync

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

Expand All @@ -25,3 +26,5 @@ private val powersyncExtension: String by lazy { extractLib("powersync") }
@ExperimentalPowerSyncAPI
@Throws(PowerSyncException::class)
public actual fun resolvePowerSyncLoadableExtensionPath(): String? = runWrapped { powersyncExtension }

internal actual fun openInMemoryConnection(): SQLiteConnection = DatabaseDriverFactory().openConnection(":memory:", 0x02)
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,5 @@ public actual fun resolvePowerSyncLoadableExtensionPath(): String? {
didLoadExtension
return null
}

internal actual fun openInMemoryConnection(): SQLiteConnection = DatabaseDriverFactory().openConnection(":memory:", 0x02)
24 changes: 12 additions & 12 deletions integrations/sqldelight/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,21 @@ kotlin {
implementation(libs.kotlinx.coroutines.core)
}

commonTest.dependencies {
// Separate project because SQLDelight can't generate code in test source sets.
implementation(projects.integrations.sqldelightTestDatabase)
val commonIntegrationTest by creating {
dependsOn(commonTest.get())

implementation(libs.kotlin.test)
implementation(libs.kotlinx.io)
implementation(libs.test.turbine)
implementation(libs.test.coroutines)
implementation(libs.test.kotest.assertions)
dependencies {
// Separate project because SQLDelight can't generate code in test source sets.
implementation(projects.integrations.sqldelightTestDatabase)

implementation(libs.sqldelight.coroutines)
}
implementation(libs.kotlin.test)
implementation(libs.kotlinx.io)
implementation(libs.test.turbine)
implementation(libs.test.coroutines)
implementation(libs.test.kotest.assertions)

val commonIntegrationTest by creating {
dependsOn(commonTest.get())
implementation(libs.sqldelight.coroutines)
}
}

// The PowerSync SDK links the core extension, so we can just run tests as-is.
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -144,12 +144,9 @@ class SqlDelightTest {

private fun databaseTest(body: suspend TestScope.(PowerSyncDatabase) -> Unit) {
runTest {
val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9')
val suffix = CharArray(8) { allowedChars.random() }.concatToString()

val db =
PowerSyncDatabase(
databaseDriverFactory(),
PowerSyncDatabase.inMemory(
scope = this,
schema =
Schema(
Table(
Expand All @@ -160,13 +157,9 @@ private fun databaseTest(body: suspend TestScope.(PowerSyncDatabase) -> Unit) {
),
),
),
dbFilename = "db-$suffix",
dbDirectory = SystemTemporaryDirectory.toString(),
)

body(db)
db.close()
}
}

expect fun databaseDriverFactory(): DatabaseDriverFactory

This file was deleted.

Loading