diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..decc074d --- /dev/null +++ b/.editorconfig @@ -0,0 +1,3 @@ +[*.{kt,kts}] +ktlint_code_style = ktlint_official +ktlint_standard_property-naming = disabled \ No newline at end of file diff --git a/PowerSync/build.gradle.kts b/PowerSync/build.gradle.kts index a43bfad3..26198500 100644 --- a/PowerSync/build.gradle.kts +++ b/PowerSync/build.gradle.kts @@ -1,6 +1,5 @@ import co.touchlab.faktory.artifactmanager.ArtifactManager import co.touchlab.faktory.capitalized -import co.touchlab.skie.configuration.SuspendInterop import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget import java.net.URL import java.security.MessageDigest @@ -10,6 +9,7 @@ plugins { alias(libs.plugins.kmmbridge) alias(libs.plugins.skie) alias(libs.plugins.mavenPublishPlugin) + alias(libs.plugins.kotlinter) id("com.powersync.plugins.sonatype") } @@ -17,7 +17,7 @@ kotlin { listOf( iosX64(), iosArm64(), - iosSimulatorArm64() + iosSimulatorArm64(), ).forEach { it.binaries.framework { export(project(":core")) @@ -40,16 +40,6 @@ kotlin { } } -skie { - features { - group { - // We turn this off as the suspend interop feature results in - // threading issues when implementing SDK in Swift - SuspendInterop.Enabled(false) - } - } -} - kmmbridge { artifactManager.set(SonatypePortalPublishArtifactManager(project, repositoryName = null)) artifactManager.finalizeValue() @@ -70,19 +60,22 @@ class SonatypePortalPublishArtifactManager( val project: Project, private val publicationName: String = "KMMBridgeFramework", artifactSuffix: String = "kmmbridge", - private val repositoryName: String?, + private val repositoryName: String? ) : ArtifactManager { private val group: String = project.group.toString().replace(".", "/") private val kmmbridgeArtifactId = - "${project.name}-${artifactSuffix}" + "${project.name}-$artifactSuffix" private val LIBRARY_VERSION: String by project + // This is the URL that will be added to Package.swift in Github package so that // KMMBridge is downloaded when a user includes the package in XCode private val MAVEN_CENTRAL_PACKAGE_ZIP_URL = "https://repo1.maven.org/maven2/com/powersync/${kmmbridgeArtifactId.lowercase()}/${LIBRARY_VERSION}/${kmmbridgeArtifactId.lowercase()}-${LIBRARY_VERSION}.zip" - override fun deployArtifact(project: Project, zipFilePath: File, version: String): String { - return MAVEN_CENTRAL_PACKAGE_ZIP_URL - } + override fun deployArtifact( + project: Project, + zipFilePath: File, + version: String + ): String = MAVEN_CENTRAL_PACKAGE_ZIP_URL override fun configure( project: Project, @@ -91,12 +84,14 @@ class SonatypePortalPublishArtifactManager( kmmPublishTask: TaskProvider ) { project.extensions.getByType().publications.create( - publicationName, MavenPublication::class.java + publicationName, + MavenPublication::class.java, ) { this.version = version - val archiveProvider = project.tasks.named("zipXCFramework", Zip::class.java).flatMap { - it.archiveFile - } + val archiveProvider = + project.tasks.named("zipXCFramework", Zip::class.java).flatMap { + it.archiveFile + } artifact(archiveProvider) { extension = "zip" } @@ -136,11 +131,14 @@ class SonatypePortalPublishArtifactManager( // Either the user has supplied a correct name, or we use the default. If neither is found, fail. val publicationNameCap = - publishingExtension.publications.getByName( - publicationName - ).name.capitalized() - - return publishingExtension.repositories.filterIsInstance() + publishingExtension.publications + .getByName( + publicationName, + ).name + .capitalized() + + return publishingExtension.repositories + .filterIsInstance() .map { repo -> val repositoryName = repo.name.capitalized() val publishTaskName = @@ -175,26 +173,27 @@ abstract class UpdatePackageSwiftChecksumTask : DefaultTask() { } // Compute the checksum - val checksum = zipFile.inputStream().use { input -> - val digest = MessageDigest.getInstance("SHA-256") - val buffer = ByteArray(8192) - var bytes = input.read(buffer) - while (bytes >= 0) { - digest.update(buffer, 0, bytes) - bytes = input.read(buffer) + val checksum = + zipFile.inputStream().use { input -> + val digest = MessageDigest.getInstance("SHA-256") + val buffer = ByteArray(8192) + var bytes = input.read(buffer) + while (bytes >= 0) { + digest.update(buffer, 0, bytes) + bytes = input.read(buffer) + } + digest.digest().joinToString("") { "%02x".format(it) } } - digest.digest().joinToString("") { "%02x".format(it) } - } // Update Package.swift val packageSwiftFile = project.rootProject.file("Package.swift") - val updatedContent = packageSwiftFile.readText().replace( - Regex("let remoteKotlinChecksum = \"[a-f0-9]+\""), - "let remoteKotlinChecksum = \"$checksum\"" - ) + val updatedContent = + packageSwiftFile.readText().replace( + Regex("let remoteKotlinChecksum = \"[a-f0-9]+\""), + "let remoteKotlinChecksum = \"$checksum\"", + ) packageSwiftFile.writeText(updatedContent) println("Updated Package.swift with new checksum: $checksum") } } - diff --git a/PowerSync/src/iosMain/kotlin/com/powersync/SDK.kt b/PowerSync/src/iosMain/kotlin/com/powersync/SDK.kt index 8576d83e..05e8dc8d 100644 --- a/PowerSync/src/iosMain/kotlin/com/powersync/SDK.kt +++ b/PowerSync/src/iosMain/kotlin/com/powersync/SDK.kt @@ -1,2 +1,5 @@ +@file:Suppress("ktlint:standard:no-empty-file") + // This is required to build the iOS framework -package com.powersync \ No newline at end of file + +package com.powersync diff --git a/README.md b/README.md index ee07cbf3..65f2be4a 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,6 @@ Demo applications are located in the [`demos/`](./demos) directory. See their re - [demos/hello-powersync](./demos/hello-powersync/README.md): A minimal example demonstrating the use of the PowerSync Kotlin Multiplatform SDK and the Supabase connector. - [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 @@ -100,6 +99,11 @@ cocoapods { Note: The `linkOnly` attribute is set to `true` and framework is set to `isStatic = true` to ensure that the `powersync-sqlite-core` binaries are only statically linked. +## Formatting and Linting + +This repo uses [ktlint](https://pinterest.github.io/ktlint/) to handle formatting and linting. If you would like the IDE to automatically format your code and show linting errors install the [ktlint plugin](https://plugins.jetbrains.com/plugin/15057-ktlint). Then in Settings go to Tools -> Ktlint -> Select Distract free (recommended) mode. +It will automatically use the rules set in the `.editorconfig` file. + ## Getting Started Our [full SDK reference](https://docs.powersync.com/client-sdk-references/kotlin-multiplatform-alpha#getting-started) contains everything you need to know to get started implementing PowerSync in your project. diff --git a/build.gradle.kts b/build.gradle.kts index d73a7cdc..7e998244 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,6 +12,7 @@ plugins { alias(libs.plugins.grammarKitComposer) apply false alias(libs.plugins.mavenPublishPlugin) apply false alias(libs.plugins.downloadPlugin) apply false + alias(libs.plugins.kotlinter) apply false } // Having different versions of this lead to the issue mentioned here @@ -38,7 +39,6 @@ allprojects { } } - configurations.configureEach { exclude(group = "com.jetbrains.rd") exclude(group = "com.github.jetbrains", module = "jetCheck") diff --git a/compose/build.gradle.kts b/compose/build.gradle.kts index ca0f0a80..7fca5ded 100644 --- a/compose/build.gradle.kts +++ b/compose/build.gradle.kts @@ -5,6 +5,7 @@ plugins { alias(libs.plugins.androidLibrary) alias(libs.plugins.jetbrainsCompose) alias(libs.plugins.compose.compiler) + alias(libs.plugins.kotlinter) id("com.powersync.plugins.sonatype") } @@ -33,13 +34,19 @@ kotlin { android { namespace = "com.powersync.compose" - compileSdk = libs.versions.android.compileSdk.get().toInt() + compileSdk = + libs.versions.android.compileSdk + .get() + .toInt() defaultConfig { - minSdk = libs.versions.android.minSdk.get().toInt() + minSdk = + libs.versions.android.minSdk + .get() + .toInt() } kotlin { jvmToolchain(17) } } -setupGithubRepository() \ No newline at end of file +setupGithubRepository() diff --git a/compose/src/androidMain/kotlin/com/powersync/compose/DatabaseDriverFactory.compose.android.kt b/compose/src/androidMain/kotlin/com/powersync/compose/DatabaseDriverFactory.compose.android.kt index 5c00df24..7c65f957 100644 --- a/compose/src/androidMain/kotlin/com/powersync/compose/DatabaseDriverFactory.compose.android.kt +++ b/compose/src/androidMain/kotlin/com/powersync/compose/DatabaseDriverFactory.compose.android.kt @@ -5,12 +5,10 @@ import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext import com.powersync.DatabaseDriverFactory - @Composable public actual fun rememberDatabaseDriverFactory(): DatabaseDriverFactory { val context = LocalContext.current return remember { DatabaseDriverFactory(context) } - } diff --git a/compose/src/iosMain/kotlin/com/powersync/compose/DatabaseDriverFactory.compose.ios.kt b/compose/src/iosMain/kotlin/com/powersync/compose/DatabaseDriverFactory.compose.ios.kt index 96539b98..72a33814 100644 --- a/compose/src/iosMain/kotlin/com/powersync/compose/DatabaseDriverFactory.compose.ios.kt +++ b/compose/src/iosMain/kotlin/com/powersync/compose/DatabaseDriverFactory.compose.ios.kt @@ -4,10 +4,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import com.powersync.DatabaseDriverFactory - @Composable -public actual fun rememberDatabaseDriverFactory(): DatabaseDriverFactory { - return remember { +public actual fun rememberDatabaseDriverFactory(): DatabaseDriverFactory = + remember { DatabaseDriverFactory() } -} diff --git a/connectors/supabase/build.gradle.kts b/connectors/supabase/build.gradle.kts index 93706445..37616373 100644 --- a/connectors/supabase/build.gradle.kts +++ b/connectors/supabase/build.gradle.kts @@ -5,6 +5,7 @@ plugins { alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.kotlinSerialization) alias(libs.plugins.androidLibrary) + alias(libs.plugins.kotlinter) id("com.powersync.plugins.sonatype") } @@ -37,13 +38,19 @@ kotlin { android { namespace = "com.powersync.connector.supabase" - compileSdk = libs.versions.android.compileSdk.get().toInt() + compileSdk = + libs.versions.android.compileSdk + .get() + .toInt() defaultConfig { - minSdk = libs.versions.android.minSdk.get().toInt() + minSdk = + libs.versions.android.minSdk + .get() + .toInt() } kotlin { jvmToolchain(17) } } -setupGithubRepository() \ No newline at end of file +setupGithubRepository() diff --git a/connectors/supabase/src/commonMain/kotlin/com/powersync/connector/supabase/SupabaseConnector.kt b/connectors/supabase/src/commonMain/kotlin/com/powersync/connector/supabase/SupabaseConnector.kt index 5df5a042..024fc0eb 100644 --- a/connectors/supabase/src/commonMain/kotlin/com/powersync/connector/supabase/SupabaseConnector.kt +++ b/connectors/supabase/src/commonMain/kotlin/com/powersync/connector/supabase/SupabaseConnector.kt @@ -21,34 +21,42 @@ import kotlinx.coroutines.flow.StateFlow */ public class SupabaseConnector( public val supabaseClient: SupabaseClient, - public val powerSyncEndpoint: String + public val powerSyncEndpoint: String, ) : PowerSyncBackendConnector() { - public constructor( supabaseUrl: String, supabaseKey: String, - powerSyncEndpoint: String + powerSyncEndpoint: String, ) : this( - supabaseClient = createSupabaseClient(supabaseUrl, supabaseKey) { - install(Auth) - install(Postgrest) - }, - powerSyncEndpoint = powerSyncEndpoint + supabaseClient = + createSupabaseClient(supabaseUrl, supabaseKey) { + install(Auth) + install(Postgrest) + }, + powerSyncEndpoint = powerSyncEndpoint, ) init { require(supabaseClient.pluginManager.getPluginOrNull(Auth) != null) { "The Auth plugin must be installed on the Supabase client" } - require(supabaseClient.pluginManager.getPluginOrNull(Postgrest) != null) { "The Postgrest plugin must be installed on the Supabase client" } + require( + supabaseClient.pluginManager.getPluginOrNull(Postgrest) != null, + ) { "The Postgrest plugin must be installed on the Supabase client" } } - public suspend fun login(email: String, password: String) { + public suspend fun login( + email: String, + password: String, + ) { supabaseClient.auth.signInWith(Email) { this.email = email this.password = password } } - public suspend fun signUp(email: String, password: String) { + public suspend fun signUp( + email: String, + password: String, + ) { supabaseClient.auth.signUpWith(Email) { this.email = email this.password = password @@ -59,9 +67,7 @@ public class SupabaseConnector( supabaseClient.auth.signOut() } - public fun session(): UserSession? { - return supabaseClient.auth.currentSessionOrNull() - } + public fun session(): UserSession? = supabaseClient.auth.currentSessionOrNull() public val sessionStatus: StateFlow = supabaseClient.auth.sessionStatus @@ -84,7 +90,7 @@ public class SupabaseConnector( return PowerSyncCredentials( endpoint = powerSyncEndpoint, token = session.accessToken, // Use the access token to authenticate against PowerSync - userId = session.user!!.id + userId = session.user!!.id, ) } @@ -95,12 +101,10 @@ public class SupabaseConnector( * If this call throws an error, it is retried periodically. */ override suspend fun uploadData(database: PowerSyncDatabase) { - val transaction = database.getNextCrudTransaction() ?: return var lastEntry: CrudEntry? = null try { - for (entry in transaction.crud) { lastEntry = entry @@ -131,7 +135,6 @@ public class SupabaseConnector( } transaction.complete(null) - } catch (e: Exception) { println("Data upload error - retrying last entry: ${lastEntry!!}, $e") throw e diff --git a/core/build.gradle.kts b/core/build.gradle.kts index c46b0ef9..97a8a309 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -1,6 +1,6 @@ +import com.powersync.plugins.sonatype.setupGithubRepository import de.undercouch.gradle.tasks.download.Download import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget -import com.powersync.plugins.sonatype.setupGithubRepository plugins { alias(libs.plugins.kotlinMultiplatform) @@ -8,6 +8,7 @@ plugins { alias(libs.plugins.androidLibrary) alias(libs.plugins.mavenPublishPlugin) alias(libs.plugins.downloadPlugin) + alias(libs.plugins.kotlinter) id("com.powersync.plugins.sonatype") alias(libs.plugins.mokkery) } @@ -16,12 +17,14 @@ val sqliteVersion = "3450000" val sqliteReleaseYear = "2024" val sqliteSrcFolder = - project.layout.buildDirectory.dir("interop/sqlite").get() + project.layout.buildDirectory + .dir("interop/sqlite") + .get() val downloadSQLiteSources by tasks.registering(Download::class) { val zipFileName = "sqlite-amalgamation-$sqliteVersion.zip" val destination = sqliteSrcFolder.file(zipFileName).asFile - src("https://www.sqlite.org/$sqliteReleaseYear/${zipFileName}") + src("https://www.sqlite.org/$sqliteReleaseYear/$zipFileName") dest(destination) onlyIfNewer(true) overwrite(false) @@ -30,15 +33,17 @@ val downloadSQLiteSources by tasks.registering(Download::class) { val unzipSQLiteSources by tasks.registering(Copy::class) { dependsOn(downloadSQLiteSources) - from(zipTree(downloadSQLiteSources.get().dest).matching { - include("*/sqlite3.*") - exclude { - it.isDirectory - } - eachFile { - this.path = this.name - } - }) + from( + zipTree(downloadSQLiteSources.get().dest).matching { + include("*/sqlite3.*") + exclude { + it.isDirectory + } + eachFile { + this.path = this.name + } + }, + ) into(sqliteSrcFolder) } @@ -53,8 +58,8 @@ val buildCInteropDef by tasks.registering { """ package = com.powersync.sqlite3 --- - - """.trimIndent() + cFile.readText() + + """.trimIndent() + cFile.readText(), ) } outputs.files(defFile) @@ -75,7 +80,10 @@ kotlin { cinterops.create("sqlite") { val cInteropTask = tasks[interopProcessingTaskName] cInteropTask.dependsOn(buildCInteropDef) - defFile = buildCInteropDef.get().outputs.files.singleFile + defFile = + buildCInteropDef + .get() + .outputs.files.singleFile compilerOpts.addAll(listOf("-DHAVE_GETHOSTUUID=0")) } cinterops.create("powersync-sqlite-core") @@ -127,7 +135,6 @@ android { jvmToolchain(17) } - buildFeatures { buildConfig = true } @@ -142,16 +149,22 @@ android { } namespace = "com.powersync" - compileSdk = libs.versions.android.compileSdk.get().toInt() + compileSdk = + libs.versions.android.compileSdk + .get() + .toInt() defaultConfig { - minSdk = libs.versions.android.minSdk.get().toInt() + minSdk = + libs.versions.android.minSdk + .get() + .toInt() externalNativeBuild { cmake { arguments.addAll( listOf( - "-DSQLITE3_SRC_DIR=${sqliteSrcFolder.asFile.absolutePath}" - ) + "-DSQLITE3_SRC_DIR=${sqliteSrcFolder.asFile.absolutePath}", + ), ) } } @@ -164,18 +177,18 @@ android { } } - afterEvaluate { - val buildTasks = tasks.matching { - val taskName = it.name - if (taskName.contains("Clean")) { + val buildTasks = + tasks.matching { + val taskName = it.name + if (taskName.contains("Clean")) { + return@matching false + } + if (taskName.contains("externalNative") || taskName.contains("CMake") || taskName.contains("generateJsonModel")) { + return@matching true + } return@matching false } - if (taskName.contains("externalNative") || taskName.contains("CMake") || taskName.contains("generateJsonModel")) { - return@matching true - } - return@matching false - } buildTasks.forEach { it.dependsOn(buildCInteropDef) @@ -183,4 +196,3 @@ afterEvaluate { } setupGithubRepository() - diff --git a/core/src/androidMain/kotlin/BuildConfig.kt b/core/src/androidMain/kotlin/BuildConfig.kt index 73a27ee5..e7af1cc1 100644 --- a/core/src/androidMain/kotlin/BuildConfig.kt +++ b/core/src/androidMain/kotlin/BuildConfig.kt @@ -1,4 +1,4 @@ public actual object BuildConfig { public actual val isDebug: Boolean get() = com.powersync.BuildConfig.DEBUG -} \ No newline at end of file +} diff --git a/core/src/androidMain/kotlin/com/powersync/DatabaseDriverFactory.android.kt b/core/src/androidMain/kotlin/com/powersync/DatabaseDriverFactory.android.kt index 9f2e17c3..013b08ea 100644 --- a/core/src/androidMain/kotlin/com/powersync/DatabaseDriverFactory.android.kt +++ b/core/src/androidMain/kotlin/com/powersync/DatabaseDriverFactory.android.kt @@ -10,8 +10,11 @@ import io.requery.android.database.sqlite.SQLiteCustomExtension import kotlinx.coroutines.CoroutineScope @Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") -public actual class DatabaseDriverFactory(private val context: Context) { +public actual class DatabaseDriverFactory( + private val context: Context, +) { private var driver: PsSqlDriver? = null + private external fun setupSqliteBinding() @Suppress("unused") @@ -36,43 +39,50 @@ public actual class DatabaseDriverFactory(private val context: Context) { ): PsSqlDriver { val schema = InternalSchema.synchronous() - this.driver = PsSqlDriver(scope = scope, driver = AndroidSqliteDriver( - context = context, - schema = schema, - name = dbFilename, - factory = RequerySQLiteOpenHelperFactory( - listOf(RequerySQLiteOpenHelperFactory.ConfigurationOptions { config -> - config.customExtensions.add( - SQLiteCustomExtension( - "libpowersync", - "sqlite3_powersync_init" - ) - ) - config.customExtensions.add( - SQLiteCustomExtension( - "libpowersync-sqlite", - "powersync_init" - ) - ) - config - }) - ), - callback = object : AndroidSqliteDriver.Callback(schema) { - override fun onConfigure(db: SupportSQLiteDatabase) { - db.enableWriteAheadLogging() - super.onConfigure(db) - } - } - )) + this.driver = + PsSqlDriver( + scope = scope, + driver = + AndroidSqliteDriver( + context = context, + schema = schema, + name = dbFilename, + factory = + RequerySQLiteOpenHelperFactory( + listOf( + RequerySQLiteOpenHelperFactory.ConfigurationOptions { config -> + config.customExtensions.add( + SQLiteCustomExtension( + "libpowersync", + "sqlite3_powersync_init", + ), + ) + config.customExtensions.add( + SQLiteCustomExtension( + "libpowersync-sqlite", + "powersync_init", + ), + ) + config + }, + ), + ), + callback = + object : AndroidSqliteDriver.Callback(schema) { + override fun onConfigure(db: SupportSQLiteDatabase) { + db.enableWriteAheadLogging() + super.onConfigure(db) + } + }, + ), + ) setupSqliteBinding() return this.driver as PsSqlDriver } - public companion object { init { System.loadLibrary("powersync-sqlite") } } - -} \ No newline at end of file +} diff --git a/core/src/commonMain/kotlin/BuildConfig.kt b/core/src/commonMain/kotlin/BuildConfig.kt index 537526bd..38e47877 100644 --- a/core/src/commonMain/kotlin/BuildConfig.kt +++ b/core/src/commonMain/kotlin/BuildConfig.kt @@ -1,3 +1,3 @@ public expect object BuildConfig { public val isDebug: Boolean -} \ No newline at end of file +} diff --git a/core/src/commonMain/kotlin/com/powersync/DatabaseDriverFactory.kt b/core/src/commonMain/kotlin/com/powersync/DatabaseDriverFactory.kt index ac3bcb2f..dfe1c07d 100644 --- a/core/src/commonMain/kotlin/com/powersync/DatabaseDriverFactory.kt +++ b/core/src/commonMain/kotlin/com/powersync/DatabaseDriverFactory.kt @@ -4,9 +4,8 @@ import kotlinx.coroutines.CoroutineScope @Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") public expect class DatabaseDriverFactory { - public fun createDriver( scope: CoroutineScope, - dbFilename: String + dbFilename: String, ): PsSqlDriver -} \ No newline at end of file +} diff --git a/core/src/commonMain/kotlin/com/powersync/PowerSyncDatabase.kt b/core/src/commonMain/kotlin/com/powersync/PowerSyncDatabase.kt index 47bc6ec8..27c6adf4 100644 --- a/core/src/commonMain/kotlin/com/powersync/PowerSyncDatabase.kt +++ b/core/src/commonMain/kotlin/com/powersync/PowerSyncDatabase.kt @@ -17,7 +17,6 @@ import com.powersync.utils.JsonParam * All changes to local tables are automatically recorded, whether connected or not. Once connected, the changes are uploaded. */ public interface PowerSyncDatabase : Queries { - /** * The current sync status. */ @@ -62,10 +61,9 @@ public interface PowerSyncDatabase : Queries { connector: PowerSyncBackendConnector, crudThrottleMs: Long = 1000L, retryDelayMs: Long = 5000L, - params: Map = emptyMap() + params: Map = emptyMap(), ) - /** * Get a batch of crud data to upload. * @@ -85,7 +83,6 @@ public interface PowerSyncDatabase : Queries { */ public suspend fun getCrudBatch(limit: Int = 100): CrudBatch? - /** * Get the next recorded transaction to upload. * diff --git a/core/src/commonMain/kotlin/com/powersync/PowerSyncDatabaseFactory.kt b/core/src/commonMain/kotlin/com/powersync/PowerSyncDatabaseFactory.kt index 201fc303..c3552a06 100644 --- a/core/src/commonMain/kotlin/com/powersync/PowerSyncDatabaseFactory.kt +++ b/core/src/commonMain/kotlin/com/powersync/PowerSyncDatabaseFactory.kt @@ -1,13 +1,13 @@ package com.powersync +import co.touchlab.kermit.Logger +import co.touchlab.skie.configuration.annotations.DefaultArgumentInterop import com.powersync.db.PowerSyncDatabaseImpl import com.powersync.db.schema.Schema +import com.powersync.utils.generateLogger import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope -import co.touchlab.skie.configuration.annotations.DefaultArgumentInterop -import co.touchlab.kermit.Logger -import com.powersync.utils.generateLogger public const val DEFAULT_DB_FILENAME: String = "powersync.db" @@ -21,7 +21,7 @@ public fun PowerSyncDatabase( schema: Schema, dbFilename: String = DEFAULT_DB_FILENAME, scope: CoroutineScope = GlobalScope, - logger: Logger? = null + logger: Logger? = null, ): PowerSyncDatabase { val generatedLogger: Logger = generateLogger(logger) @@ -30,6 +30,6 @@ public fun PowerSyncDatabase( factory = factory, dbFilename = dbFilename, scope = scope, - logger = generatedLogger + logger = generatedLogger, ) -} \ No newline at end of file +} diff --git a/core/src/commonMain/kotlin/com/powersync/PsSqlDriver.kt b/core/src/commonMain/kotlin/com/powersync/PsSqlDriver.kt index 0f06afca..7e1a4052 100644 --- a/core/src/commonMain/kotlin/com/powersync/PsSqlDriver.kt +++ b/core/src/commonMain/kotlin/com/powersync/PsSqlDriver.kt @@ -9,8 +9,10 @@ import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -public class PsSqlDriver(private val driver: SqlDriver, private val scope: CoroutineScope) : - SqlDriver by driver { +public class PsSqlDriver( + private val driver: SqlDriver, + private val scope: CoroutineScope, +) : SqlDriver by driver { // MutableSharedFlow to emit batched table updates private val tableUpdatesFlow = MutableSharedFlow>(replay = 0) @@ -26,14 +28,10 @@ public class PsSqlDriver(private val driver: SqlDriver, private val scope: Corou } // Flows on table updates - public fun tableUpdates(): Flow> { - return tableUpdatesFlow.asSharedFlow() - } + public fun tableUpdates(): Flow> = tableUpdatesFlow.asSharedFlow() // Flows on table updates containing a specific table - public fun updatesOnTable(tableName: String): Flow { - return tableUpdates().filter { it.contains(tableName) }.map { } - } + public fun updatesOnTable(tableName: String): Flow = tableUpdates().filter { it.contains(tableName) }.map { } public fun fireTableUpdates() { val updates = pendingUpdates.toList() @@ -45,4 +43,4 @@ public class PsSqlDriver(private val driver: SqlDriver, private val scope: Corou } pendingUpdates.clear() } -} \ No newline at end of file +} diff --git a/core/src/commonMain/kotlin/com/powersync/bucket/BucketChecksum.kt b/core/src/commonMain/kotlin/com/powersync/bucket/BucketChecksum.kt index f03b9d0e..8c5544c4 100644 --- a/core/src/commonMain/kotlin/com/powersync/bucket/BucketChecksum.kt +++ b/core/src/commonMain/kotlin/com/powersync/bucket/BucketChecksum.kt @@ -8,5 +8,5 @@ internal data class BucketChecksum( val bucket: String, val checksum: Int, val count: Int? = null, - @SerialName("last_op_id") val lastOpId: String? = null -) \ No newline at end of file + @SerialName("last_op_id") val lastOpId: String? = null, +) diff --git a/core/src/commonMain/kotlin/com/powersync/bucket/BucketRequest.kt b/core/src/commonMain/kotlin/com/powersync/bucket/BucketRequest.kt index b54359cc..b797842c 100644 --- a/core/src/commonMain/kotlin/com/powersync/bucket/BucketRequest.kt +++ b/core/src/commonMain/kotlin/com/powersync/bucket/BucketRequest.kt @@ -3,4 +3,7 @@ package com.powersync.bucket import kotlinx.serialization.Serializable @Serializable -internal data class BucketRequest(val name: String, val after: String) \ No newline at end of file +internal data class BucketRequest( + val name: String, + val after: String, +) diff --git a/core/src/commonMain/kotlin/com/powersync/bucket/BucketState.kt b/core/src/commonMain/kotlin/com/powersync/bucket/BucketState.kt index f743afaf..c5e29c0e 100644 --- a/core/src/commonMain/kotlin/com/powersync/bucket/BucketState.kt +++ b/core/src/commonMain/kotlin/com/powersync/bucket/BucketState.kt @@ -6,8 +6,7 @@ import kotlinx.serialization.Serializable @Serializable internal data class BucketState( val bucket: String, - @SerialName("op_id") val opId: String + @SerialName("op_id") val opId: String, ) { override fun toString() = "BucketState<$bucket:$opId>" } - diff --git a/core/src/commonMain/kotlin/com/powersync/bucket/BucketStorage.kt b/core/src/commonMain/kotlin/com/powersync/bucket/BucketStorage.kt index 7f55d180..e0778a51 100644 --- a/core/src/commonMain/kotlin/com/powersync/bucket/BucketStorage.kt +++ b/core/src/commonMain/kotlin/com/powersync/bucket/BucketStorage.kt @@ -6,14 +6,24 @@ import com.powersync.sync.SyncLocalDatabaseResult internal interface BucketStorage { fun getMaxOpId(): String + suspend fun getClientId(): String + suspend fun nextCrudItem(): CrudEntry? + suspend fun hasCrud(): Boolean + suspend fun updateLocalTarget(checkpointCallback: suspend () -> String): Boolean + suspend fun saveSyncData(syncDataBatch: SyncDataBatch) + suspend fun getBucketStates(): List + suspend fun removeBuckets(bucketsToDelete: List) + suspend fun hasCompletedSync(): Boolean + suspend fun syncLocalDatabase(targetCheckpoint: Checkpoint): SyncLocalDatabaseResult + fun setTargetCheckpoint(checkpoint: Checkpoint) -} \ No newline at end of file +} diff --git a/core/src/commonMain/kotlin/com/powersync/bucket/BucketStorageImpl.kt b/core/src/commonMain/kotlin/com/powersync/bucket/BucketStorageImpl.kt index 3d528dfb..67bd7d23 100644 --- a/core/src/commonMain/kotlin/com/powersync/bucket/BucketStorageImpl.kt +++ b/core/src/commonMain/kotlin/com/powersync/bucket/BucketStorageImpl.kt @@ -1,20 +1,20 @@ package com.powersync.bucket import co.touchlab.kermit.Logger -import com.powersync.sync.SyncDataBatch -import com.powersync.sync.SyncLocalDatabaseResult import co.touchlab.stately.concurrency.AtomicBoolean import com.powersync.db.crud.CrudEntry import com.powersync.db.crud.CrudRow import com.powersync.db.internal.InternalDatabase -import kotlinx.serialization.encodeToString import com.powersync.db.internal.InternalTable +import com.powersync.sync.SyncDataBatch +import com.powersync.sync.SyncLocalDatabaseResult import com.powersync.utils.JsonUtil +import kotlinx.serialization.encodeToString internal class BucketStorageImpl( private val db: InternalDatabase, - private val logger: Logger -): BucketStorage { + private val logger: Logger, +) : BucketStorage { private val tableNames: MutableSet = mutableSetOf() private var hasCompletedSync = AtomicBoolean(false) private var pendingBucketDeletes = AtomicBoolean(false) @@ -41,35 +41,36 @@ internal class BucketStorageImpl( tableNames.addAll(names) } - override fun getMaxOpId(): String { - return MAX_OP_ID - } + override fun getMaxOpId(): String = MAX_OP_ID override suspend fun getClientId(): String { - val id = db.getOptional("SELECT powersync_client_id() as client_id") { - it.getString(0)!! - } + val id = + db.getOptional("SELECT powersync_client_id() as client_id") { + it.getString(0)!! + } return id ?: throw IllegalStateException("Client ID not found") } override suspend fun nextCrudItem(): CrudEntry? { - val crudItem = db.getOptional("SELECT id, tx_id, data FROM ${InternalTable.CRUD} ORDER BY id ASC LIMIT 1") { cursor -> - CrudEntry.fromRow( - CrudRow( - id = cursor.getString(0)!!, - txId = cursor.getString(1)?.toInt(), - data = cursor.getString(2)!! + val crudItem = + db.getOptional("SELECT id, tx_id, data FROM ${InternalTable.CRUD} ORDER BY id ASC LIMIT 1") { cursor -> + CrudEntry.fromRow( + CrudRow( + id = cursor.getString(0)!!, + txId = cursor.getString(1)?.toInt(), + data = cursor.getString(2)!!, + ), ) - ) - } + } return crudItem } override suspend fun hasCrud(): Boolean { - val res = db.getOptional("SELECT 1 FROM ps_crud LIMIT 1") { - it.getLong(0)!! - } + val res = + db.getOptional("SELECT 1 FROM ps_crud LIMIT 1") { + it.getLong(0)!! + } return res == 1L } @@ -77,7 +78,7 @@ internal class BucketStorageImpl( db.getOptional( "SELECT target_op FROM ${InternalTable.BUCKETS} WHERE name = '\$local' AND target_op = ?", parameters = listOf(MAX_OP_ID), - mapper = { cursor -> cursor.getLong(0)!! } + mapper = { cursor -> cursor.getLong(0)!! }, ) ?: // Nothing to update return false @@ -86,7 +87,7 @@ internal class BucketStorageImpl( db.getOptional("SELECT seq FROM sqlite_sequence WHERE name = '${InternalTable.CRUD}'") { it.getLong(0)!! } ?: // Nothing to update - return false + return false val opId = checkpointCallback() @@ -113,7 +114,7 @@ internal class BucketStorageImpl( db.execute( "UPDATE ${InternalTable.BUCKETS} SET target_op = CAST(? as INTEGER) WHERE name='\$local'", - listOf(opId) + listOf(opId), ) return@writeTransaction true @@ -125,22 +126,22 @@ internal class BucketStorageImpl( val jsonString = JsonUtil.json.encodeToString(syncDataBatch) tx.execute( "INSERT INTO powersync_operations(op, data) VALUES(?, ?)", - listOf("save", jsonString) + listOf("save", jsonString), ) } this.compactCounter += syncDataBatch.buckets.sumOf { it.data.size } } - override suspend fun getBucketStates(): List { - return db.getAll( + override suspend fun getBucketStates(): List = + db.getAll( "SELECT name AS bucket, CAST(last_op AS TEXT) AS op_id FROM ${InternalTable.BUCKETS} WHERE pending_delete = 0", mapper = { cursor -> BucketState( bucket = cursor.getString(0)!!, - opId = cursor.getString(1)!! + opId = cursor.getString(1)!!, ) - }) - } + }, + ) override suspend fun removeBuckets(bucketsToDelete: List) { bucketsToDelete.forEach { bucketName -> @@ -149,11 +150,10 @@ internal class BucketStorageImpl( } private suspend fun deleteBucket(bucketName: String) { - - db.writeTransaction{ tx -> + db.writeTransaction { tx -> tx.execute( "INSERT INTO powersync_operations(op, data) VALUES(?, ?)", - listOf("delete_bucket", bucketName) + listOf("delete_bucket", bucketName), ) } @@ -167,11 +167,13 @@ internal class BucketStorageImpl( return true } - val completedSync = db.getOptional( - "SELECT powersync_last_synced_at()", - mapper = { cursor -> - cursor.getString(0)!! - }) + val completedSync = + db.getOptional( + "SELECT powersync_last_synced_at()", + mapper = { cursor -> + cursor.getString(0)!! + }, + ) return if (completedSync != null) { hasCompletedSync.value = true @@ -198,7 +200,7 @@ internal class BucketStorageImpl( db.writeTransaction { tx -> tx.execute( "UPDATE ps_buckets SET last_op = ? WHERE name IN (SELECT json_each.value FROM json_each(?))", - listOf(targetCheckpoint.lastOpId, JsonUtil.json.encodeToString(bucketNames)) + listOf(targetCheckpoint.lastOpId, JsonUtil.json.encodeToString(bucketNames)), ) if (targetCheckpoint.writeCheckpoint != null) { @@ -226,17 +228,19 @@ internal class BucketStorageImpl( } private suspend fun validateChecksums(checkpoint: Checkpoint): SyncLocalDatabaseResult { - val res = db.getOptional( - "SELECT powersync_validate_checkpoint(?) AS result", - parameters = listOf(JsonUtil.json.encodeToString(checkpoint)), - mapper = { cursor -> - cursor.getString(0)!! - }) - ?: //no result - return SyncLocalDatabaseResult( - ready = false, - checkpointValid = false, + val res = + db.getOptional( + "SELECT powersync_validate_checkpoint(?) AS result", + parameters = listOf(JsonUtil.json.encodeToString(checkpoint)), + mapper = { cursor -> + cursor.getString(0)!! + }, ) + ?: // no result + return SyncLocalDatabaseResult( + ready = false, + checkpointValid = false, + ) return JsonUtil.json.decodeFromString(res) } @@ -251,12 +255,13 @@ internal class BucketStorageImpl( tx.execute( "INSERT INTO powersync_operations(op, data) VALUES(?, ?)", - listOf("sync_local", "") + listOf("sync_local", ""), ) - val res = tx.get("select last_insert_rowid()") { cursor -> - cursor.getLong(0)!! - } + val res = + tx.get("select last_insert_rowid()") { cursor -> + cursor.getLong(0)!! + } return@writeTransaction res == 1L } @@ -270,7 +275,6 @@ internal class BucketStorageImpl( this.autoCompact() } - private suspend fun autoCompact() { // 1. Delete buckets deletePendingBuckets() @@ -286,7 +290,8 @@ internal class BucketStorageImpl( db.writeTransaction { tx -> tx.execute( - "INSERT INTO powersync_operations(op, data) VALUES (?, ?)", listOf("delete_pending_buckets","") + "INSERT INTO powersync_operations(op, data) VALUES (?, ?)", + listOf("delete_pending_buckets", ""), ) // Executed once after start-up, and again when there are pending deletes. @@ -302,7 +307,7 @@ internal class BucketStorageImpl( db.writeTransaction { tx -> tx.execute( "INSERT INTO powersync_operations(op, data) VALUES (?, ?)", - listOf("clear_remove_ops", "") + listOf("clear_remove_ops", ""), ) } this.compactCounter = 0 diff --git a/core/src/commonMain/kotlin/com/powersync/bucket/Checkpoint.kt b/core/src/commonMain/kotlin/com/powersync/bucket/Checkpoint.kt index c837d889..f0088dbf 100644 --- a/core/src/commonMain/kotlin/com/powersync/bucket/Checkpoint.kt +++ b/core/src/commonMain/kotlin/com/powersync/bucket/Checkpoint.kt @@ -7,9 +7,7 @@ import kotlinx.serialization.Serializable internal data class Checkpoint( @SerialName("last_op_id") val lastOpId: String, @SerialName("buckets") val checksums: List, - @SerialName("write_checkpoint") val writeCheckpoint: String? = null + @SerialName("write_checkpoint") val writeCheckpoint: String? = null, ) { - fun clone(): Checkpoint { - return Checkpoint(lastOpId, checksums, writeCheckpoint) - } -} \ No newline at end of file + fun clone(): Checkpoint = Checkpoint(lastOpId, checksums, writeCheckpoint) +} diff --git a/core/src/commonMain/kotlin/com/powersync/bucket/ChecksumCache.kt b/core/src/commonMain/kotlin/com/powersync/bucket/ChecksumCache.kt index 9c6c2c87..bd1ce5c6 100644 --- a/core/src/commonMain/kotlin/com/powersync/bucket/ChecksumCache.kt +++ b/core/src/commonMain/kotlin/com/powersync/bucket/ChecksumCache.kt @@ -6,5 +6,5 @@ import kotlinx.serialization.Serializable @Serializable internal data class ChecksumCache( @SerialName("last_op_id") val lostOpId: String, - val checksums: Map -) \ No newline at end of file + val checksums: Map, +) diff --git a/core/src/commonMain/kotlin/com/powersync/bucket/OpType.kt b/core/src/commonMain/kotlin/com/powersync/bucket/OpType.kt index 2a0a6ee9..f65d6b5e 100644 --- a/core/src/commonMain/kotlin/com/powersync/bucket/OpType.kt +++ b/core/src/commonMain/kotlin/com/powersync/bucket/OpType.kt @@ -3,13 +3,14 @@ package com.powersync.bucket import kotlinx.serialization.Serializable @Serializable -internal enum class OpType(private val value: Int) { +internal enum class OpType( + private val value: Int, +) { CLEAR(1), MOVE(2), PUT(3), - REMOVE(4); + REMOVE(4), + ; - override fun toString(): String { - return value.toString() - } -} \ No newline at end of file + override fun toString(): String = value.toString() +} diff --git a/core/src/commonMain/kotlin/com/powersync/bucket/OplogEntry.kt b/core/src/commonMain/kotlin/com/powersync/bucket/OplogEntry.kt index 0ff92d2e..dac62094 100644 --- a/core/src/commonMain/kotlin/com/powersync/bucket/OplogEntry.kt +++ b/core/src/commonMain/kotlin/com/powersync/bucket/OplogEntry.kt @@ -15,5 +15,5 @@ internal data class OplogEntry( * There may be multiple source entries for a single "rowType + rowId" combination. */ val subkey: String? = null, - val data: String? = null -) \ No newline at end of file + val data: String? = null, +) diff --git a/core/src/commonMain/kotlin/com/powersync/bucket/SqliteOp.kt b/core/src/commonMain/kotlin/com/powersync/bucket/SqliteOp.kt index 28542a31..176f2264 100644 --- a/core/src/commonMain/kotlin/com/powersync/bucket/SqliteOp.kt +++ b/core/src/commonMain/kotlin/com/powersync/bucket/SqliteOp.kt @@ -1,3 +1,6 @@ package com.powersync.bucket -internal data class SqliteOp(val sql: String, val args: List) +internal data class SqliteOp( + val sql: String, + val args: List, +) diff --git a/core/src/commonMain/kotlin/com/powersync/bucket/WriteCheckpointResult.kt b/core/src/commonMain/kotlin/com/powersync/bucket/WriteCheckpointResult.kt index 50ff63c2..2aafd0de 100644 --- a/core/src/commonMain/kotlin/com/powersync/bucket/WriteCheckpointResult.kt +++ b/core/src/commonMain/kotlin/com/powersync/bucket/WriteCheckpointResult.kt @@ -5,10 +5,10 @@ import kotlinx.serialization.Serializable @Serializable internal data class WriteCheckpointResponse( - val data: WriteCheckpointData + val data: WriteCheckpointData, ) @Serializable internal data class WriteCheckpointData( - @SerialName("write_checkpoint") val writeCheckpoint: String -) \ No newline at end of file + @SerialName("write_checkpoint") val writeCheckpoint: String, +) diff --git a/core/src/commonMain/kotlin/com/powersync/connectors/PowerSyncBackendConnector.kt b/core/src/commonMain/kotlin/com/powersync/connectors/PowerSyncBackendConnector.kt index 551bea61..c20488a2 100644 --- a/core/src/commonMain/kotlin/com/powersync/connectors/PowerSyncBackendConnector.kt +++ b/core/src/commonMain/kotlin/com/powersync/connectors/PowerSyncBackendConnector.kt @@ -52,12 +52,13 @@ public abstract class PowerSyncBackendConnector { public open suspend fun prefetchCredentials(): Job? { fetchRequest?.takeIf { it.isActive }?.let { return it } - fetchRequest = scope.launch { - fetchCredentials().also { value -> - cachedCredentials = value - fetchRequest = null + fetchRequest = + scope.launch { + fetchCredentials().also { value -> + cachedCredentials = value + fetchRequest = null + } } - } return fetchRequest } @@ -83,4 +84,4 @@ public abstract class PowerSyncBackendConnector { * Any thrown errors will result in a retry after the configured wait period (default: 5 seconds). */ public abstract suspend fun uploadData(database: PowerSyncDatabase) -} \ No newline at end of file +} diff --git a/core/src/commonMain/kotlin/com/powersync/connectors/PowerSyncCredentials.kt b/core/src/commonMain/kotlin/com/powersync/connectors/PowerSyncCredentials.kt index 24a3c331..e3c99d75 100644 --- a/core/src/commonMain/kotlin/com/powersync/connectors/PowerSyncCredentials.kt +++ b/core/src/commonMain/kotlin/com/powersync/connectors/PowerSyncCredentials.kt @@ -1,7 +1,7 @@ package com.powersync.connectors -import kotlinx.serialization.Serializable import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable /** * Temporary credentials to connect to the PowerSync service. @@ -19,13 +19,9 @@ public data class PowerSyncCredentials( /** * User ID. */ - @SerialName("user_id") val userId: String? + @SerialName("user_id") val userId: String?, ) { - override fun toString(): String { - return "PowerSyncCredentials" - } + override fun toString(): String = "PowerSyncCredentials" - public fun endpointUri(path: String): String { - return "$endpoint/$path" - } -} \ No newline at end of file + public fun endpointUri(path: String): String = "$endpoint/$path" +} diff --git a/core/src/commonMain/kotlin/com/powersync/db/PowerSyncDatabaseImpl.kt b/core/src/commonMain/kotlin/com/powersync/db/PowerSyncDatabaseImpl.kt index 4a53ec7d..f7dbbae8 100644 --- a/core/src/commonMain/kotlin/com/powersync/db/PowerSyncDatabaseImpl.kt +++ b/core/src/commonMain/kotlin/com/powersync/db/PowerSyncDatabaseImpl.kt @@ -92,8 +92,8 @@ internal class PowerSyncDatabaseImpl( connector: PowerSyncBackendConnector, crudThrottleMs: Long, retryDelayMs: Long, - params: Map) - { + params: Map, + ) { // close connection if one is open disconnect() @@ -104,12 +104,13 @@ internal class PowerSyncDatabaseImpl( uploadCrud = suspend { connector.uploadData(this) }, retryDelayMs = retryDelayMs, logger = logger, - params = params.toJsonObject() + params = params.toJsonObject(), ) - syncJob = scope.launch { - syncStream!!.streamingSync() - } + syncJob = + scope.launch { + syncStream!!.streamingSync() + } scope.launch { syncStream!!.status.asFlow().collect { @@ -122,16 +123,17 @@ internal class PowerSyncDatabaseImpl( uploadError = it.uploadError, downloadError = it.downloadError, clearDownloadError = it.downloadError == null, - clearUploadError = it.uploadError == null + clearUploadError = it.uploadError == null, ) } } - uploadJob = scope.launch { - internalDb.updatesOnTable(InternalTable.CRUD.toString()).debounce(crudThrottleMs).collect { - syncStream!!.triggerCrudUpload() + uploadJob = + scope.launch { + internalDb.updatesOnTable(InternalTable.CRUD.toString()).debounce(crudThrottleMs).collect { + syncStream!!.triggerCrudUpload() + } } - } } override suspend fun getCrudBatch(limit: Int): CrudBatch? { @@ -139,15 +141,16 @@ internal class PowerSyncDatabaseImpl( return null } - val entries = internalDb.queries.getCrudEntries((limit + 1).toLong()).awaitAsList().map { - CrudEntry.fromRow( - CrudRow( - id = it.id.toString(), - data = it.data_!!, - txId = it.tx_id?.toInt() + val entries = + internalDb.queries.getCrudEntries((limit + 1).toLong()).awaitAsList().map { + CrudEntry.fromRow( + CrudRow( + id = it.id.toString(), + data = it.data_!!, + txId = it.tx_id?.toInt(), + ), ) - ) - } + } if (entries.isEmpty()) { return null @@ -165,24 +168,25 @@ internal class PowerSyncDatabaseImpl( override suspend fun getNextCrudTransaction(): CrudTransaction? { return this.readTransaction { - val entry = bucketStorage.nextCrudItem() - ?: return@readTransaction null - + val entry = + bucketStorage.nextCrudItem() + ?: return@readTransaction null val txId = entry.transactionId - val entries: List = if (txId == null) { - listOf(entry) - } else { - internalDb.queries.getCrudEntryByTxId(txId.toLong()).awaitAsList().map { - CrudEntry.fromRow( - CrudRow( - id = it.id.toString(), - data = it.data_!!, - txId = it.tx_id?.toInt() + val entries: List = + if (txId == null) { + listOf(entry) + } else { + internalDb.queries.getCrudEntryByTxId(txId.toLong()).awaitAsList().map { + CrudEntry.fromRow( + CrudRow( + id = it.id.toString(), + data = it.data_!!, + txId = it.tx_id?.toInt(), + ), ) - ) + } } - } return@readTransaction CrudTransaction( crud = entries, @@ -190,65 +194,56 @@ internal class PowerSyncDatabaseImpl( complete = { writeCheckpoint -> logger.i { "[CrudTransaction::complete] Completing transaction with checkpoint $writeCheckpoint" } handleWriteCheckpoint(entries.last().clientId, writeCheckpoint) - } + }, ) } } - override suspend fun getPowerSyncVersion(): String { - return internalDb.queries.powerSyncVersion().awaitAsOne() - } + override suspend fun getPowerSyncVersion(): String = internalDb.queries.powerSyncVersion().awaitAsOne() override suspend fun get( sql: String, parameters: List?, - mapper: (SqlCursor) -> RowType - ): RowType { - return internalDb.get(sql, parameters, mapper) - } + mapper: (SqlCursor) -> RowType, + ): RowType = internalDb.get(sql, parameters, mapper) override suspend fun getAll( sql: String, parameters: List?, - mapper: (SqlCursor) -> RowType - ): List { - return internalDb.getAll(sql, parameters, mapper) - } + mapper: (SqlCursor) -> RowType, + ): List = internalDb.getAll(sql, parameters, mapper) override suspend fun getOptional( sql: String, parameters: List?, - mapper: (SqlCursor) -> RowType - ): RowType? { - return internalDb.getOptional(sql, parameters, mapper) - } + mapper: (SqlCursor) -> RowType, + ): RowType? = internalDb.getOptional(sql, parameters, mapper) override fun watch( sql: String, parameters: List?, - mapper: (SqlCursor) -> RowType - ): Flow> { - return internalDb.watch(sql, parameters, mapper) - } - + mapper: (SqlCursor) -> RowType, + ): Flow> = internalDb.watch(sql, parameters, mapper) - override suspend fun readTransaction(callback: suspend (tx: PowerSyncTransaction) -> R): R { - return internalDb.readTransaction { tx -> + override suspend fun readTransaction(callback: suspend (tx: PowerSyncTransaction) -> R): R = + internalDb.readTransaction { tx -> callback(tx) } - } - override suspend fun writeTransaction(callback: suspend (tx: PowerSyncTransaction) -> R): R { - return internalDb.writeTransaction { tx -> + override suspend fun writeTransaction(callback: suspend (tx: PowerSyncTransaction) -> R): R = + internalDb.writeTransaction { tx -> callback(tx) } - } - override suspend fun execute(sql: String, parameters: List?): Long { - return internalDb.execute(sql, parameters) - } + override suspend fun execute( + sql: String, + parameters: List?, + ): Long = internalDb.execute(sql, parameters) - private suspend fun handleWriteCheckpoint(lastTransactionId: Int, writeCheckpoint: String?) { + private suspend fun handleWriteCheckpoint( + lastTransactionId: Int, + writeCheckpoint: String?, + ) { writeTransaction { tx -> internalDb.queries.deleteEntriesWithIdLessThan(lastTransactionId.toLong()) @@ -275,7 +270,7 @@ internal class PowerSyncDatabaseImpl( uploadJob?.cancelAndJoin() } - if(syncStream != null) { + if (syncStream != null) { syncStream?.invalidateCredentials() syncStream = null } @@ -287,7 +282,7 @@ internal class PowerSyncDatabaseImpl( disconnect() this.writeTransaction { - internalDb.queries.powersyncClear(if(clearLocal) "1" else "0").awaitAsOne() + internalDb.queries.powersyncClear(if (clearLocal) "1" else "0").awaitAsOne() } currentStatus.update(lastSyncedAt = null, hasSynced = false) } @@ -295,9 +290,10 @@ internal class PowerSyncDatabaseImpl( private suspend fun updateHasSynced() { // Query the database to see if any data has been synced. try { - val timestamp = internalDb.getOptional("SELECT powersync_last_synced_at() as synced_at", null) { cursor -> - cursor.getString(0)!! - } + val timestamp = + internalDb.getOptional("SELECT powersync_last_synced_at() as synced_at", null) { cursor -> + cursor.getString(0)!! + } val hasSynced = timestamp != null if (hasSynced != currentStatus.hasSynced) { @@ -306,7 +302,7 @@ internal class PowerSyncDatabaseImpl( currentStatus.update(hasSynced = hasSynced, lastSyncedAt = lastSyncedAt) } } catch (e: Exception) { - if(e is NullPointerException) { + if (e is NullPointerException) { // No data has been synced which results in a null pointer exception // and can be safely ignored. return @@ -328,20 +324,23 @@ internal class PowerSyncDatabaseImpl( * Check that a supported version of the powersync extension is loaded. */ private suspend fun checkVersion() { - val version: String = try { - getPowerSyncVersion() - } catch (e: Exception) { - throw Exception("The powersync extension is not loaded correctly. Details: $e") - } + val version: String = + try { + getPowerSyncVersion() + } catch (e: Exception) { + throw Exception("The powersync extension is not loaded correctly. Details: $e") + } // Parse version - val versionInts: List = try { - version.split(Regex("[./]")) - .take(3) - .map { it.toInt() } - } catch (e: Exception) { - throw Exception("Unsupported powersync extension version. Need ^0.2.0, got: $version. Details: $e") - } + val versionInts: List = + try { + version + .split(Regex("[./]")) + .take(3) + .map { it.toInt() } + } catch (e: Exception) { + throw Exception("Unsupported powersync extension version. Need ^0.2.0, got: $version. Details: $e") + } // Validate ^0.2.0 if (versionInts[0] != 0 || versionInts[1] < 2 || versionInts[2] < 0) { diff --git a/core/src/commonMain/kotlin/com/powersync/db/Queries.kt b/core/src/commonMain/kotlin/com/powersync/db/Queries.kt index 75dc1ed4..95497436 100644 --- a/core/src/commonMain/kotlin/com/powersync/db/Queries.kt +++ b/core/src/commonMain/kotlin/com/powersync/db/Queries.kt @@ -1,16 +1,17 @@ package com.powersync.db -import app.cash.sqldelight.SuspendingTransactionWithReturn import app.cash.sqldelight.db.SqlCursor import com.powersync.db.internal.PowerSyncTransaction import kotlinx.coroutines.flow.Flow public interface Queries { - /** * Execute a write query (INSERT, UPDATE, DELETE) */ - public suspend fun execute(sql: String, parameters: List? = listOf()): Long + public suspend fun execute( + sql: String, + parameters: List? = listOf(), + ): Long /** * Execute a read-only (SELECT) query and return a single result. @@ -20,7 +21,7 @@ public interface Queries { public suspend fun get( sql: String, parameters: List? = listOf(), - mapper: (SqlCursor) -> RowType + mapper: (SqlCursor) -> RowType, ): RowType /** @@ -29,7 +30,7 @@ public interface Queries { public suspend fun getAll( sql: String, parameters: List? = listOf(), - mapper: (SqlCursor) -> RowType + mapper: (SqlCursor) -> RowType, ): List /** @@ -38,7 +39,7 @@ public interface Queries { public suspend fun getOptional( sql: String, parameters: List? = listOf(), - mapper: (SqlCursor) -> RowType + mapper: (SqlCursor) -> RowType, ): RowType? /** @@ -47,10 +48,10 @@ public interface Queries { public fun watch( sql: String, parameters: List? = listOf(), - mapper: (SqlCursor) -> RowType + mapper: (SqlCursor) -> RowType, ): Flow> public suspend fun writeTransaction(callback: suspend (PowerSyncTransaction) -> R): R public suspend fun readTransaction(callback: suspend (PowerSyncTransaction) -> R): R -} \ No newline at end of file +} diff --git a/core/src/commonMain/kotlin/com/powersync/db/crud/CrudBatch.kt b/core/src/commonMain/kotlin/com/powersync/db/crud/CrudBatch.kt index db812836..63091912 100644 --- a/core/src/commonMain/kotlin/com/powersync/db/crud/CrudBatch.kt +++ b/core/src/commonMain/kotlin/com/powersync/db/crud/CrudBatch.kt @@ -8,16 +8,14 @@ public data class CrudBatch( * List of client-side changes. */ val crud: List, - /** * true if there are more changes in the local queue */ val hasMore: Boolean, - /** * Call to remove the changes from the local queue, once successfully uploaded. * * [writeCheckpoint] is optional. */ - val complete: suspend (writeCheckpoint: String?) -> Unit -) \ No newline at end of file + val complete: suspend (writeCheckpoint: String?) -> Unit, +) diff --git a/core/src/commonMain/kotlin/com/powersync/db/crud/CrudEntry.kt b/core/src/commonMain/kotlin/com/powersync/db/crud/CrudEntry.kt index 99bad40c..7acfb0a9 100644 --- a/core/src/commonMain/kotlin/com/powersync/db/crud/CrudEntry.kt +++ b/core/src/commonMain/kotlin/com/powersync/db/crud/CrudEntry.kt @@ -9,29 +9,24 @@ import kotlinx.serialization.json.jsonPrimitive * A single client-side change. */ public data class CrudEntry( - /** * ID of the changed row. */ val id: String, - /** * Auto-incrementing client-side id. * * Reset whenever the database is re-created. */ val clientId: Int, - /** * Type of change. */ val op: UpdateType, - /** * Table that contained the change. */ val table: String, - /** * Auto-incrementing transaction id. This is the same for all operations * within the same transaction. @@ -42,7 +37,6 @@ public data class CrudEntry( * This may change in the future. */ val transactionId: Int?, - /** * Data associated with the change. * @@ -52,7 +46,7 @@ public data class CrudEntry( * * For DELETE, this is null. */ - val opData: Map? + val opData: Map?, ) { public companion object { public fun fromRow(row: CrudRow): CrudEntry { @@ -61,17 +55,15 @@ public data class CrudEntry( id = data["id"]!!.jsonPrimitive.content, clientId = row.id.toInt(), op = UpdateType.fromJsonChecked(data["op"]!!.jsonPrimitive.content), - opData = data["data"]?.jsonObject?.mapValues { (_, value) -> - value.jsonPrimitive.contentOrNull - }, + opData = + data["data"]?.jsonObject?.mapValues { (_, value) -> + value.jsonPrimitive.contentOrNull + }, table = data["type"]!!.jsonPrimitive.content, transactionId = row.txId, ) } } - override fun toString(): String { - return "CrudEntry<$transactionId/$clientId ${op.toJson()} $table/$id $opData>" - } - + override fun toString(): String = "CrudEntry<$transactionId/$clientId ${op.toJson()} $table/$id $opData>" } diff --git a/core/src/commonMain/kotlin/com/powersync/db/crud/CrudRow.kt b/core/src/commonMain/kotlin/com/powersync/db/crud/CrudRow.kt index 3533a374..2494bf03 100644 --- a/core/src/commonMain/kotlin/com/powersync/db/crud/CrudRow.kt +++ b/core/src/commonMain/kotlin/com/powersync/db/crud/CrudRow.kt @@ -3,4 +3,8 @@ package com.powersync.db.crud import kotlinx.serialization.Serializable @Serializable -public data class CrudRow(val id: String, val data: String, val txId: Int?) \ No newline at end of file +public data class CrudRow( + val id: String, + val data: String, + val txId: Int?, +) diff --git a/core/src/commonMain/kotlin/com/powersync/db/crud/CrudTransaction.kt b/core/src/commonMain/kotlin/com/powersync/db/crud/CrudTransaction.kt index 6a3b2bf4..46562c26 100644 --- a/core/src/commonMain/kotlin/com/powersync/db/crud/CrudTransaction.kt +++ b/core/src/commonMain/kotlin/com/powersync/db/crud/CrudTransaction.kt @@ -10,18 +10,16 @@ public data class CrudTransaction( * If null, this contains a list of changes recorded without an explicit transaction associated. */ val transactionId: Int?, - /** * List of client-side changes. */ val crud: List, - /** * Call to remove the changes from the local queue, once successfully uploaded. * * [writeCheckpoint] is optional. */ - val complete: suspend (writeCheckpoint: String?) -> Unit + val complete: suspend (writeCheckpoint: String?) -> Unit, ) { override fun toString(): String = "CrudTransaction<$transactionId, $crud>" -} \ No newline at end of file +} diff --git a/core/src/commonMain/kotlin/com/powersync/db/crud/UpdateType.kt b/core/src/commonMain/kotlin/com/powersync/db/crud/UpdateType.kt index b4a0919d..29a9e4db 100644 --- a/core/src/commonMain/kotlin/com/powersync/db/crud/UpdateType.kt +++ b/core/src/commonMain/kotlin/com/powersync/db/crud/UpdateType.kt @@ -3,7 +3,9 @@ package com.powersync.db.crud /** * Type of local change. */ -public enum class UpdateType(private val json: String) { +public enum class UpdateType( + private val json: String, +) { /** * Insert or replace a row. All non-null columns are included in the data. */ @@ -17,16 +19,13 @@ public enum class UpdateType(private val json: String) { /** * Delete a row if it exists. */ - DELETE("DELETE"); + DELETE("DELETE"), + ; - public fun toJson(): String { - return json - } + public fun toJson(): String = json public companion object { - public fun fromJson(json: String): UpdateType? { - return entries.find { it.json == json } - } + public fun fromJson(json: String): UpdateType? = entries.find { it.json == json } public fun fromJsonChecked(json: String): UpdateType { val v = fromJson(json) @@ -34,4 +33,4 @@ public enum class UpdateType(private val json: String) { return v } } -} \ No newline at end of file +} diff --git a/core/src/commonMain/kotlin/com/powersync/db/crud/UploadQueueStats.kt b/core/src/commonMain/kotlin/com/powersync/db/crud/UploadQueueStats.kt index 4499fe72..e6f5dd8c 100644 --- a/core/src/commonMain/kotlin/com/powersync/db/crud/UploadQueueStats.kt +++ b/core/src/commonMain/kotlin/com/powersync/db/crud/UploadQueueStats.kt @@ -8,18 +8,15 @@ public data class UploadQueueStats( * Number of records in the upload queue. */ val count: Int, - /** * Size of the upload queue in bytes. */ - val size: Int? + val size: Int?, ) { - - override fun toString(): String { - return if (size == null) { + override fun toString(): String = + if (size == null) { "UploadQueueStats" } else { "UploadQueueStats" } - } -} \ No newline at end of file +} diff --git a/core/src/commonMain/kotlin/com/powersync/db/internal/InternalDatabase.kt b/core/src/commonMain/kotlin/com/powersync/db/internal/InternalDatabase.kt index 5796e12f..d894e241 100644 --- a/core/src/commonMain/kotlin/com/powersync/db/internal/InternalDatabase.kt +++ b/core/src/commonMain/kotlin/com/powersync/db/internal/InternalDatabase.kt @@ -12,5 +12,6 @@ internal interface InternalDatabase : Queries { val queries: PowersyncQueries fun getExistingTableNames(tableGlob: String): List + fun updatesOnTable(tableName: String): Flow -} \ No newline at end of file +} diff --git a/core/src/commonMain/kotlin/com/powersync/db/internal/InternalDatabaseImpl.kt b/core/src/commonMain/kotlin/com/powersync/db/internal/InternalDatabaseImpl.kt index a4a967fc..74785480 100644 --- a/core/src/commonMain/kotlin/com/powersync/db/internal/InternalDatabaseImpl.kt +++ b/core/src/commonMain/kotlin/com/powersync/db/internal/InternalDatabaseImpl.kt @@ -21,11 +21,11 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.serialization.encodeToString - @OptIn(FlowPreview::class) -internal class InternalDatabaseImpl(override val driver: PsSqlDriver, private val scope: CoroutineScope): - InternalDatabase { - +internal class InternalDatabaseImpl( + override val driver: PsSqlDriver, + private val scope: CoroutineScope, +) : InternalDatabase { override val transactor: PsDatabase = PsDatabase(driver) override val queries = transactor.powersyncQueries @@ -52,72 +52,77 @@ internal class InternalDatabaseImpl(override val driver: PsSqlDriver, private va override suspend fun execute( sql: String, - parameters: List? + parameters: List?, ): Long { val numParams = parameters?.size ?: 0 - return driver.execute( - identifier = null, - sql = sql, - parameters = numParams, - binders = getBindersFromParams(parameters) - ).await() + return driver + .execute( + identifier = null, + sql = sql, + parameters = numParams, + binders = getBindersFromParams(parameters), + ).await() } override suspend fun get( sql: String, parameters: List?, - mapper: (SqlCursor) -> RowType + mapper: (SqlCursor) -> RowType, ): RowType { - val result = this.createQuery( - query = sql, - parameters = parameters?.size ?: 0, - binders = getBindersFromParams(parameters), - mapper = mapper - ).awaitAsOneOrNull() + val result = + this + .createQuery( + query = sql, + parameters = parameters?.size ?: 0, + binders = getBindersFromParams(parameters), + mapper = mapper, + ).awaitAsOneOrNull() return requireNotNull(result) { "Query returned no result" } } override suspend fun getAll( sql: String, parameters: List?, - mapper: (SqlCursor) -> RowType - ): List { - return this.createQuery( - query = sql, - parameters = parameters?.size ?: 0, - binders = getBindersFromParams(parameters), - mapper = mapper - ).awaitAsList() - } + mapper: (SqlCursor) -> RowType, + ): List = + this + .createQuery( + query = sql, + parameters = parameters?.size ?: 0, + binders = getBindersFromParams(parameters), + mapper = mapper, + ).awaitAsList() override suspend fun getOptional( sql: String, parameters: List?, - mapper: (SqlCursor) -> RowType - ): RowType? { - return this.createQuery( - query = sql, - parameters = parameters?.size ?: 0, - binders = getBindersFromParams(parameters), - mapper = mapper - ).awaitAsOneOrNull() - } + mapper: (SqlCursor) -> RowType, + ): RowType? = + this + .createQuery( + query = sql, + parameters = parameters?.size ?: 0, + binders = getBindersFromParams(parameters), + mapper = mapper, + ).awaitAsOneOrNull() override fun watch( sql: String, parameters: List?, - mapper: (SqlCursor) -> RowType + mapper: (SqlCursor) -> RowType, ): Flow> { - - val tables = getSourceTables(sql, parameters).map { toFriendlyTableName(it) } - .filter { it.isNotBlank() }.toSet() + val tables = + getSourceTables(sql, parameters) + .map { toFriendlyTableName(it) } + .filter { it.isNotBlank() } + .toSet() return watchQuery( query = sql, parameters = parameters?.size ?: 0, binders = getBindersFromParams(parameters), mapper = mapper, - tables = tables + tables = tables, ).asFlow().mapToList(scope.coroutineContext) } @@ -126,26 +131,22 @@ internal class InternalDatabaseImpl(override val driver: PsSqlDriver, private va mapper: (SqlCursor) -> T, parameters: Int = 0, binders: (SqlPreparedStatement.() -> Unit)? = null, - ): ExecutableQuery { - return object : ExecutableQuery(mapper) { - override fun execute(mapper: (SqlCursor) -> QueryResult): QueryResult { - return driver.executeQuery(null, query, mapper, parameters, binders) - } + ): ExecutableQuery = + object : ExecutableQuery(mapper) { + override fun execute(mapper: (SqlCursor) -> QueryResult): QueryResult = + driver.executeQuery(null, query, mapper, parameters, binders) } - } private fun watchQuery( query: String, mapper: (SqlCursor) -> T, parameters: Int = 0, binders: (SqlPreparedStatement.() -> Unit)? = null, - tables: Set = setOf() - ): Query { - - return object : Query(mapper) { - override fun execute(mapper: (SqlCursor) -> QueryResult): QueryResult { - return driver.executeQuery(null, query, mapper, parameters, binders) - } + tables: Set = setOf(), + ): Query = + object : Query(mapper) { + override fun execute(mapper: (SqlCursor) -> QueryResult): QueryResult = + driver.executeQuery(null, query, mapper, parameters, binders) override fun addListener(listener: Listener) { driver.addListener(queryKeys = tables.toTypedArray(), listener = listener) @@ -155,31 +156,24 @@ internal class InternalDatabaseImpl(override val driver: PsSqlDriver, private va driver.removeListener(queryKeys = tables.toTypedArray(), listener = listener) } } - } - override suspend fun readTransaction(callback: suspend (PowerSyncTransaction) -> R): R { - return transactor.transactionWithResult(noEnclosing = true) { + override suspend fun readTransaction(callback: suspend (PowerSyncTransaction) -> R): R = + transactor.transactionWithResult(noEnclosing = true) { val transaction = PowerSyncTransaction(this@InternalDatabaseImpl) callback(transaction) } - } - override suspend fun writeTransaction(callback: suspend (PowerSyncTransaction) -> R): R { - return transactor.transactionWithResult(noEnclosing = true) { + override suspend fun writeTransaction(callback: suspend (PowerSyncTransaction) -> R): R = + transactor.transactionWithResult(noEnclosing = true) { val transaction = PowerSyncTransaction(this@InternalDatabaseImpl) callback(transaction) } - } // Register callback for table updates - private fun tableUpdates(): Flow> { - return driver.tableUpdates() - } + private fun tableUpdates(): Flow> = driver.tableUpdates() // Register callback for table updates on a specific table - override fun updatesOnTable(tableName: String): Flow { - return driver.updatesOnTable(tableName) - } + override fun updatesOnTable(tableName: String): Flow = driver.updatesOnTable(tableName) private fun toFriendlyTableName(tableName: String): String { val regex = POWERSYNC_TABLE_MATCH.toRegex() @@ -193,20 +187,21 @@ internal class InternalDatabaseImpl(override val driver: PsSqlDriver, private va sql: String, parameters: List?, ): Set { - val rows = createQuery( - query = "EXPLAIN $sql", - parameters = parameters?.size ?: 0, - binders = getBindersFromParams(parameters), - mapper = { - ExplainQueryResult( - addr = it.getString(0)!!, - opcode = it.getString(1)!!, - p1 = it.getLong(2)!!, - p2 = it.getLong(3)!!, - p3 = it.getLong(4)!! - ) - } - ).executeAsList() + val rows = + createQuery( + query = "EXPLAIN $sql", + parameters = parameters?.size ?: 0, + binders = getBindersFromParams(parameters), + mapper = { + ExplainQueryResult( + addr = it.getString(0)!!, + opcode = it.getString(1)!!, + p1 = it.getLong(2)!!, + p2 = it.getLong(3)!!, + p3 = it.getLong(4)!!, + ) + }, + ).executeAsList() val rootPages = mutableListOf() for (row in rows) { @@ -215,28 +210,31 @@ internal class InternalDatabaseImpl(override val driver: PsSqlDriver, private va } } val params = listOf(JsonUtil.json.encodeToString(rootPages)) - val tableRows = createQuery( - "SELECT tbl_name FROM sqlite_master WHERE rootpage IN (SELECT json_each.value FROM json_each(?))", - parameters = params.size, - binders = { - bindString(0, params[0]) - }, mapper = { it.getString(0)!! } - ).executeAsList() + val tableRows = + createQuery( + "SELECT tbl_name FROM sqlite_master WHERE rootpage IN (SELECT json_each.value FROM json_each(?))", + parameters = params.size, + binders = { + bindString(0, params[0]) + }, + mapper = { it.getString(0)!! }, + ).executeAsList() return tableRows.toSet() } override fun getExistingTableNames(tableGlob: String): List { - val existingTableNames = createQuery( - "SELECT name FROM sqlite_master WHERE type='table' AND name GLOB ?", - parameters = 1, - binders = { - bindString(0, tableGlob) - }, - mapper = { cursor -> - cursor.getString(0)!! - } - ).executeAsList() + val existingTableNames = + createQuery( + "SELECT name FROM sqlite_master WHERE type='table' AND name GLOB ?", + parameters = 1, + binders = { + bindString(0, tableGlob) + }, + mapper = { cursor -> + cursor.getString(0)!! + }, + ).executeAsList() return existingTableNames } @@ -262,11 +260,11 @@ internal fun getBindersFromParams(parameters: List?): (SqlPreparedStatemen is Double -> bindDouble(index, parameter) is ByteArray -> bindBytes(index, parameter) else -> { - if(parameter != null) { + if (parameter != null) { throw IllegalArgumentException("Unsupported parameter type: ${parameter::class}, at index $index") } } } } } -} \ No newline at end of file +} diff --git a/core/src/commonMain/kotlin/com/powersync/db/internal/InternalSchema.kt b/core/src/commonMain/kotlin/com/powersync/db/internal/InternalSchema.kt index f020cac7..a0c491fd 100644 --- a/core/src/commonMain/kotlin/com/powersync/db/internal/InternalSchema.kt +++ b/core/src/commonMain/kotlin/com/powersync/db/internal/InternalSchema.kt @@ -9,14 +9,14 @@ internal object InternalSchema : SqlSchema> { override val version: Long get() = 1 - override fun create(driver: SqlDriver): QueryResult.AsyncValue = QueryResult.AsyncValue { - } + override fun create(driver: SqlDriver): QueryResult.AsyncValue = + QueryResult.AsyncValue {} override fun migrate( driver: SqlDriver, oldVersion: Long, newVersion: Long, vararg callbacks: AfterVersion, - ): QueryResult.AsyncValue = QueryResult.AsyncValue { - } -} \ No newline at end of file + ): QueryResult.AsyncValue = + QueryResult.AsyncValue {} +} diff --git a/core/src/commonMain/kotlin/com/powersync/db/internal/InternalTable.kt b/core/src/commonMain/kotlin/com/powersync/db/internal/InternalTable.kt index 6a1a978e..20fff682 100644 --- a/core/src/commonMain/kotlin/com/powersync/db/internal/InternalTable.kt +++ b/core/src/commonMain/kotlin/com/powersync/db/internal/InternalTable.kt @@ -1,14 +1,14 @@ package com.powersync.db.internal -internal enum class InternalTable(private val tableName: String) { +internal enum class InternalTable( + private val tableName: String, +) { DATA("ps_data"), CRUD("ps_crud"), BUCKETS("ps_buckets"), OPLOG("ps_oplog"), - UNTYPED("ps_untyped"); + UNTYPED("ps_untyped"), + ; - - override fun toString(): String { - return tableName - } -} \ No newline at end of file + override fun toString(): String = tableName +} diff --git a/core/src/commonMain/kotlin/com/powersync/db/internal/PowerSyncTransaction.kt b/core/src/commonMain/kotlin/com/powersync/db/internal/PowerSyncTransaction.kt index 7ace2e33..6168b75d 100644 --- a/core/src/commonMain/kotlin/com/powersync/db/internal/PowerSyncTransaction.kt +++ b/core/src/commonMain/kotlin/com/powersync/db/internal/PowerSyncTransaction.kt @@ -3,23 +3,26 @@ package com.powersync.db.internal import app.cash.sqldelight.db.SqlCursor public interface PowerSyncTransaction { - public suspend fun execute(sql: String, parameters: List? = listOf()): Long + public suspend fun execute( + sql: String, + parameters: List? = listOf(), + ): Long public suspend fun getOptional( sql: String, parameters: List? = listOf(), - mapper: (SqlCursor) -> RowType + mapper: (SqlCursor) -> RowType, ): RowType? public suspend fun getAll( sql: String, parameters: List? = listOf(), - mapper: (SqlCursor) -> RowType + mapper: (SqlCursor) -> RowType, ): List public suspend fun get( sql: String, parameters: List? = listOf(), - mapper: (SqlCursor) -> RowType + mapper: (SqlCursor) -> RowType, ): RowType } diff --git a/core/src/commonMain/kotlin/com/powersync/db/internal/PowerSyncTransactionFactory.kt b/core/src/commonMain/kotlin/com/powersync/db/internal/PowerSyncTransactionFactory.kt index 8d2e11b9..f5a32508 100644 --- a/core/src/commonMain/kotlin/com/powersync/db/internal/PowerSyncTransactionFactory.kt +++ b/core/src/commonMain/kotlin/com/powersync/db/internal/PowerSyncTransactionFactory.kt @@ -1,41 +1,35 @@ package com.powersync import app.cash.sqldelight.db.SqlCursor -import com.powersync.db.internal.PowerSyncTransaction import com.powersync.db.internal.InternalDatabaseImpl +import com.powersync.db.internal.PowerSyncTransaction -internal fun PowerSyncTransaction( - internalDatabase: InternalDatabaseImpl, -): PowerSyncTransaction { - val transaction = object : PowerSyncTransaction { +internal fun PowerSyncTransaction(internalDatabase: InternalDatabaseImpl): PowerSyncTransaction { + val transaction = + object : PowerSyncTransaction { + override suspend fun execute( + sql: String, + parameters: List?, + ): Long = internalDatabase.execute(sql, parameters ?: emptyList()) - override suspend fun execute(sql: String, parameters: List?): Long { - return internalDatabase.execute(sql, parameters ?: emptyList()) - } + override suspend fun get( + sql: String, + parameters: List?, + mapper: (SqlCursor) -> RowType, + ): RowType = internalDatabase.get(sql, parameters ?: emptyList(), mapper) - override suspend fun get( - sql: String, - parameters: List?, - mapper: (SqlCursor) -> RowType - ): RowType { - return internalDatabase.get(sql, parameters ?: emptyList(), mapper) - } + override suspend fun getAll( + sql: String, + parameters: List?, + mapper: (SqlCursor) -> RowType, + ): List = internalDatabase.getAll(sql, parameters ?: emptyList(), mapper) - override suspend fun getAll( - sql: String, - parameters: List?, - mapper: (SqlCursor) -> RowType - ): List { - return internalDatabase.getAll(sql, parameters ?: emptyList(), mapper) + override suspend fun getOptional( + sql: String, + parameters: List?, + mapper: (SqlCursor) -> RowType, + ): RowType? = internalDatabase.getOptional(sql, parameters ?: emptyList(), mapper) } - override suspend fun getOptional( - sql: String, - parameters: List?, - mapper: (SqlCursor) -> RowType - ): RowType? { - return internalDatabase.getOptional(sql, parameters ?: emptyList(), mapper) - } - } return transaction -} \ No newline at end of file +} diff --git a/core/src/commonMain/kotlin/com/powersync/db/schema/Column.kt b/core/src/commonMain/kotlin/com/powersync/db/schema/Column.kt index 17266de7..04a4d5f5 100644 --- a/core/src/commonMain/kotlin/com/powersync/db/schema/Column.kt +++ b/core/src/commonMain/kotlin/com/powersync/db/schema/Column.kt @@ -7,7 +7,6 @@ import kotlinx.serialization.Serializable public data class Column( /** Name of the column. */ val name: String, - /** Type of the column. * * If the underlying data does not match this type, @@ -16,7 +15,7 @@ public data class Column( * For details on the cast, see: * https://www.sqlite.org/lang_expr.html#castexpr */ - val type: ColumnType + val type: ColumnType, ) { public companion object { /** Create a TEXT column. */ @@ -28,4 +27,4 @@ public data class Column( /** Create a REAL column. */ public fun real(name: String): Column = Column(name, ColumnType.REAL) } -} \ No newline at end of file +} diff --git a/core/src/commonMain/kotlin/com/powersync/db/schema/ColumnType.kt b/core/src/commonMain/kotlin/com/powersync/db/schema/ColumnType.kt index 7a260261..45e06237 100644 --- a/core/src/commonMain/kotlin/com/powersync/db/schema/ColumnType.kt +++ b/core/src/commonMain/kotlin/com/powersync/db/schema/ColumnType.kt @@ -4,4 +4,4 @@ public enum class ColumnType { INTEGER, TEXT, REAL, -} \ No newline at end of file +} diff --git a/core/src/commonMain/kotlin/com/powersync/db/schema/Index.kt b/core/src/commonMain/kotlin/com/powersync/db/schema/Index.kt index 997b98d3..0bc33287 100644 --- a/core/src/commonMain/kotlin/com/powersync/db/schema/Index.kt +++ b/core/src/commonMain/kotlin/com/powersync/db/schema/Index.kt @@ -8,13 +8,11 @@ public data class Index( * Descriptive name of the index. */ val name: String, - /** * List of columns used for the index. */ - val columns: List + val columns: List, ) { - /** * @param name Descriptive name of the index. * @param columns List of columns used for the index. @@ -25,18 +23,17 @@ public data class Index( * Construct a new index with the specified column names. */ public companion object { - public fun ascending(name: String, columns: List): Index { - return Index(name, columns.map { IndexedColumn.ascending(it) }) - } + public fun ascending( + name: String, + columns: List, + ): Index = Index(name, columns.map { IndexedColumn.ascending(it) }) } /** * Internal use only. * Specifies the full name of this index on a table. */ - internal fun fullName(table: Table): String { - return "${table.internalName}__$name" - } + internal fun fullName(table: Table): String = "${table.internalName}__$name" /** * Internal use only. @@ -46,4 +43,4 @@ public data class Index( val fields = columns.joinToString(", ") { it.toSql(table) } return """CREATE INDEX "${fullName(table)}" ON "${table.internalName}"($fields)""" } -} \ No newline at end of file +} diff --git a/core/src/commonMain/kotlin/com/powersync/db/schema/IndexedColumn.kt b/core/src/commonMain/kotlin/com/powersync/db/schema/IndexedColumn.kt index 8ad5dbb3..3d9f68b0 100644 --- a/core/src/commonMain/kotlin/com/powersync/db/schema/IndexedColumn.kt +++ b/core/src/commonMain/kotlin/com/powersync/db/schema/IndexedColumn.kt @@ -13,21 +13,19 @@ public data class IndexedColumn( */ @SerialName("name") val column: String, - /** * Whether this column is stored in ascending order in the index. */ private val ascending: Boolean = true, - private var columnDefinition: Column? = null, - /** * The column definition type */ - var type: ColumnType? = null + var type: ColumnType? = null, ) { public companion object { public fun ascending(column: String): IndexedColumn = IndexedColumn(column, true) + public fun descending(column: String): IndexedColumn = IndexedColumn(column, false) } @@ -36,8 +34,8 @@ public data class IndexedColumn( * is required for the serialized JSON payload of powersync_replace_schema */ internal fun setColumnDefinition(column: Column) { - this.type = column.type; - this.columnDefinition = column; + this.type = column.type + this.columnDefinition = column } internal fun toSql(table: Table): String { @@ -48,6 +46,4 @@ public data class IndexedColumn( } } -internal fun mapColumn(column: Column): String { - return "CAST(json_extract(data, ${column.name}) as ${column.type})" -} \ No newline at end of file +internal fun mapColumn(column: Column): String = "CAST(json_extract(data, ${column.name}) as ${column.type})" diff --git a/core/src/commonMain/kotlin/com/powersync/db/schema/Schema.kt b/core/src/commonMain/kotlin/com/powersync/db/schema/Schema.kt index ca184a06..fa10e723 100644 --- a/core/src/commonMain/kotlin/com/powersync/db/schema/Schema.kt +++ b/core/src/commonMain/kotlin/com/powersync/db/schema/Schema.kt @@ -3,14 +3,15 @@ package com.powersync.db.schema import kotlinx.serialization.Serializable @Serializable -public data class Schema(val tables: List) { +public data class Schema( + val tables: List
, +) { init { validate() } public constructor(vararg tables: Table) : this(tables.asList()) - public fun validate() { val tableNames = mutableSetOf() tables.forEach { table -> @@ -20,4 +21,4 @@ public data class Schema(val tables: List
) { table.validate() } } -} \ No newline at end of file +} diff --git a/core/src/commonMain/kotlin/com/powersync/db/schema/Table.kt b/core/src/commonMain/kotlin/com/powersync/db/schema/Table.kt index 9870f53c..4ecc522d 100644 --- a/core/src/commonMain/kotlin/com/powersync/db/schema/Table.kt +++ b/core/src/commonMain/kotlin/com/powersync/db/schema/Table.kt @@ -32,9 +32,8 @@ public data class Table constructor( /** * Override the name for the view */ - private val viewNameOverride: String? = null + private val viewNameOverride: String? = null, ) { - init { /** * Need to set the column definition for each index column. @@ -42,8 +41,9 @@ public data class Table constructor( */ indexes.forEach { index -> index.columns.forEach { - val matchingColumn = columns.find { c -> c.name == it.column } - ?: throw AssertionError("Could not find column definition for index ${index.name}:${it.column}") + val matchingColumn = + columns.find { c -> c.name == it.column } + ?: throw AssertionError("Could not find column definition for index ${index.name}:${it.column}") it.setColumnDefinition(column = matchingColumn) } } @@ -59,17 +59,16 @@ public data class Table constructor( name: String, columns: List, indexes: List = listOf(), - viewName: String? = null - ): Table { - return Table( + viewName: String? = null, + ): Table = + Table( name, columns, indexes, localOnly = true, insertOnly = false, - viewNameOverride = viewName + viewNameOverride = viewName, ) - } /** * Create a table that only supports inserts. @@ -78,16 +77,19 @@ public data class Table constructor( * * SELECT queries on the table will always return 0 rows. */ - public fun insertOnly(name: String, columns: List, viewName: String? = null): Table { - return Table( + public fun insertOnly( + name: String, + columns: List, + viewName: String? = null, + ): Table = + Table( name, columns, indexes = listOf(), localOnly = false, insertOnly = true, - viewNameOverride = viewName + viewNameOverride = viewName, ) - } } /** @@ -98,19 +100,20 @@ public data class Table constructor( internal val internalName: String get() = if (localOnly) "ps_data_local__$name" else "ps_data__$name" - public operator fun get(columnName: String): Column { - return columns.first { it.name == columnName } - } + public operator fun get(columnName: String): Column = columns.first { it.name == columnName } /** * Whether this table name is valid. */ val validName: Boolean - get() = !invalidSqliteCharacters.containsMatchIn(name) && - (viewNameOverride == null || !invalidSqliteCharacters.containsMatchIn( - viewNameOverride - )) - + get() = + !invalidSqliteCharacters.containsMatchIn(name) && + ( + viewNameOverride == null || + !invalidSqliteCharacters.containsMatchIn( + viewNameOverride, + ) + ) /** * Check that there are no issues in the table definition. @@ -124,8 +127,9 @@ public data class Table constructor( throw AssertionError("Invalid characters in table name: $name") } - if (viewNameOverride != null && invalidSqliteCharacters.containsMatchIn( - viewNameOverride + if (viewNameOverride != null && + invalidSqliteCharacters.containsMatchIn( + viewNameOverride, ) ) { throw AssertionError("Invalid characters in view name: $viewNameOverride") @@ -179,4 +183,4 @@ public data class Table constructor( */ public val viewName: String get() = viewNameOverride ?: name -} \ No newline at end of file +} diff --git a/core/src/commonMain/kotlin/com/powersync/db/schema/validation.kt b/core/src/commonMain/kotlin/com/powersync/db/schema/validation.kt index bf2cfb65..ecd160d6 100644 --- a/core/src/commonMain/kotlin/com/powersync/db/schema/validation.kt +++ b/core/src/commonMain/kotlin/com/powersync/db/schema/validation.kt @@ -1,3 +1,5 @@ +@file:Suppress("ktlint:standard:filename") + package com.powersync.db.schema -internal val invalidSqliteCharacters = Regex("""["'%,.#\s\[\]]""") \ No newline at end of file +internal val invalidSqliteCharacters = Regex("""["'%,.#\s\[\]]""") diff --git a/core/src/commonMain/kotlin/com/powersync/sync/StreamingSyncCheckpointDiff.kt b/core/src/commonMain/kotlin/com/powersync/sync/StreamingSyncCheckpointDiff.kt index 84fcab7f..94d26fba 100644 --- a/core/src/commonMain/kotlin/com/powersync/sync/StreamingSyncCheckpointDiff.kt +++ b/core/src/commonMain/kotlin/com/powersync/sync/StreamingSyncCheckpointDiff.kt @@ -9,5 +9,5 @@ internal data class StreamingSyncCheckpointDiff( @SerialName("last_op_id") val lastOpId: String, @SerialName("updated_buckets") val updatedBuckets: List, @SerialName("removed_buckets") val removedBuckets: List, - @SerialName("write_checkpoint") val writeCheckpoint: String? = null -) \ No newline at end of file + @SerialName("write_checkpoint") val writeCheckpoint: String? = null, +) diff --git a/core/src/commonMain/kotlin/com/powersync/sync/StreamingSyncRequest.kt b/core/src/commonMain/kotlin/com/powersync/sync/StreamingSyncRequest.kt index 6fda9fe4..9c77ef8d 100644 --- a/core/src/commonMain/kotlin/com/powersync/sync/StreamingSyncRequest.kt +++ b/core/src/commonMain/kotlin/com/powersync/sync/StreamingSyncRequest.kt @@ -10,7 +10,8 @@ internal data class StreamingSyncRequest( val buckets: List, @SerialName("include_checksum") val includeChecksum: Boolean = true, @SerialName("client_id") val clientId: String, - val parameters: JsonObject = JsonObject(mapOf()) + val parameters: JsonObject = JsonObject(mapOf()), ) { - @SerialName("raw_data") private val rawData: Boolean = true -} \ No newline at end of file + @SerialName("raw_data") + private val rawData: Boolean = true +} diff --git a/core/src/commonMain/kotlin/com/powersync/sync/SyncDataBatch.kt b/core/src/commonMain/kotlin/com/powersync/sync/SyncDataBatch.kt index 37b25b27..5aa87dfc 100644 --- a/core/src/commonMain/kotlin/com/powersync/sync/SyncDataBatch.kt +++ b/core/src/commonMain/kotlin/com/powersync/sync/SyncDataBatch.kt @@ -3,4 +3,6 @@ package com.powersync.sync import kotlinx.serialization.Serializable @Serializable -internal data class SyncDataBatch(val buckets: List) \ No newline at end of file +internal data class SyncDataBatch( + val buckets: List, +) diff --git a/core/src/commonMain/kotlin/com/powersync/sync/SyncDataBucket.kt b/core/src/commonMain/kotlin/com/powersync/sync/SyncDataBucket.kt index 82039bbe..b5fd63e6 100644 --- a/core/src/commonMain/kotlin/com/powersync/sync/SyncDataBucket.kt +++ b/core/src/commonMain/kotlin/com/powersync/sync/SyncDataBucket.kt @@ -5,10 +5,10 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -internal data class SyncDataBucket ( +internal data class SyncDataBucket( val bucket: String, val data: List, @SerialName("has_more") val hasMore: Boolean = false, val after: String?, - @SerialName("next_after")val nextAfter: String? -) \ No newline at end of file + @SerialName("next_after")val nextAfter: String?, +) diff --git a/core/src/commonMain/kotlin/com/powersync/sync/SyncLocalDatabaseResult.kt b/core/src/commonMain/kotlin/com/powersync/sync/SyncLocalDatabaseResult.kt index deb68c67..2de883d5 100644 --- a/core/src/commonMain/kotlin/com/powersync/sync/SyncLocalDatabaseResult.kt +++ b/core/src/commonMain/kotlin/com/powersync/sync/SyncLocalDatabaseResult.kt @@ -7,8 +7,7 @@ import kotlinx.serialization.Serializable internal data class SyncLocalDatabaseResult( var ready: Boolean = true, @SerialName("valid") val checkpointValid: Boolean = true, - @SerialName("failed_buckets") val checkpointFailures: List? = null + @SerialName("failed_buckets") val checkpointFailures: List? = null, ) { - override fun toString() = - "SyncLocalDatabaseResult" -} \ No newline at end of file + override fun toString() = "SyncLocalDatabaseResult" +} diff --git a/core/src/commonMain/kotlin/com/powersync/sync/SyncStatus.kt b/core/src/commonMain/kotlin/com/powersync/sync/SyncStatus.kt index b20ebf10..3377a43b 100644 --- a/core/src/commonMain/kotlin/com/powersync/sync/SyncStatus.kt +++ b/core/src/commonMain/kotlin/com/powersync/sync/SyncStatus.kt @@ -1,9 +1,9 @@ package com.powersync.sync +import com.powersync.connectors.PowerSyncBackendConnector import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow -import com.powersync.connectors.PowerSyncBackendConnector import kotlinx.datetime.Instant public interface SyncStatusData { @@ -81,18 +81,15 @@ internal data class SyncStatusDataContainer( get() = downloadError ?: uploadError } - public data class SyncStatus internal constructor( - private var data: SyncStatusDataContainer = SyncStatusDataContainer() + private var data: SyncStatusDataContainer = SyncStatusDataContainer(), ) : SyncStatusData { private val stateFlow: MutableStateFlow = MutableStateFlow(data) /** * @returns a flow which emits whenever the sync status has changed */ - public fun asFlow(): SharedFlow { - return stateFlow.asSharedFlow() - } + public fun asFlow(): SharedFlow = stateFlow.asSharedFlow() /** * Updates the internal sync status indicators and emits Flow updates @@ -109,16 +106,17 @@ public data class SyncStatus internal constructor( clearUploadError: Boolean = false, clearDownloadError: Boolean = false, ) { - data = data.copy( - connected = connected ?: data.connected, - connecting = connecting ?: data.connecting, - downloading = downloading ?: data.downloading, - uploading = uploading ?: data.uploading, - lastSyncedAt = lastSyncedAt ?: data.lastSyncedAt, - hasSynced = hasSynced ?: data.hasSynced, - uploadError = if (clearUploadError) null else uploadError, - downloadError = if (clearDownloadError) null else downloadError, - ) + data = + data.copy( + connected = connected ?: data.connected, + connecting = connecting ?: data.connecting, + downloading = downloading ?: data.downloading, + uploading = uploading ?: data.uploading, + lastSyncedAt = lastSyncedAt ?: data.lastSyncedAt, + hasSynced = hasSynced ?: data.hasSynced, + uploadError = if (clearUploadError) null else uploadError, + downloadError = if (clearDownloadError) null else downloadError, + ) stateFlow.value = data } @@ -149,9 +147,8 @@ public data class SyncStatus internal constructor( override val downloadError: Any? get() = data.downloadError - override fun toString(): String { - return "SyncStatus(connected=$connected, connecting=$connecting, downloading=$downloading, uploading=$uploading, lastSyncedAt=$lastSyncedAt, hasSynced=$hasSynced, error=$anyError)" - } + override fun toString(): String = + "SyncStatus(connected=$connected, connecting=$connecting, downloading=$downloading, uploading=$uploading, lastSyncedAt=$lastSyncedAt, hasSynced=$hasSynced, error=$anyError)" public companion object { public fun empty(): SyncStatus = SyncStatus() diff --git a/core/src/commonMain/kotlin/com/powersync/sync/SyncStream.kt b/core/src/commonMain/kotlin/com/powersync/sync/SyncStream.kt index 3940282c..bd4dfd64 100644 --- a/core/src/commonMain/kotlin/com/powersync/sync/SyncStream.kt +++ b/core/src/commonMain/kotlin/com/powersync/sync/SyncStream.kt @@ -1,12 +1,12 @@ package com.powersync.sync import co.touchlab.kermit.Logger +import co.touchlab.stately.concurrency.AtomicBoolean import com.powersync.bucket.BucketChecksum import com.powersync.bucket.BucketRequest +import com.powersync.bucket.BucketStorage import com.powersync.bucket.Checkpoint import com.powersync.bucket.WriteCheckpointResponse -import co.touchlab.stately.concurrency.AtomicBoolean -import com.powersync.bucket.BucketStorage import com.powersync.connectors.PowerSyncBackendConnector import com.powersync.db.crud.CrudEntry import com.powersync.utils.JsonUtil @@ -24,7 +24,8 @@ import io.ktor.http.ContentType import io.ktor.http.HttpHeaders import io.ktor.http.HttpStatusCode import io.ktor.http.contentType -import io.ktor.utils.io.* +import io.ktor.utils.io.ByteReadChannel +import io.ktor.utils.io.readUTF8Line import kotlinx.coroutines.CancellationException import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow @@ -34,8 +35,8 @@ import kotlinx.serialization.encodeToString import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive -import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.decodeFromJsonElement +import kotlinx.serialization.json.jsonObject internal class SyncStream( private val bucketStorage: BucketStorage, @@ -43,7 +44,7 @@ internal class SyncStream( private val uploadCrud: suspend () -> Unit, private val retryDelayMs: Long = 5000L, private val logger: Logger, - private val params: JsonObject + private val params: JsonObject, ) { private var isUploadingCrud = AtomicBoolean(false) @@ -54,38 +55,28 @@ internal class SyncStream( private var clientId: String? = null - private val httpClient: HttpClient = HttpClient { - install(HttpTimeout) - install(ContentNegotiation) - } + private val httpClient: HttpClient = + HttpClient { + install(HttpTimeout) + install(ContentNegotiation) + } companion object { - fun isStreamingSyncData(obj: JsonObject): Boolean { - return obj.containsKey("data") - } + fun isStreamingSyncData(obj: JsonObject): Boolean = obj.containsKey("data") - fun isStreamingKeepAlive(obj: JsonObject): Boolean { - return obj.containsKey("token_expires_in") - } + fun isStreamingKeepAlive(obj: JsonObject): Boolean = obj.containsKey("token_expires_in") - fun isStreamingSyncCheckpoint(obj: JsonObject): Boolean { - return obj.containsKey("checkpoint") - } + fun isStreamingSyncCheckpoint(obj: JsonObject): Boolean = obj.containsKey("checkpoint") - fun isStreamingSyncCheckpointComplete(obj: JsonObject): Boolean { - return obj.containsKey("checkpoint_complete") - } + fun isStreamingSyncCheckpointComplete(obj: JsonObject): Boolean = obj.containsKey("checkpoint_complete") - fun isStreamingSyncCheckpointDiff(obj: JsonObject): Boolean { - return obj.containsKey("checkpoint_diff") - } + fun isStreamingSyncCheckpointDiff(obj: JsonObject): Boolean = obj.containsKey("checkpoint_diff") } fun invalidateCredentials() { connector.invalidateCredentials() } - suspend fun streamingSync() { var invalidCredentials = false clientId = bucketStorage.getClientId() @@ -105,13 +96,13 @@ internal class SyncStream( // break; // } } catch (e: Exception) { - //If the coroutine was cancelled, don't log an error - if(e !is CancellationException) { + // If the coroutine was cancelled, don't log an error + if (e !is CancellationException) { logger.e(e) { "Error in streamingSync" } } invalidCredentials = true status.update( - downloadError = e + downloadError = e, ) } finally { status.update( @@ -135,7 +126,7 @@ internal class SyncStream( private suspend fun uploadAllCrud() { var checkedCrudItem: CrudEntry? = null - + while (true) { status.update(uploading = true) /** @@ -148,14 +139,14 @@ internal class SyncStream( logger.w( """Potentially previously uploaded CRUD entries are still present in the upload queue. Make sure to handle uploads and complete CRUD transactions or batches by calling and awaiting their [.complete()] method. - The next upload iteration will be delayed.""" + The next upload iteration will be delayed.""", ) throw Exception("Delaying due to previously encountered CRUD item.") } - checkedCrudItem = nextCrudItem - uploadCrud() - } else { + checkedCrudItem = nextCrudItem + uploadCrud() + } else { // Uploading is completed bucketStorage.updateLocalTarget { getWriteCheckpoint() } break @@ -175,13 +166,14 @@ internal class SyncStream( require(credentials != null) { "Not logged in" } val uri = credentials.endpointUri("write-checkpoint2.json?client_id=$clientId") - val response = httpClient.get(uri) { - contentType(ContentType.Application.Json) - headers { - append(HttpHeaders.Authorization, "Token ${credentials.token}") - append("User-Id", credentials.userId ?: "") + val response = + httpClient.get(uri) { + contentType(ContentType.Application.Json) + headers { + append(HttpHeaders.Authorization, "Token ${credentials.token}") + append("User-Id", credentials.userId ?: "") + } } - } if (response.status.value == 401) { connector.invalidateCredentials() } @@ -193,67 +185,70 @@ internal class SyncStream( return body.data.writeCheckpoint } - private suspend fun streamingSyncRequest(req: StreamingSyncRequest): Flow = flow { - val credentials = connector.getCredentialsCached() - require(credentials != null) { "Not logged in" } + private suspend fun streamingSyncRequest(req: StreamingSyncRequest): Flow = + flow { + val credentials = connector.getCredentialsCached() + require(credentials != null) { "Not logged in" } - val uri = credentials.endpointUri("sync/stream") + val uri = credentials.endpointUri("sync/stream") - val bodyJson = JsonUtil.json.encodeToString(req) + val bodyJson = JsonUtil.json.encodeToString(req) - val request = httpClient.preparePost(uri) { - contentType(ContentType.Application.Json) - headers { - append(HttpHeaders.Authorization, "Token ${credentials.token}") - append("User-Id", credentials.userId ?: "") - } - timeout { socketTimeoutMillis = Long.MAX_VALUE } - setBody(bodyJson) - } + val request = + httpClient.preparePost(uri) { + contentType(ContentType.Application.Json) + headers { + append(HttpHeaders.Authorization, "Token ${credentials.token}") + append("User-Id", credentials.userId ?: "") + } + timeout { socketTimeoutMillis = Long.MAX_VALUE } + setBody(bodyJson) + } - request.execute { httpResponse -> - if (httpResponse.status.value == 401) { - connector.invalidateCredentials() - } + request.execute { httpResponse -> + if (httpResponse.status.value == 401) { + connector.invalidateCredentials() + } - if (httpResponse.status != HttpStatusCode.OK) { - throw RuntimeException("Received error when connecting to sync stream: ${httpResponse.bodyAsText()}") - } + if (httpResponse.status != HttpStatusCode.OK) { + throw RuntimeException("Received error when connecting to sync stream: ${httpResponse.bodyAsText()}") + } - status.update(connected = true, connecting = false) - val channel: ByteReadChannel = httpResponse.body() + status.update(connected = true, connecting = false) + val channel: ByteReadChannel = httpResponse.body() - while (!channel.isClosedForRead) { - val line = channel.readUTF8Line() - if (line != null) { - emit(line) + while (!channel.isClosedForRead) { + val line = channel.readUTF8Line() + if (line != null) { + emit(line) + } } } } - } private suspend fun streamingSyncIteration(): SyncStreamState { - val bucketEntries = bucketStorage.getBucketStates() val initialBuckets = mutableMapOf() - val state = SyncStreamState( - targetCheckpoint = null, - validatedCheckpoint = null, - appliedCheckpoint = null, - bucketSet = initialBuckets.keys.toMutableSet(), - retry = false - ) + val state = + SyncStreamState( + targetCheckpoint = null, + validatedCheckpoint = null, + appliedCheckpoint = null, + bucketSet = initialBuckets.keys.toMutableSet(), + retry = false, + ) bucketEntries.forEach { entry -> initialBuckets[entry.bucket] = entry.opId } - val req = StreamingSyncRequest( - buckets = initialBuckets.map { (bucket, after) -> BucketRequest(bucket, after) }, - clientId = clientId!!, - parameters = params - ) + val req = + StreamingSyncRequest( + buckets = initialBuckets.map { (bucket, after) -> BucketRequest(bucket, after) }, + clientId = clientId!!, + parameters = params, + ) streamingSyncRequest(req).collect { value -> handleInstruction(value, state) @@ -264,19 +259,19 @@ internal class SyncStream( private suspend fun handleInstruction( jsonString: String, - state: SyncStreamState + state: SyncStreamState, ): SyncStreamState { val obj = JsonUtil.json.parseToJsonElement(jsonString).jsonObject // TODO: Clean up when { isStreamingSyncCheckpoint(obj) -> return handleStreamingSyncCheckpoint(obj, state) isStreamingSyncCheckpointComplete(obj) -> return handleStreamingSyncCheckpointComplete( - state + state, ) isStreamingSyncCheckpointDiff(obj) -> return handleStreamingSyncCheckpointDiff( obj, - state + state, ) isStreamingSyncData(obj) -> return handleStreamingSyncData(obj, state) @@ -290,7 +285,7 @@ internal class SyncStream( private suspend fun handleStreamingSyncCheckpoint( jsonObj: JsonObject, - state: SyncStreamState + state: SyncStreamState, ): SyncStreamState { val checkpoint = JsonUtil.json.decodeFromJsonElement(jsonObj["checkpoint"] as JsonElement) @@ -316,9 +311,7 @@ internal class SyncStream( return state } - private suspend fun handleStreamingSyncCheckpointComplete( - state: SyncStreamState - ): SyncStreamState { + private suspend fun handleStreamingSyncCheckpointComplete(state: SyncStreamState): SyncStreamState { val result = bucketStorage.syncLocalDatabase(state.targetCheckpoint!!) if (!result.checkpointValid) { // This means checksums failed. Start again with a new checkpoint. @@ -344,7 +337,7 @@ internal class SyncStream( private suspend fun handleStreamingSyncCheckpointDiff( jsonObj: JsonObject, - state: SyncStreamState + state: SyncStreamState, ): SyncStreamState { // TODO: It may be faster to just keep track of the diff, instead of the entire checkpoint if (state.targetCheckpoint == null) { @@ -364,11 +357,12 @@ internal class SyncStream( checkpointDiff.removedBuckets.forEach { bucket -> newBuckets.remove(bucket) } - val newCheckpoint = Checkpoint( - lastOpId = checkpointDiff.lastOpId, - checksums = newBuckets.values.toList(), - writeCheckpoint = checkpointDiff.writeCheckpoint - ) + val newCheckpoint = + Checkpoint( + lastOpId = checkpointDiff.lastOpId, + checksums = newBuckets.values.toList(), + writeCheckpoint = checkpointDiff.writeCheckpoint, + ) state.targetCheckpoint = newCheckpoint @@ -386,7 +380,7 @@ internal class SyncStream( private suspend fun handleStreamingSyncData( jsonObj: JsonObject, - state: SyncStreamState + state: SyncStreamState, ): SyncStreamState { val syncBuckets = listOf(JsonUtil.json.decodeFromJsonElement(jsonObj["data"] as JsonElement)) @@ -398,7 +392,7 @@ internal class SyncStream( private suspend fun handleStreamingKeepAlive( jsonObj: JsonObject, - state: SyncStreamState + state: SyncStreamState, ): SyncStreamState { val tokenExpiresIn = (jsonObj["token_expires_in"] as JsonPrimitive).content.toInt() @@ -412,14 +406,12 @@ internal class SyncStream( triggerCrudUpload() return state } - } internal data class SyncStreamState( var targetCheckpoint: Checkpoint?, var validatedCheckpoint: Checkpoint?, var appliedCheckpoint: Checkpoint?, - var bucketSet: MutableSet?, var retry: Boolean, ) diff --git a/core/src/commonMain/kotlin/com/powersync/utils/Json.kt b/core/src/commonMain/kotlin/com/powersync/utils/Json.kt index d8ddac8f..faac801f 100644 --- a/core/src/commonMain/kotlin/com/powersync/utils/Json.kt +++ b/core/src/commonMain/kotlin/com/powersync/utils/Json.kt @@ -11,34 +11,55 @@ import kotlinx.serialization.json.JsonPrimitive * A global instance of a JSON serializer. */ internal object JsonUtil { - val json = Json { - encodeDefaults = true - ignoreUnknownKeys = true - } + val json = + Json { + encodeDefaults = true + ignoreUnknownKeys = true + } } public sealed class JsonParam { - public data class Number(val value: kotlin.Number) : JsonParam() - public data class String(val value: kotlin.String) : JsonParam() - public data class Boolean(val value: kotlin.Boolean) : JsonParam() - public data class Map(val value: kotlin.collections.Map) : JsonParam() - public data class Collection(val value: kotlin.collections.Collection) : JsonParam() - public data class JsonElement(val value: kotlinx.serialization.json.JsonElement) : JsonParam() + public data class Number( + val value: kotlin.Number, + ) : JsonParam() + + public data class String( + val value: kotlin.String, + ) : JsonParam() + + public data class Boolean( + val value: kotlin.Boolean, + ) : JsonParam() + + public data class Map( + val value: kotlin.collections.Map, + ) : JsonParam() + + public data class Collection( + val value: kotlin.collections.Collection, + ) : JsonParam() + + public data class JsonElement( + val value: kotlinx.serialization.json.JsonElement, + ) : JsonParam() + public data object Null : JsonParam() - internal fun toJsonElement(): kotlinx.serialization.json.JsonElement = when (this) { - is Number -> JsonPrimitive(value) - is String -> JsonPrimitive(value) - is Boolean -> JsonPrimitive(value) - is Map -> JsonObject(value.mapValues { it.value.toJsonElement() }) - is Collection -> JsonArray(value.map { it.toJsonElement() }) - is JsonElement -> value - Null -> JsonNull - } + internal fun toJsonElement(): kotlinx.serialization.json.JsonElement = + when (this) { + is Number -> JsonPrimitive(value) + is String -> JsonPrimitive(value) + is Boolean -> JsonPrimitive(value) + is Map -> JsonObject(value.mapValues { it.value.toJsonElement() }) + is Collection -> JsonArray(value.map { it.toJsonElement() }) + is JsonElement -> value + Null -> JsonNull + } } -public fun Map.toJsonObject(): JsonObject { - return JsonObject(this.mapValues { (_, value) -> - value?.toJsonElement() ?: JsonNull - }) -} \ No newline at end of file +public fun Map.toJsonObject(): JsonObject = + JsonObject( + this.mapValues { (_, value) -> + value?.toJsonElement() ?: JsonNull + }, + ) diff --git a/core/src/commonMain/kotlin/com/powersync/utils/Log.kt b/core/src/commonMain/kotlin/com/powersync/utils/Log.kt index 284945f6..53a1eb4e 100644 --- a/core/src/commonMain/kotlin/com/powersync/utils/Log.kt +++ b/core/src/commonMain/kotlin/com/powersync/utils/Log.kt @@ -8,17 +8,17 @@ import co.touchlab.kermit.Severity * if no Logger is provided. */ public fun generateLogger(logger: Logger?): Logger { - if(logger != null) { + if (logger != null) { return logger } val defaultLogger: Logger = Logger - if(BuildConfig.isDebug) { + if (BuildConfig.isDebug) { Logger.setMinSeverity(Severity.Verbose) } else { Logger.setMinSeverity(Severity.Warn) } return defaultLogger -} \ No newline at end of file +} diff --git a/core/src/commonMain/kotlin/com/powersync/utils/Strings.kt b/core/src/commonMain/kotlin/com/powersync/utils/Strings.kt index 6ecfab57..f5c04dde 100644 --- a/core/src/commonMain/kotlin/com/powersync/utils/Strings.kt +++ b/core/src/commonMain/kotlin/com/powersync/utils/Strings.kt @@ -1,7 +1,5 @@ package com.powersync.utils internal object Strings { - fun quoteIdentifier(s: String): String { - return "\"${s.replace("\"", "\"\"")}\"" - } -} \ No newline at end of file + fun quoteIdentifier(s: String): String = "\"${s.replace("\"", "\"\"")}\"" +} diff --git a/core/src/commonTest/kotlin/com/powersync/bucket/BucketStorageTest.kt b/core/src/commonTest/kotlin/com/powersync/bucket/BucketStorageTest.kt index 4284a1b6..946b8580 100644 --- a/core/src/commonTest/kotlin/com/powersync/bucket/BucketStorageTest.kt +++ b/core/src/commonTest/kotlin/com/powersync/bucket/BucketStorageTest.kt @@ -1,7 +1,6 @@ -import kotlin.test.* -import com.powersync.bucket.BucketStorageImpl import co.touchlab.kermit.Logger import com.powersync.bucket.BucketState +import com.powersync.bucket.BucketStorageImpl import com.powersync.db.crud.CrudEntry import com.powersync.db.crud.UpdateType import com.powersync.db.internal.InternalDatabase @@ -12,6 +11,11 @@ import dev.mokkery.matcher.any import dev.mokkery.mock import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertTrue class BucketStorageTest { private lateinit var bucketStorage: BucketStorageImpl @@ -19,128 +23,162 @@ class BucketStorageTest { @Test fun testGetMaxOpId() { - mockDb = mock() { - every { getExistingTableNames("ps_data_*") } returns listOf("list_1", "list_2") - } + mockDb = + mock { + every { getExistingTableNames("ps_data_*") } returns listOf("list_1", "list_2") + } bucketStorage = BucketStorageImpl(mockDb, Logger) assertEquals("9223372036854775807", bucketStorage.getMaxOpId()) } @Test - fun testGetClientId() = runTest { - mockDb = mock() { - every { getExistingTableNames("ps_data_*") } returns listOf("list_1", "list_2") - everySuspend { getOptional( - any(), - any(), - any() - )} returns "test-client-id" + fun testGetClientId() = + runTest { + mockDb = + mock { + every { getExistingTableNames("ps_data_*") } returns listOf("list_1", "list_2") + everySuspend { + getOptional( + any(), + any(), + any(), + ) + } returns "test-client-id" + } + bucketStorage = BucketStorageImpl(mockDb, Logger) + val clientId = bucketStorage.getClientId() + assertEquals("test-client-id", clientId) } - bucketStorage = BucketStorageImpl(mockDb, Logger) - val clientId = bucketStorage.getClientId() - assertEquals("test-client-id", clientId) - } @Test - fun testGetClientIdThrowsException() = runTest { - mockDb = mock() { - every { getExistingTableNames("ps_data_*") } returns listOf("list_1", "list_2") - everySuspend { getOptional( - any(), - any(), - any() - )} returns null + fun testGetClientIdThrowsException() = + runTest { + mockDb = + mock { + every { getExistingTableNames("ps_data_*") } returns listOf("list_1", "list_2") + everySuspend { + getOptional( + any(), + any(), + any(), + ) + } returns null + } + bucketStorage = BucketStorageImpl(mockDb, Logger) + + assertFailsWith { + bucketStorage.getClientId() + } } - bucketStorage = BucketStorageImpl(mockDb, Logger) - - assertFailsWith { - bucketStorage.getClientId() - } - } @Test - fun testNextCrudItem() = runTest { - val mockCrudEntry = CrudEntry(id = "1", clientId = 1, op = UpdateType.PUT, table = "table1", transactionId = 1, opData = mapOf("key" to "value")) - mockDb = mock() { - every { getExistingTableNames("ps_data_*") } returns listOf("list_1", "list_2") - everySuspend { getOptional(any(),any(), any()) } returns mockCrudEntry + fun testNextCrudItem() = + runTest { + val mockCrudEntry = + CrudEntry( + id = "1", + clientId = 1, + op = UpdateType.PUT, + table = "table1", + transactionId = 1, + opData = + mapOf( + "key" to "value", + ), + ) + mockDb = + mock { + every { getExistingTableNames("ps_data_*") } returns listOf("list_1", "list_2") + everySuspend { getOptional(any(), any(), any()) } returns mockCrudEntry + } + bucketStorage = BucketStorageImpl(mockDb, Logger) + + val result = bucketStorage.nextCrudItem() + assertEquals(mockCrudEntry, result) } - bucketStorage = BucketStorageImpl(mockDb, Logger) - - - val result = bucketStorage.nextCrudItem() - assertEquals(mockCrudEntry, result) - } @Test - fun testNullNextCrudItem() = runTest { - mockDb = mock() { - every { getExistingTableNames("ps_data_*") } returns listOf("list_1", "list_2") - everySuspend { getOptional(any(),any(), any()) } returns null + fun testNullNextCrudItem() = + runTest { + mockDb = + mock { + every { getExistingTableNames("ps_data_*") } returns listOf("list_1", "list_2") + everySuspend { getOptional(any(), any(), any()) } returns null + } + bucketStorage = BucketStorageImpl(mockDb, Logger) + + val result = bucketStorage.nextCrudItem() + assertEquals(null, result) } - bucketStorage = BucketStorageImpl(mockDb, Logger) - - - val result = bucketStorage.nextCrudItem() - assertEquals(null, result) - } @Test - fun testHasCrud() = runTest { - mockDb = mock() { - every { getExistingTableNames("ps_data_*") } returns listOf("list_1", "list_2") - everySuspend { getOptional(any(),any(), any()) } returns 1L + fun testHasCrud() = + runTest { + mockDb = + mock { + every { getExistingTableNames("ps_data_*") } returns listOf("list_1", "list_2") + everySuspend { getOptional(any(), any(), any()) } returns 1L + } + bucketStorage = BucketStorageImpl(mockDb, Logger) + + assertTrue(bucketStorage.hasCrud()) } - bucketStorage = BucketStorageImpl(mockDb, Logger) - - assertTrue(bucketStorage.hasCrud()) - } @Test - fun testNullHasCrud() = runTest { - mockDb = mock() { - every { getExistingTableNames("ps_data_*") } returns listOf("list_1", "list_2") - everySuspend { getOptional(any(),any(), any()) } returns null + fun testNullHasCrud() = + runTest { + mockDb = + mock { + every { getExistingTableNames("ps_data_*") } returns listOf("list_1", "list_2") + everySuspend { getOptional(any(), any(), any()) } returns null + } + bucketStorage = BucketStorageImpl(mockDb, Logger) + + assertFalse(bucketStorage.hasCrud()) } - bucketStorage = BucketStorageImpl(mockDb, Logger) - - assertFalse(bucketStorage.hasCrud()) - } @Test - fun testUpdateLocalTarget() = runBlocking { - mockDb = mock() { - every { getExistingTableNames("ps_data_*") } returns listOf("list_1", "list_2") - everySuspend { getOptional( - any(), - any(), - any() - )} returns 1L - everySuspend { writeTransaction(any()) } returns true + fun testUpdateLocalTarget() = + runBlocking { + mockDb = + mock { + every { getExistingTableNames("ps_data_*") } returns listOf("list_1", "list_2") + everySuspend { + getOptional( + any(), + any(), + any(), + ) + } returns 1L + everySuspend { writeTransaction(any()) } returns true + } + bucketStorage = BucketStorageImpl(mockDb, Logger) + + val result = bucketStorage.updateLocalTarget { "new-checkpoint" } + assertTrue(result) } - bucketStorage = BucketStorageImpl(mockDb, Logger) - - val result = bucketStorage.updateLocalTarget { "new-checkpoint" } - assertTrue(result) - } @Test - fun testGetBucketStates() = runTest { - val mockBucketStates = listOf(BucketState("bucket1", "op1"), BucketState("bucket2", "op2")) - mockDb = mock() { - every { getExistingTableNames("ps_data_*") } returns listOf("list_1", "list_2") - everySuspend { getOptional( - any(), - any(), - any() - )} returns 1L - everySuspend { getAll(any(), any(), any()) } returns mockBucketStates + fun testGetBucketStates() = + runTest { + val mockBucketStates = listOf(BucketState("bucket1", "op1"), BucketState("bucket2", "op2")) + mockDb = + mock { + every { getExistingTableNames("ps_data_*") } returns listOf("list_1", "list_2") + everySuspend { + getOptional( + any(), + any(), + any(), + ) + } returns 1L + everySuspend { getAll(any(), any(), any()) } returns mockBucketStates + } + bucketStorage = BucketStorageImpl(mockDb, Logger) + + val result = bucketStorage.getBucketStates() + assertEquals(mockBucketStates, result) } - bucketStorage = BucketStorageImpl(mockDb, Logger) - - val result = bucketStorage.getBucketStates() - assertEquals(mockBucketStates, result) - } // TODO: Add tests for removeBuckets, hasCompletedSync, syncLocalDatabase currently not covered because // currently the internal methods are private and cannot be accessed from the test class diff --git a/core/src/commonTest/kotlin/com/powersync/db/schema/SchemaTest.kt b/core/src/commonTest/kotlin/com/powersync/db/schema/SchemaTest.kt index 0c4d3ec7..81cce024 100644 --- a/core/src/commonTest/kotlin/com/powersync/db/schema/SchemaTest.kt +++ b/core/src/commonTest/kotlin/com/powersync/db/schema/SchemaTest.kt @@ -1,9 +1,10 @@ package com.powersync.db.schema -import kotlin.test.* +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith class SchemaTest { - @Test fun schemaConstructionWithValidTablesShouldSucceed() { val table1 = Table("table1", listOf(Column("column1", ColumnType.TEXT))) @@ -21,9 +22,10 @@ class SchemaTest { val validTable = Table("validTable", listOf(Column("column1", ColumnType.TEXT))) val invalidTable = Table("#invalid-table", listOf(Column("column1", ColumnType.TEXT))) - val exception = assertFailsWith { - Schema(validTable, invalidTable) - } + val exception = + assertFailsWith { + Schema(validTable, invalidTable) + } assertEquals(exception.message, "Invalid characters in table name: #invalid-table") } @@ -32,9 +34,10 @@ class SchemaTest { val table1 = Table("table1", listOf(Column("column1", ColumnType.TEXT))) val table2 = Table("table1", listOf(Column("column2", ColumnType.INTEGER))) - val exception = assertFailsWith { - Schema(table1, table2) - } + val exception = + assertFailsWith { + Schema(table1, table2) + } assertEquals(exception.message, "Duplicate table name: table1") } -} \ No newline at end of file +} diff --git a/core/src/commonTest/kotlin/com/powersync/db/schema/TableTest.kt b/core/src/commonTest/kotlin/com/powersync/db/schema/TableTest.kt index 0fa0c16f..df9b01f2 100644 --- a/core/src/commonTest/kotlin/com/powersync/db/schema/TableTest.kt +++ b/core/src/commonTest/kotlin/com/powersync/db/schema/TableTest.kt @@ -1,15 +1,19 @@ package com.powersync.db.schema -import kotlin.test.* +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertTrue class TableTest { - @Test fun testTableInitialization() { - val columns = listOf( - Column("name", ColumnType.TEXT), - Column("age", ColumnType.INTEGER) - ) + val columns = + listOf( + Column("name", ColumnType.TEXT), + Column("age", ColumnType.INTEGER), + ) val table = Table("users", columns) assertEquals("users", table.name) @@ -37,10 +41,11 @@ class TableTest { @Test fun testColumnRetrieval() { - val columns = listOf( - Column("name", ColumnType.TEXT), - Column("age", ColumnType.INTEGER) - ) + val columns = + listOf( + Column("name", ColumnType.TEXT), + Column("age", ColumnType.INTEGER), + ) val table = Table("users", columns) assertEquals(ColumnType.TEXT, table["name"].type) @@ -60,7 +65,7 @@ class TableTest { val columns = listOf(Column("name", ColumnType.TEXT)) val table = Table("valid_table_name", columns, viewNameOverride = "new_view_name") - assertEquals(table.viewName,"new_view_name") + assertEquals(table.viewName, "new_view_name") } @Test @@ -73,10 +78,11 @@ class TableTest { @Test fun testValidation() { - val columns = listOf( - Column("name", ColumnType.TEXT), - Column("age", ColumnType.INTEGER) - ) + val columns = + listOf( + Column("name", ColumnType.TEXT), + Column("age", ColumnType.INTEGER), + ) val table = Table("users", columns) // This should not throw an exception @@ -85,15 +91,17 @@ class TableTest { @Test fun testValidationFailsDuplicateColumn() { - val columns = listOf( - Column("name", ColumnType.TEXT), - Column("name", ColumnType.TEXT) - ) + val columns = + listOf( + Column("name", ColumnType.TEXT), + Column("name", ColumnType.TEXT), + ) val table = Table("users", columns) - val exception = assertFailsWith { - table.validate() - } + val exception = + assertFailsWith { + table.validate() + } assertEquals(exception.message, "Duplicate column users.name") } @@ -102,9 +110,10 @@ class TableTest { val columns = listOf(Column("#invalid-name", ColumnType.TEXT)) val table = Table("users", columns) - val exception = assertFailsWith { - table.validate() - } + val exception = + assertFailsWith { + table.validate() + } assertEquals(exception.message, "Invalid characters in column name: users.#invalid-name") } @@ -113,10 +122,11 @@ class TableTest { val columns = List(64) { Column("column$it", ColumnType.TEXT) } val table = Table("users", columns) - val exception = assertFailsWith { - table.validate() - } - assertEquals(exception.message,"Table users has more than 63 columns, which is not supported") + val exception = + assertFailsWith { + table.validate() + } + assertEquals(exception.message, "Table users has more than 63 columns, which is not supported") } @Test @@ -124,9 +134,10 @@ class TableTest { val columns = listOf(Column("name", ColumnType.TEXT)) val indexes = listOf(Index("idx_age", listOf(IndexedColumn("age")))) - val exception = assertFailsWith { - Table("users", columns, indexes) - } + val exception = + assertFailsWith { + Table("users", columns, indexes) + } assertEquals(exception.message, "Could not find column definition for index idx_age:age") } @@ -136,10 +147,11 @@ class TableTest { val indexes = listOf(Index("#name_index", listOf(IndexedColumn("name")))) val table = Table("users", columns, indexes) - val exception = assertFailsWith { - table.validate() - } - assertEquals(exception.message,"Invalid characters in index name: users.#name_index") + val exception = + assertFailsWith { + table.validate() + } + assertEquals(exception.message, "Invalid characters in index name: users.#name_index") } @Test @@ -148,23 +160,24 @@ class TableTest { val indexes = listOf(Index("name_index", listOf(IndexedColumn("name"))), Index("name_index", listOf(IndexedColumn("name")))) val table = Table("users", columns, indexes) - val exception = assertFailsWith { - table.validate() - } + val exception = + assertFailsWith { + table.validate() + } - assertEquals(exception.message,"Duplicate index users.name_index") + assertEquals(exception.message, "Duplicate index users.name_index") } - @Test fun testValidationOfIdColumn() { val columns = listOf(Column("id", ColumnType.TEXT)) val table = Table("users", columns) - val exception = assertFailsWith { - table.validate() - } + val exception = + assertFailsWith { + table.validate() + } - assertEquals(exception.message,"users: id column is automatically added, custom id columns are not supported") + assertEquals(exception.message, "users: id column is automatically added, custom id columns are not supported") } -} \ No newline at end of file +} diff --git a/core/src/commonTest/kotlin/com/powersync/sync/StreamingSyncRequestTest.kt b/core/src/commonTest/kotlin/com/powersync/sync/StreamingSyncRequestTest.kt index a5c4557a..0d9fb85d 100644 --- a/core/src/commonTest/kotlin/com/powersync/sync/StreamingSyncRequestTest.kt +++ b/core/src/commonTest/kotlin/com/powersync/sync/StreamingSyncRequestTest.kt @@ -4,33 +4,38 @@ import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive -import kotlin.test.* +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue class StreamingSyncRequestTest { - private val json = Json { ignoreUnknownKeys = true } @Test fun testSerialization() { - val request = StreamingSyncRequest( - buckets = listOf(BucketRequest("table1", "op1"), BucketRequest("table2", "op2")), - includeChecksum = true, - clientId = "client123", - parameters = JsonObject(mapOf("param1" to JsonPrimitive("value1"))) - ) + val request = + StreamingSyncRequest( + buckets = listOf(BucketRequest("table1", "op1"), BucketRequest("table2", "op2")), + includeChecksum = true, + clientId = "client123", + parameters = JsonObject(mapOf("param1" to JsonPrimitive("value1"))), + ) val serialized = json.encodeToString(request) - val expected = """ + val expected = + """ {"buckets":[{"name":"table1","after":"op1"},{"name":"table2","after":"op2"}],"client_id":"client123","parameters":{"param1":"value1"}} - """.trimIndent().replace("\n", "") + """.trimIndent().replace("\n", "") assertEquals(expected, serialized) } @Test fun testDeserialization() { - val jsonString = """ + val jsonString = + """ { "buckets": [{"name": "table1", "after": "op1"}, {"name": "table2", "after": "op2"}], "include_checksum": false, @@ -38,7 +43,7 @@ class StreamingSyncRequestTest { "parameters": {"param2": "value2"}, "raw_data": true } - """.trimIndent() + """.trimIndent() val deserialized = json.decodeFromString(jsonString) @@ -49,23 +54,25 @@ class StreamingSyncRequestTest { assertEquals("op2", deserialized.buckets[1].after) assertFalse(deserialized.includeChecksum) assertEquals("client456", deserialized.clientId) - assertEquals(JsonPrimitive("value2"), deserialized.parameters?.get("param2")) + assertEquals(JsonPrimitive("value2"), deserialized.parameters.get("param2")) } @Test fun testDefaultValues() { - val request = StreamingSyncRequest( - buckets = listOf(), - clientId = "client789" - ) + val request = + StreamingSyncRequest( + buckets = listOf(), + clientId = "client789", + ) assertTrue(request.includeChecksum) assertEquals(request.parameters, JsonObject(mapOf())) val serialized = json.encodeToString(request) - val expected = """ + val expected = + """ {"buckets":[],"client_id":"client789"} - """.trimIndent().replace("\n", "") + """.trimIndent().replace("\n", "") assertEquals(serialized, expected) } -} \ No newline at end of file +} diff --git a/core/src/commonTest/kotlin/com/powersync/sync/SyncStreamTest.kt b/core/src/commonTest/kotlin/com/powersync/sync/SyncStreamTest.kt index 06fc6ceb..04b743a9 100644 --- a/core/src/commonTest/kotlin/com/powersync/sync/SyncStreamTest.kt +++ b/core/src/commonTest/kotlin/com/powersync/sync/SyncStreamTest.kt @@ -1,7 +1,5 @@ package com.powersync.sync -import kotlin.test.* -import kotlinx.serialization.json.JsonObject import co.touchlab.kermit.Logger import co.touchlab.kermit.Severity import co.touchlab.kermit.TestConfig @@ -15,21 +13,28 @@ import dev.mokkery.everySuspend import dev.mokkery.mock import dev.mokkery.verify import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.JsonObject +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertContains +import kotlin.test.assertEquals @OptIn(co.touchlab.kermit.ExperimentalKermitApi::class) class SyncStreamTest { private lateinit var bucketStorage: BucketStorage private lateinit var connector: PowerSyncBackendConnector private lateinit var syncStream: SyncStream - private val testLogWriter = TestLogWriter( - loggable = Severity.Verbose - ) - private val logger = Logger( - TestConfig( - minSeverity = Severity.Debug, - logWriterList = listOf(testLogWriter) + private val testLogWriter = + TestLogWriter( + loggable = Severity.Verbose, + ) + private val logger = + Logger( + TestConfig( + minSeverity = Severity.Debug, + logWriterList = listOf(testLogWriter), + ), ) - ) @BeforeTest fun setup() { @@ -38,56 +43,73 @@ class SyncStreamTest { } @Test - fun testInvalidateCredentials() = runTest { - connector = mock() { - everySuspend { invalidateCredentials() } returns Unit - } + fun testInvalidateCredentials() = + runTest { + connector = + mock { + everySuspend { invalidateCredentials() } returns Unit + } - syncStream = SyncStream( - bucketStorage = bucketStorage, - connector = connector, - uploadCrud = {}, - logger = logger, - params = JsonObject(emptyMap()) - ) + syncStream = + SyncStream( + bucketStorage = bucketStorage, + connector = connector, + uploadCrud = {}, + logger = logger, + params = JsonObject(emptyMap()), + ) - syncStream.invalidateCredentials() - verify { connector.invalidateCredentials() } - } + syncStream.invalidateCredentials() + verify { connector.invalidateCredentials() } + } // TODO: Work on improving testing this without needing to test the logs are displayed @Test - fun testTriggerCrudUploadWhenAlreadyUploading() = runTest { - val mockCrudEntry = CrudEntry(id = "1", clientId = 1, op = UpdateType.PUT, table = "table1", transactionId = 1, opData = mapOf("key" to "value")) - bucketStorage = mock() { - everySuspend { nextCrudItem() } returns mockCrudEntry - } + fun testTriggerCrudUploadWhenAlreadyUploading() = + runTest { + val mockCrudEntry = + CrudEntry( + id = "1", + clientId = 1, + op = UpdateType.PUT, + table = "table1", + transactionId = 1, + opData = + mapOf( + "key" to "value", + ), + ) + bucketStorage = + mock { + everySuspend { nextCrudItem() } returns mockCrudEntry + } - syncStream = SyncStream( - bucketStorage = bucketStorage, - connector = connector, - uploadCrud = { }, - retryDelayMs = 10, - logger = logger, - params = JsonObject(emptyMap()) - ) + syncStream = + SyncStream( + bucketStorage = bucketStorage, + connector = connector, + uploadCrud = { }, + retryDelayMs = 10, + logger = logger, + params = JsonObject(emptyMap()), + ) - syncStream.status.update(connected = true) - syncStream.triggerCrudUpload() + syncStream.status.update(connected = true) + syncStream.triggerCrudUpload() - testLogWriter.assertCount(2) - - with(testLogWriter.logs[0]) { - assertContains( - message, - "Potentially previously uploaded CRUD entries are still present in the upload queue." - ) - assertEquals(Severity.Warn, severity) - } + testLogWriter.assertCount(2) + + with(testLogWriter.logs[0]) { + assertContains( + message, + "Potentially previously uploaded CRUD entries are still present in the upload queue.", + ) + assertEquals(Severity.Warn, severity) + } - with(testLogWriter.logs[1]) { - assertEquals(message,"Error uploading crud") - assertEquals(Severity.Error, severity) + with(testLogWriter.logs[1]) { + assertEquals(message, "Error uploading crud") + assertEquals(Severity.Error, severity) + } } - } -} \ No newline at end of file +} diff --git a/core/src/commonTest/kotlin/com/powersync/utils/JsonTest.kt b/core/src/commonTest/kotlin/com/powersync/utils/JsonTest.kt index af3fd451..0157012f 100644 --- a/core/src/commonTest/kotlin/com/powersync/utils/JsonTest.kt +++ b/core/src/commonTest/kotlin/com/powersync/utils/JsonTest.kt @@ -1,8 +1,23 @@ package com.powersync.utils - -import kotlinx.serialization.json.* -import kotlin.test.* +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.add +import kotlinx.serialization.json.boolean +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.double +import kotlinx.serialization.json.int +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue class JsonTest { @Test @@ -31,10 +46,13 @@ class JsonTest { @Test fun testMapToJsonElement() { - val map = JsonParam.Map(mapOf( - "key1" to JsonParam.String("value1"), - "key2" to JsonParam.Number(42) - )) + val map = + JsonParam.Map( + mapOf( + "key1" to JsonParam.String("value1"), + "key2" to JsonParam.Number(42), + ), + ) val jsonElement = map.toJsonElement() assertTrue(jsonElement is JsonObject) assertEquals("value1", jsonElement["key1"]?.jsonPrimitive?.content) @@ -43,10 +61,13 @@ class JsonTest { @Test fun testListToJsonElement() { - val list = JsonParam.Collection(listOf( - JsonParam.String("item1"), - JsonParam.Number(42) - )) + val list = + JsonParam.Collection( + listOf( + JsonParam.String("item1"), + JsonParam.Number(42), + ), + ) val jsonElement = list.toJsonElement() assertTrue(jsonElement is JsonArray) assertEquals("item1", jsonElement[0].jsonPrimitive.content) @@ -55,9 +76,10 @@ class JsonTest { @Test fun testJsonElementParamToJsonElement() { - val originalJson = buildJsonObject { - put("key", "value") - } + val originalJson = + buildJsonObject { + put("key", "value") + } val jsonElementParam = JsonParam.JsonElement(originalJson) val jsonElement = jsonElementParam.toJsonElement() assertEquals(originalJson, jsonElement) @@ -72,12 +94,13 @@ class JsonTest { @Test fun testMapToJsonObject() { - val params = mapOf( - "string" to JsonParam.String("value"), - "number" to JsonParam.Number(42), - "boolean" to JsonParam.Boolean(true), - "null" to JsonParam.Null - ) + val params = + mapOf( + "string" to JsonParam.String("value"), + "number" to JsonParam.Number(42), + "boolean" to JsonParam.Boolean(true), + "null" to JsonParam.Null, + ) val jsonObject = params.toJsonObject() assertEquals("value", jsonObject["string"]?.jsonPrimitive?.content) assertEquals(42, jsonObject["number"]?.jsonPrimitive?.int) @@ -87,37 +110,57 @@ class JsonTest { @Test fun testComplexNestedMapToJsonObject() { - val complexNestedMap = mapOf( - "string" to JsonParam.String("value"), - "number" to JsonParam.Number(42), - "boolean" to JsonParam.Boolean(true), - "null" to JsonParam.Null, - "nestedMap" to JsonParam.Map(mapOf( - "list" to JsonParam.Collection(listOf( - JsonParam.Number(1), - JsonParam.String("two"), - JsonParam.Boolean(false) - )), - "deeplyNested" to JsonParam.Map(mapOf( - "jsonElement" to JsonParam.JsonElement(buildJsonObject { - put("key", "value") - put("array", buildJsonArray { - add(1) - add("string") - add(true) - }) - }), - "mixedList" to JsonParam.Collection(arrayListOf( - JsonParam.Number(3.14), - JsonParam.Map(mapOf( - "key" to JsonParam.String("nestedValue") - )), - JsonParam.Null - ) - ) - )) - )) - ) + val complexNestedMap = + mapOf( + "string" to JsonParam.String("value"), + "number" to JsonParam.Number(42), + "boolean" to JsonParam.Boolean(true), + "null" to JsonParam.Null, + "nestedMap" to + JsonParam.Map( + mapOf( + "list" to + JsonParam.Collection( + listOf( + JsonParam.Number(1), + JsonParam.String("two"), + JsonParam.Boolean(false), + ), + ), + "deeplyNested" to + JsonParam.Map( + mapOf( + "jsonElement" to + JsonParam.JsonElement( + buildJsonObject { + put("key", "value") + put( + "array", + buildJsonArray { + add(1) + add("string") + add(true) + }, + ) + }, + ), + "mixedList" to + JsonParam.Collection( + arrayListOf( + JsonParam.Number(3.14), + JsonParam.Map( + mapOf( + "key" to JsonParam.String("nestedValue"), + ), + ), + JsonParam.Null, + ), + ), + ), + ), + ), + ), + ) val jsonObject = complexNestedMap.toJsonObject() @@ -164,4 +207,4 @@ class JsonTest { assertEquals("nestedValue", nestedMapInList["key"]?.jsonPrimitive?.content) assertTrue(mixedList[2] is JsonNull) } -} \ No newline at end of file +} diff --git a/core/src/iosMain/kotlin/BuildConfig.kt b/core/src/iosMain/kotlin/BuildConfig.kt index cfb552ff..2d264663 100644 --- a/core/src/iosMain/kotlin/BuildConfig.kt +++ b/core/src/iosMain/kotlin/BuildConfig.kt @@ -4,4 +4,4 @@ import kotlin.native.Platform public actual object BuildConfig { @OptIn(ExperimentalNativeApi::class) public actual val isDebug: Boolean = Platform.isDebugBinary -} \ No newline at end of file +} diff --git a/core/src/iosMain/kotlin/com/powersync/DatabaseDriverFactory.ios.kt b/core/src/iosMain/kotlin/com/powersync/DatabaseDriverFactory.ios.kt index f9eb275b..adb40057 100644 --- a/core/src/iosMain/kotlin/com/powersync/DatabaseDriverFactory.ios.kt +++ b/core/src/iosMain/kotlin/com/powersync/DatabaseDriverFactory.ios.kt @@ -28,7 +28,12 @@ public actual class DatabaseDriverFactory { } @Suppress("unused", "UNUSED_PARAMETER") - private fun updateTableHook(opType: Int, databaseName: String, tableName: String, rowId: Long) { + private fun updateTableHook( + opType: Int, + databaseName: String, + tableName: String, + rowId: Long, + ) { driver?.updateTable(tableName) } @@ -47,25 +52,31 @@ public actual class DatabaseDriverFactory { dbFilename: String, ): PsSqlDriver { val schema = InternalSchema.synchronous() - this.driver = PsSqlDriver(scope = scope, driver = NativeSqliteDriver( - configuration = DatabaseConfiguration( - name = dbFilename, - version = schema.version.toInt(), - create = { connection -> wrapConnection(connection) { schema.create(it) } }, - lifecycleConfig = DatabaseConfiguration.Lifecycle( - onCreateConnection = { connection -> - setupSqliteBinding(connection) - wrapConnection(connection) { driver -> - schema.create(driver) - } - }, - onCloseConnection = { connection -> - deregisterSqliteBinding(connection) - } - ) + this.driver = + PsSqlDriver( + scope = scope, + driver = + NativeSqliteDriver( + configuration = + DatabaseConfiguration( + name = dbFilename, + version = schema.version.toInt(), + create = { connection -> wrapConnection(connection) { schema.create(it) } }, + lifecycleConfig = + DatabaseConfiguration.Lifecycle( + onCreateConnection = { connection -> + setupSqliteBinding(connection) + wrapConnection(connection) { driver -> + schema.create(driver) + } + }, + onCloseConnection = { connection -> + deregisterSqliteBinding(connection) + }, + ), + ), + ), ) - ) - ) return this.driver as PsSqlDriver } @@ -77,16 +88,17 @@ public actual class DatabaseDriverFactory { ptr, staticCFunction { usrPtr, updateType, dbName, tableName, rowId -> val callback = - usrPtr!!.asStableRef<(Int, String, String, Long) -> Unit>() + usrPtr!! + .asStableRef<(Int, String, String, Long) -> Unit>() .get() callback( updateType, dbName!!.toKString(), tableName!!.toKString(), - rowId + rowId, ) }, - StableRef.create(::updateTableHook).asCPointer() + StableRef.create(::updateTableHook).asCPointer(), ) // Register transaction hooks @@ -97,7 +109,7 @@ public actual class DatabaseDriverFactory { callback(true) 0 }, - StableRef.create(::onTransactionCommit).asCPointer() + StableRef.create(::onTransactionCommit).asCPointer(), ) sqlite3_rollback_hook( ptr, @@ -105,7 +117,7 @@ public actual class DatabaseDriverFactory { val callback = usrPtr!!.asStableRef<(Boolean) -> Unit>().get() callback(false) }, - StableRef.create(::onTransactionCommit).asCPointer() + StableRef.create(::onTransactionCommit).asCPointer(), ) } @@ -114,8 +126,7 @@ public actual class DatabaseDriverFactory { sqlite3_update_hook( ptr, null, - null + null, ) } } - diff --git a/dialect/build.gradle b/dialect/build.gradle index 565943b6..5d9300d4 100644 --- a/dialect/build.gradle +++ b/dialect/build.gradle @@ -1,6 +1,7 @@ plugins { alias(libs.plugins.kotlin.jvm) alias(libs.plugins.grammarKitComposer) + alias(libs.plugins.kotlinter) } grammarKit { diff --git a/dialect/src/main/kotlin/com/powersync/sqlite/PowerSyncDialect.kt b/dialect/src/main/kotlin/com/powersync/sqlite/PowerSyncDialect.kt index fcf2858e..c9361db0 100644 --- a/dialect/src/main/kotlin/com/powersync/sqlite/PowerSyncDialect.kt +++ b/dialect/src/main/kotlin/com/powersync/sqlite/PowerSyncDialect.kt @@ -5,19 +5,25 @@ import app.cash.sqldelight.dialect.api.PrimitiveType import app.cash.sqldelight.dialect.api.SqlDelightDialect import app.cash.sqldelight.dialect.api.TypeResolver import app.cash.sqldelight.dialects.sqlite_3_35.SqliteTypeResolver -import app.cash.sqldelight.dialects.sqlite_3_38.SqliteDialect as Sqlite338Dialect import com.alecstrong.sql.psi.core.psi.SqlFunctionExpr +import app.cash.sqldelight.dialects.sqlite_3_38.SqliteDialect as Sqlite338Dialect public class PowerSyncDialect : SqlDelightDialect by Sqlite338Dialect() { override fun typeResolver(parentResolver: TypeResolver): PowerSyncTypeResolver = PowerSyncTypeResolver(parentResolver) } -public class PowerSyncTypeResolver(private val parentResolver: TypeResolver) : - TypeResolver by SqliteTypeResolver(parentResolver) { +public class PowerSyncTypeResolver( + private val parentResolver: TypeResolver, +) : TypeResolver by SqliteTypeResolver(parentResolver) { override fun functionType(functionExpr: SqlFunctionExpr): IntermediateType? { when (functionExpr.functionName.text) { - "sqlite_version", "powersync_rs_version", "powersync_replace_schema", "powersync_clear", "powersync_init" -> return IntermediateType( - PrimitiveType.TEXT + "sqlite_version", + "powersync_rs_version", + "powersync_replace_schema", + "powersync_clear", + "powersync_init", + -> return IntermediateType( + PrimitiveType.TEXT, ) } return parentResolver.functionType(functionExpr) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f3aa4de2..d60bc4e0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -34,6 +34,7 @@ maven-publish = "0.27.0" download-plugin = "5.5.0" grammerKit = "0.1.12" mokkery = "2.4.0" +kotlinter = "4.4.1" # Sample - Android androidx-core = "1.13.1" @@ -107,6 +108,7 @@ grammarKitComposer = { id = "com.alecstrong.grammar.kit.composer", version.ref = mavenPublishPlugin = { id = "com.vanniktech.maven.publish", version.ref = "maven-publish" } downloadPlugin = { id = "de.undercouch.download", version.ref = "download-plugin" } mokkery = { id = "dev.mokkery", version.ref = "mokkery" } +kotlinter = { id = "org.jmailen.kotlinter", version.ref = "kotlinter" } [bundles] sqldelight = [ diff --git a/persistence/build.gradle.kts b/persistence/build.gradle.kts index dff79f8e..977c9e7c 100644 --- a/persistence/build.gradle.kts +++ b/persistence/build.gradle.kts @@ -4,6 +4,7 @@ plugins { alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.sqldelight) alias(libs.plugins.androidLibrary) + alias(libs.plugins.kotlinter) id("com.powersync.plugins.sonatype") } @@ -41,7 +42,6 @@ android { jvmToolchain(17) } - buildFeatures { buildConfig = true } @@ -55,11 +55,17 @@ android { } } defaultConfig { - minSdk = libs.versions.android.minSdk.get().toInt() + minSdk = + libs.versions.android.minSdk + .get() + .toInt() } namespace = "com.powersync.persistence" - compileSdk = libs.versions.android.compileSdk.get().toInt() + compileSdk = + libs.versions.android.compileSdk + .get() + .toInt() } sqldelight { @@ -72,4 +78,12 @@ sqldelight { } } +tasks.formatKotlinCommonMain { + exclude { it.file.path.contains("generated/") } +} + +tasks.lintKotlinCommonMain { + exclude { it.file.path.contains("generated/") } +} + setupGithubRepository() diff --git a/persistence/src/commonMain/kotlin/com/persistence/PsInternalDatabase.kt b/persistence/src/commonMain/kotlin/com/persistence/PsInternalDatabase.kt index 64f438d8..2836dffd 100644 --- a/persistence/src/commonMain/kotlin/com/persistence/PsInternalDatabase.kt +++ b/persistence/src/commonMain/kotlin/com/persistence/PsInternalDatabase.kt @@ -1 +1,4 @@ -package com.persistence \ No newline at end of file +@file:Suppress("ktlint:standard:no-empty-file") +// Need this for the commonMain source set to be recognized + +package com.persistence