diff --git a/CHANGELOG.md b/CHANGELOG.md index 72f06b38..4972f5e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 1.0.0-BETA25 + +* JVM: Lower minimum supported version from 17 to 8. + ## 1.0.0-BETA24 * Improve internal handling of watch queries to avoid issues where updates are not being received due to transaction commits occurring after the query is run. diff --git a/build.gradle.kts b/build.gradle.kts index 7e998244..e80a02d8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -51,6 +51,9 @@ allprojects { exclude(group = "ai.grazie.model") exclude(group = "ai.grazie.utils") exclude(group = "ai.grazie.nlp") + + // We have a transitive dependency on this due to Kermit, but need the fixed version to support Java 8 + resolutionStrategy.force("co.touchlab:stately-collections:${libs.versions.stately.get()}") } // diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 44c29c7e..2dfe928e 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -2,7 +2,10 @@ import app.cash.sqldelight.core.capitalize import com.powersync.plugins.sonatype.setupGithubRepository import de.undercouch.gradle.tasks.download.Download import org.gradle.internal.os.OperatingSystem +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi +import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget +import org.jetbrains.kotlin.gradle.targets.jvm.tasks.KotlinJvmTest import java.util.* plugins { @@ -76,8 +79,20 @@ val buildCInteropDef by tasks.registering { kotlin { androidTarget { publishLibraryVariants("release", "debug") + + @OptIn(ExperimentalKotlinGradlePluginApi::class) + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } + } + jvm { + @OptIn(ExperimentalKotlinGradlePluginApi::class) + compilerOptions { + jvmTarget.set(JvmTarget.JVM_1_8) + // https://jakewharton.com/kotlins-jdk-release-compatibility-flag/ + freeCompilerArgs.add("-Xjdk-release=8") + } } - jvm() iosX64() iosArm64() @@ -147,8 +162,8 @@ kotlin { } android { - kotlin { - jvmToolchain(17) + compileOptions { + targetCompatibility = JavaVersion.VERSION_17 } buildFeatures { @@ -339,6 +354,22 @@ tasks.named(kotlin.jvm().compilations["main"].processResources from(getBinaries, downloadPowersyncDesktopBinaries) } +// We want to build with recent JDKs, but need to make sure we support Java 8. https://jakewharton.com/build-on-latest-java-test-through-lowest-java/ +val testWithJava8 by tasks.registering(KotlinJvmTest::class) { + javaLauncher = javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(8) + } + + description = "Run tests with Java 8" + group = LifecycleBasePlugin.VERIFICATION_GROUP + + // Copy inputs from the normal test task + val testTask = tasks.getByName("jvmTest") as KotlinJvmTest + classpath = testTask.classpath + testClassesDirs = testTask.testClassesDirs +} +tasks.named("check").configure { dependsOn(testWithJava8) } + afterEvaluate { val buildTasks = tasks.matching { diff --git a/core/src/androidMain/kotlin/com/powersync/DatabaseDriverFactory.android.kt b/core/src/androidMain/kotlin/com/powersync/DatabaseDriverFactory.android.kt index 2755591b..090be5e1 100644 --- a/core/src/androidMain/kotlin/com/powersync/DatabaseDriverFactory.android.kt +++ b/core/src/androidMain/kotlin/com/powersync/DatabaseDriverFactory.android.kt @@ -42,38 +42,38 @@ public actual class DatabaseDriverFactory( 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 + 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) + } }, - ), ), - callback = - object : AndroidSqliteDriver.Callback(schema) { - override fun onConfigure(db: SupportSQLiteDatabase) { - db.enableWriteAheadLogging() - super.onConfigure(db) - } - }, - ), ) setupSqliteBinding() return this.driver as PsSqlDriver diff --git a/core/src/commonMain/kotlin/com/powersync/Exceptions.kt b/core/src/commonMain/kotlin/com/powersync/PowerSyncException.kt similarity index 100% rename from core/src/commonMain/kotlin/com/powersync/Exceptions.kt rename to core/src/commonMain/kotlin/com/powersync/PowerSyncException.kt diff --git a/core/src/commonMain/kotlin/com/powersync/sync/SyncStatus.kt b/core/src/commonMain/kotlin/com/powersync/sync/SyncStatus.kt index 3377a43b..698bbbab 100644 --- a/core/src/commonMain/kotlin/com/powersync/sync/SyncStatus.kt +++ b/core/src/commonMain/kotlin/com/powersync/sync/SyncStatus.kt @@ -81,6 +81,7 @@ internal data class SyncStatusDataContainer( get() = downloadError ?: uploadError } +@ConsistentCopyVisibility public data class SyncStatus internal constructor( private var data: SyncStatusDataContainer = SyncStatusDataContainer(), ) : SyncStatusData { diff --git a/core/src/iosMain/kotlin/com/powersync/DatabaseDriverFactory.ios.kt b/core/src/iosMain/kotlin/com/powersync/DatabaseDriverFactory.ios.kt index 1cd80702..a12f7974 100644 --- a/core/src/iosMain/kotlin/com/powersync/DatabaseDriverFactory.ios.kt +++ b/core/src/iosMain/kotlin/com/powersync/DatabaseDriverFactory.ios.kt @@ -75,27 +75,27 @@ public actual class DatabaseDriverFactory { PsSqlDriver( scope = scope, driver = - NativeSqliteDriver( - configuration = - DatabaseConfiguration( - name = dbFilename, - version = schema.version.toInt(), - create = { connection -> wrapConnection(connection) { schema.create(it) } }, - loggingConfig = Logging(logger = sqlLogger), - lifecycleConfig = - DatabaseConfiguration.Lifecycle( - onCreateConnection = { connection -> - setupSqliteBinding(connection) - wrapConnection(connection) { driver -> - schema.create(driver) - } - }, - onCloseConnection = { connection -> - deregisterSqliteBinding(connection) - }, - ), + NativeSqliteDriver( + configuration = + DatabaseConfiguration( + name = dbFilename, + version = schema.version.toInt(), + create = { connection -> wrapConnection(connection) { schema.create(it) } }, + loggingConfig = Logging(logger = sqlLogger), + lifecycleConfig = + DatabaseConfiguration.Lifecycle( + onCreateConnection = { connection -> + setupSqliteBinding(connection) + wrapConnection(connection) { driver -> + schema.create(driver) + } + }, + onCloseConnection = { connection -> + deregisterSqliteBinding(connection) + }, + ), + ), ), - ), ) return this.driver as PsSqlDriver } diff --git a/gradle.properties b/gradle.properties index 4279c08c..e219cfed 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ development=true RELEASE_SIGNING_ENABLED=true # Library config GROUP=com.powersync -LIBRARY_VERSION=1.0.0-BETA24 +LIBRARY_VERSION=1.0.0-BETA25 GITHUB_REPO=https://github.com/powersync-ja/powersync-kotlin.git # POM POM_URL=https://github.com/powersync-ja/powersync-kotlin/ diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2150fcb9..1c8dd30b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,7 +8,7 @@ java = "17" idea = "222.4459.24" # Flamingo | 2022.2.1 (see https://plugins.jetbrains.com/docs/intellij/android-studio-releases-list.html) # Dependencies -kermit = "2.0.4" +kermit = "2.0.5" kotlin = "2.0.20" coroutines = "1.8.1" kotlinx-datetime = "0.5.0" @@ -20,7 +20,7 @@ sqlite-android = "3.45.0" sqlite-jdbc = "3.45.2.0" sqlDelight = "2.0.2" -stately = "2.0.7" +stately = "2.1.0" supabase = "3.0.1" junit = "4.13.2" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b82aa23a..cea7a793 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/persistence/build.gradle.kts b/persistence/build.gradle.kts index 40f5d701..a4fc6f10 100644 --- a/persistence/build.gradle.kts +++ b/persistence/build.gradle.kts @@ -1,4 +1,6 @@ import com.powersync.plugins.sonatype.setupGithubRepository +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi +import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { alias(libs.plugins.kotlinMultiplatform) @@ -11,9 +13,21 @@ plugins { kotlin { androidTarget { publishLibraryVariants("release", "debug") + + @OptIn(ExperimentalKotlinGradlePluginApi::class) + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } } - jvm() + jvm { + @OptIn(ExperimentalKotlinGradlePluginApi::class) + compilerOptions { + jvmTarget.set(JvmTarget.JVM_1_8) + // https://jakewharton.com/kotlins-jdk-release-compatibility-flag/ + freeCompilerArgs.add("-Xjdk-release=8") + } + } iosX64() iosArm64() @@ -45,8 +59,8 @@ kotlin { } android { - kotlin { - jvmToolchain(17) + compileOptions { + targetCompatibility = JavaVersion.VERSION_17 } buildFeatures { diff --git a/persistence/src/androidMain/kotlin/com/powersync/persistence/driver/AndroidSqliteDriver.kt b/persistence/src/androidMain/kotlin/com/powersync/persistence/driver/AndroidSqliteDriver.kt index 3390c9e3..f2627145 100644 --- a/persistence/src/androidMain/kotlin/com/powersync/persistence/driver/AndroidSqliteDriver.kt +++ b/persistence/src/androidMain/kotlin/com/powersync/persistence/driver/AndroidSqliteDriver.kt @@ -63,13 +63,15 @@ public class AndroidSqliteDriver private constructor( windowSizeBytes: Long? = null, ) : this( database = null, - openHelper = factory.create( - SupportSQLiteOpenHelper.Configuration.builder(context) - .callback(callback) - .name(name) - .noBackupDirectory(useNoBackupDirectory) - .build(), - ), + openHelper = + factory.create( + SupportSQLiteOpenHelper.Configuration + .builder(context) + .callback(callback) + .name(name) + .noBackupDirectory(useNoBackupDirectory) + .build(), + ), cacheSize = cacheSize, windowSizeBytes = windowSizeBytes, ) @@ -81,20 +83,24 @@ public class AndroidSqliteDriver private constructor( windowSizeBytes: Long? = null, ) : this(openHelper = null, database = database, cacheSize = cacheSize, windowSizeBytes = windowSizeBytes) - private val statements = object : LruCache(cacheSize) { - override fun entryRemoved( - evicted: Boolean, - key: Int, - oldValue: AndroidStatement, - newValue: AndroidStatement?, - ) { - if (evicted) oldValue.close() + private val statements = + object : LruCache(cacheSize) { + override fun entryRemoved( + evicted: Boolean, + key: Int, + oldValue: AndroidStatement, + newValue: AndroidStatement?, + ) { + if (evicted) oldValue.close() + } } - } private val listeners = linkedMapOf>() - override fun addListener(vararg queryKeys: String, listener: Query.Listener) { + override fun addListener( + vararg queryKeys: String, + listener: Query.Listener, + ) { synchronized(listeners) { queryKeys.forEach { listeners.getOrPut(it, { linkedSetOf() }).add(listener) @@ -102,7 +108,10 @@ public class AndroidSqliteDriver private constructor( } } - override fun removeListener(vararg queryKeys: String, listener: Query.Listener) { + override fun removeListener( + vararg queryKeys: String, + listener: Query.Listener, + ) { synchronized(listeners) { queryKeys.forEach { listeners[it]?.remove(listener) @@ -181,8 +190,7 @@ public class AndroidSqliteDriver private constructor( sql: String, parameters: Int, binders: (SqlPreparedStatement.() -> Unit)?, - ): QueryResult = - execute(identifier, { AndroidPreparedStatement(database.compileStatement(sql)) }, binders, { execute() }) + ): QueryResult = execute(identifier, { AndroidPreparedStatement(database.compileStatement(sql)) }, binders, { execute() }) override fun executeQuery( identifier: Int?, @@ -190,11 +198,12 @@ public class AndroidSqliteDriver private constructor( mapper: (SqlCursor) -> QueryResult, parameters: Int, binders: (SqlPreparedStatement.() -> Unit)?, - ): QueryResult.Value = execute( - identifier, - { AndroidQuery(sql, database, parameters, windowSizeBytes) }, - binders - ) { executeQuery(mapper) } + ): QueryResult.Value = + execute( + identifier, + { AndroidQuery(sql, database, parameters, windowSizeBytes) }, + binders, + ) { executeQuery(mapper) } override fun close() { statements.evictAll() @@ -205,9 +214,14 @@ public class AndroidSqliteDriver private constructor( private val schema: SqlSchema>, private vararg val callbacks: AfterVersion, ) : SupportSQLiteOpenHelper.Callback( - if (schema.version > Int.MAX_VALUE) error("Schema version is larger than Int.MAX_VALUE: ${schema.version}.") else schema.version.toInt(), - ) { - + if (schema.version > + Int.MAX_VALUE + ) { + error("Schema version is larger than Int.MAX_VALUE: ${schema.version}.") + } else { + schema.version.toInt() + }, + ) { override fun onCreate(db: SupportSQLiteDatabase) { schema.create(AndroidSqliteDriver(openHelper = null, database = db, cacheSize = 1)) } @@ -229,30 +243,47 @@ public class AndroidSqliteDriver private constructor( internal interface AndroidStatement : SqlPreparedStatement { fun execute(): Long + fun executeQuery(mapper: (SqlCursor) -> QueryResult): R + fun close() } private class AndroidPreparedStatement( private val statement: SupportSQLiteStatement, ) : AndroidStatement { - override fun bindBytes(index: Int, bytes: ByteArray?) { + override fun bindBytes( + index: Int, + bytes: ByteArray?, + ) { if (bytes == null) statement.bindNull(index + 1) else statement.bindBlob(index + 1, bytes) } - override fun bindLong(index: Int, long: Long?) { + override fun bindLong( + index: Int, + long: Long?, + ) { if (long == null) statement.bindNull(index + 1) else statement.bindLong(index + 1, long) } - override fun bindDouble(index: Int, double: Double?) { + override fun bindDouble( + index: Int, + double: Double?, + ) { if (double == null) statement.bindNull(index + 1) else statement.bindDouble(index + 1, double) } - override fun bindString(index: Int, string: String?) { + override fun bindString( + index: Int, + string: String?, + ) { if (string == null) statement.bindNull(index + 1) else statement.bindString(index + 1, string) } - override fun bindBoolean(index: Int, boolean: Boolean?) { + override fun bindBoolean( + index: Int, + boolean: Boolean?, + ) { if (boolean == null) { statement.bindNull(index + 1) } else { @@ -262,9 +293,7 @@ private class AndroidPreparedStatement( override fun executeQuery(mapper: (SqlCursor) -> QueryResult): R = throw UnsupportedOperationException() - override fun execute(): Long { - return statement.executeUpdateDelete().toLong() - } + override fun execute(): Long = statement.executeUpdateDelete().toLong() override fun close() { statement.close() @@ -280,23 +309,38 @@ private class AndroidQuery( AndroidStatement { private val binds = MutableList<((SupportSQLiteProgram) -> Unit)?>(argCount) { null } - override fun bindBytes(index: Int, bytes: ByteArray?) { + override fun bindBytes( + index: Int, + bytes: ByteArray?, + ) { binds[index] = { if (bytes == null) it.bindNull(index + 1) else it.bindBlob(index + 1, bytes) } } - override fun bindLong(index: Int, long: Long?) { + override fun bindLong( + index: Int, + long: Long?, + ) { binds[index] = { if (long == null) it.bindNull(index + 1) else it.bindLong(index + 1, long) } } - override fun bindDouble(index: Int, double: Double?) { + override fun bindDouble( + index: Int, + double: Double?, + ) { binds[index] = { if (double == null) it.bindNull(index + 1) else it.bindDouble(index + 1, double) } } - override fun bindString(index: Int, string: String?) { + override fun bindString( + index: Int, + string: String?, + ) { binds[index] = { if (string == null) it.bindNull(index + 1) else it.bindString(index + 1, string) } } - override fun bindBoolean(index: Int, boolean: Boolean?) { + override fun bindBoolean( + index: Int, + boolean: Boolean?, + ) { binds[index] = { if (boolean == null) { it.bindNull(index + 1) @@ -308,10 +352,10 @@ private class AndroidQuery( override fun execute() = throw UnsupportedOperationException() - override fun executeQuery(mapper: (SqlCursor) -> QueryResult): R { - return database.query(this) + override fun executeQuery(mapper: (SqlCursor) -> QueryResult): R = + database + .query(this) .use { cursor -> mapper(AndroidCursor(cursor, windowSizeBytes)).value } - } override fun bindTo(statement: SupportSQLiteProgram) { for (action in binds) { @@ -339,12 +383,19 @@ private class AndroidCursor( } override fun next(): QueryResult.Value = QueryResult.Value(cursor.moveToNext()) + override fun getString(index: Int) = if (cursor.isNull(index)) null else cursor.getString(index) + override fun getLong(index: Int) = if (cursor.isNull(index)) null else cursor.getLong(index) + override fun getBytes(index: Int) = if (cursor.isNull(index)) null else cursor.getBlob(index) + override fun getDouble(index: Int) = if (cursor.isNull(index)) null else cursor.getDouble(index) + override fun getBoolean(index: Int) = if (cursor.isNull(index)) null else cursor.getLong(index) == 1L + override fun columnName(index: Int): String? = cursor.getColumnName(index) + override val columnCount: Int = cursor.columnCount } diff --git a/persistence/src/commonMain/kotlin/com/powersync/persistence/driver/ColNamesSqlCursor.kt b/persistence/src/commonMain/kotlin/com/powersync/persistence/driver/ColNamesSqlCursor.kt index a04903c5..1693bac3 100644 --- a/persistence/src/commonMain/kotlin/com/powersync/persistence/driver/ColNamesSqlCursor.kt +++ b/persistence/src/commonMain/kotlin/com/powersync/persistence/driver/ColNamesSqlCursor.kt @@ -6,4 +6,4 @@ public interface ColNamesSqlCursor : SqlCursor { public fun columnName(index: Int): String? public val columnCount: Int -} \ No newline at end of file +} diff --git a/persistence/src/iosMain/kotlin/com/powersync/persistence/driver/Borrowed.kt b/persistence/src/iosMain/kotlin/com/powersync/persistence/driver/Borrowed.kt index 7f39efb2..e139e920 100644 --- a/persistence/src/iosMain/kotlin/com/powersync/persistence/driver/Borrowed.kt +++ b/persistence/src/iosMain/kotlin/com/powersync/persistence/driver/Borrowed.kt @@ -2,5 +2,6 @@ package com.powersync.persistence.driver internal interface Borrowed { val value: T + fun release() } diff --git a/persistence/src/iosMain/kotlin/com/powersync/persistence/driver/NativeSqlDatabase.kt b/persistence/src/iosMain/kotlin/com/powersync/persistence/driver/NativeSqlDatabase.kt index b6f935cb..3c4c8b35 100644 --- a/persistence/src/iosMain/kotlin/com/powersync/persistence/driver/NativeSqlDatabase.kt +++ b/persistence/src/iosMain/kotlin/com/powersync/persistence/driver/NativeSqlDatabase.kt @@ -9,7 +9,6 @@ import app.cash.sqldelight.db.SqlCursor import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.db.SqlPreparedStatement import app.cash.sqldelight.db.SqlSchema -import app.cash.sqldelight.internal.currentThreadId import co.touchlab.sqliter.DatabaseConfiguration import co.touchlab.sqliter.DatabaseConnection import co.touchlab.sqliter.DatabaseManager @@ -32,8 +31,8 @@ public sealed class ConnectionWrapper : SqlDriver { sql: String, binders: (SqlPreparedStatement.() -> Unit)?, block: (Statement) -> R, - ): R { - return accessConnection(readOnly) { + ): R = + accessConnection(readOnly) { val statement = useStatement(identifier, sql) try { if (binders != null) { @@ -45,18 +44,18 @@ public sealed class ConnectionWrapper : SqlDriver { clearIfNeeded(identifier, statement) } } - } final override fun execute( identifier: Int?, sql: String, parameters: Int, binders: (SqlPreparedStatement.() -> Unit)?, - ): QueryResult = QueryResult.Value( - accessStatement(false, identifier, sql, binders) { statement -> - statement.executeUpdateDelete().toLong() - }, - ) + ): QueryResult = + QueryResult.Value( + accessStatement(false, identifier, sql, binders) { statement -> + statement.executeUpdateDelete().toLong() + }, + ) final override fun executeQuery( identifier: Int?, @@ -64,9 +63,10 @@ public sealed class ConnectionWrapper : SqlDriver { mapper: (SqlCursor) -> QueryResult, parameters: Int, binders: (SqlPreparedStatement.() -> Unit)?, - ): QueryResult = accessStatement(true, identifier, sql, binders) { statement -> - mapper(SqliterSqlCursor(statement.query())) - } + ): QueryResult = + accessStatement(true, identifier, sql, binders) { statement -> + mapper(SqliterSqlCursor(statement.query())) + } } /** @@ -120,14 +120,22 @@ public class NativeSqliteDriver( onConfiguration: (DatabaseConfiguration) -> DatabaseConfiguration = { it }, vararg callbacks: AfterVersion, ) : this( - configuration = DatabaseConfiguration( - name = name, - version = if (schema.version > Int.MAX_VALUE) error("Schema version is larger than Int.MAX_VALUE: ${schema.version}.") else schema.version.toInt(), - create = { connection -> wrapConnection(connection) { schema.create(it) } }, - upgrade = { connection, oldVersion, newVersion -> - wrapConnection(connection) { schema.migrate(it, oldVersion.toLong(), newVersion.toLong(), *callbacks) } - }, - ).let(onConfiguration), + configuration = + DatabaseConfiguration( + name = name, + version = + if (schema.version > + Int.MAX_VALUE + ) { + error("Schema version is larger than Int.MAX_VALUE: ${schema.version}.") + } else { + schema.version.toInt() + }, + create = { connection -> wrapConnection(connection) { schema.create(it) } }, + upgrade = { connection, oldVersion, newVersion -> + wrapConnection(connection) { schema.migrate(it, oldVersion.toLong(), newVersion.toLong(), *callbacks) } + }, + ).let(onConfiguration), maxReaderConnections = maxReaderConnections, ) @@ -144,38 +152,44 @@ public class NativeSqliteDriver( init { if (databaseManager.configuration.isEphemeral) { // Single connection for transactions - transactionPool = Pool(1) { - ThreadConnection(databaseManager.createMultiThreadedConnection()) { _ -> - borrowedConnectionThread.let { - it.get()?.release() - it.value = null + transactionPool = + Pool(1) { + ThreadConnection(databaseManager.createMultiThreadedConnection()) { _ -> + borrowedConnectionThread.let { + it.get()?.release() + it.value = null + } } } - } readerPool = transactionPool } else { // Single connection for transactions - transactionPool = Pool(1) { - ThreadConnection(databaseManager.createMultiThreadedConnection()) { _ -> - borrowedConnectionThread.let { - it.get()?.release() - it.value = null + transactionPool = + Pool(1) { + ThreadConnection(databaseManager.createMultiThreadedConnection()) { _ -> + borrowedConnectionThread.let { + it.get()?.release() + it.value = null + } } } - } - readerPool = Pool(maxReaderConnections) { - val connection = databaseManager.createMultiThreadedConnection() - connection.withStatement("PRAGMA query_only = 1") { execute() } // Ensure read only - ThreadConnection(connection) { - throw UnsupportedOperationException("Should never be in a transaction") + readerPool = + Pool(maxReaderConnections) { + val connection = databaseManager.createMultiThreadedConnection() + connection.withStatement("PRAGMA query_only = 1") { execute() } // Ensure read only + ThreadConnection(connection) { + throw UnsupportedOperationException("Should never be in a transaction") + } } - } } } - override fun addListener(vararg queryKeys: String, listener: Query.Listener) { + override fun addListener( + vararg queryKeys: String, + listener: Query.Listener, + ) { lock.withLock { queryKeys.forEach { listeners.getOrPut(it) { mutableSetOf() }.add(listener) @@ -183,7 +197,10 @@ public class NativeSqliteDriver( } } - override fun removeListener(vararg queryKeys: String, listener: Query.Listener) { + override fun removeListener( + vararg queryKeys: String, + listener: Query.Listener, + ) { lock.withLock { queryKeys.forEach { listeners.get(it)?.remove(listener) @@ -199,28 +216,32 @@ public class NativeSqliteDriver( listenersToNotify.forEach(Query.Listener::queryResultsChanged) } - override fun currentTransaction(): Transacter.Transaction? { - return borrowedConnectionThread.get()?.value?.transaction?.value - } + override fun currentTransaction(): Transacter.Transaction? = + borrowedConnectionThread + .get() + ?.value + ?.transaction + ?.value override fun newTransaction(): QueryResult { val alreadyBorrowed = borrowedConnectionThread.get() - val transaction = if (alreadyBorrowed == null) { - val borrowed = transactionPool.borrowEntry() + val transaction = + if (alreadyBorrowed == null) { + val borrowed = transactionPool.borrowEntry() - try { - val trans = borrowed.value.newTransaction() - - borrowedConnectionThread.value = borrowed - trans - } catch (e: Throwable) { - // Unlock on failure. - borrowed.release() - throw e + try { + val trans = borrowed.value.newTransaction() + + borrowedConnectionThread.value = borrowed + trans + } catch (e: Throwable) { + // Unlock on failure. + borrowed.release() + throw e + } + } else { + alreadyBorrowed.value.newTransaction() } - } else { - alreadyBorrowed.value.newTransaction() - } return QueryResult.Value(transaction) } @@ -260,19 +281,27 @@ public class NativeSqliteDriver( * Helper function to create an in-memory driver. In-memory drivers have a single connection, so * concurrent access will be block */ -public fun inMemoryDriver(schema: SqlSchema>): NativeSqliteDriver = NativeSqliteDriver( - DatabaseConfiguration( - name = null, - inMemory = true, - version = if (schema.version > Int.MAX_VALUE) error("Schema version is larger than Int.MAX_VALUE: ${schema.version}.") else schema.version.toInt(), - create = { connection -> - wrapConnection(connection) { schema.create(it) } - }, - upgrade = { connection, oldVersion, newVersion -> - wrapConnection(connection) { schema.migrate(it, oldVersion.toLong(), newVersion.toLong()) } - }, - ), -) +public fun inMemoryDriver(schema: SqlSchema>): NativeSqliteDriver = + NativeSqliteDriver( + DatabaseConfiguration( + name = null, + inMemory = true, + version = + if (schema.version > + Int.MAX_VALUE + ) { + error("Schema version is larger than Int.MAX_VALUE: ${schema.version}.") + } else { + schema.version.toInt() + }, + create = { connection -> + wrapConnection(connection) { schema.create(it) } + }, + upgrade = { connection, oldVersion, newVersion -> + wrapConnection(connection) { schema.migrate(it, oldVersion.toLong(), newVersion.toLong()) } + }, + ), + ) /** * Sqliter's DatabaseConfiguration takes lambda arguments for it's create and upgrade operations, @@ -304,19 +333,24 @@ internal class SqliterWrappedConnection( SqlDriver { override fun currentTransaction(): Transacter.Transaction? = threadConnection.transaction.value - override fun newTransaction(): QueryResult = - QueryResult.Value(threadConnection.newTransaction()) + override fun newTransaction(): QueryResult = QueryResult.Value(threadConnection.newTransaction()) override fun accessConnection( readOnly: Boolean, block: ThreadConnection.() -> R, ): R = threadConnection.block() - override fun addListener(vararg queryKeys: String, listener: Query.Listener) { + override fun addListener( + vararg queryKeys: String, + listener: Query.Listener, + ) { // No-op } - override fun removeListener(vararg queryKeys: String, listener: Query.Listener) { + override fun removeListener( + vararg queryKeys: String, + listener: Query.Listener, + ) { // No-op } @@ -346,17 +380,22 @@ internal class ThreadConnection( private val statementCache = mutableMapOf() - fun useStatement(identifier: Int?, sql: String): Statement { - return if (identifier != null) { + fun useStatement( + identifier: Int?, + sql: String, + ): Statement = + if (identifier != null) { statementCache.getOrPut(identifier) { connection.createStatement(sql) } } else { connection.createStatement(sql) } - } - fun clearIfNeeded(identifier: Int?, statement: Statement) { + fun clearIfNeeded( + identifier: Int?, + statement: Statement, + ) { if (identifier == null || closed) { statement.finalizeStatement() } @@ -394,7 +433,6 @@ internal class ThreadConnection( private inner class Transaction( override val enclosingTransaction: Transacter.Transaction?, ) : Transacter.Transaction() { - override fun endTransaction(successful: Boolean): QueryResult { transaction.value = enclosingTransaction diff --git a/persistence/src/iosMain/kotlin/com/powersync/persistence/driver/Pool.kt b/persistence/src/iosMain/kotlin/com/powersync/persistence/driver/Pool.kt index 78c1f785..b2741f66 100644 --- a/persistence/src/iosMain/kotlin/com/powersync/persistence/driver/Pool.kt +++ b/persistence/src/iosMain/kotlin/com/powersync/persistence/driver/Pool.kt @@ -9,7 +9,10 @@ import kotlin.concurrent.AtomicReference * A shared pool of connections. Borrowing is blocking when all connections are in use, and the pool has reached its * designated capacity. */ -internal class Pool(internal val capacity: Int, private val producer: () -> T) { +internal class Pool( + internal val capacity: Int, + private val producer: () -> T, +) { /** * Hold a list of active connections. If it is null, it means the MultiPool has been closed. */ @@ -19,9 +22,10 @@ internal class Pool(internal val capacity: Int, private val produ /** * For test purposes only */ - internal fun entryCount(): Int = poolLock.withLock { - entriesRef.value?.size ?: 0 - } + internal fun entryCount(): Int = + poolLock.withLock { + entriesRef.value?.size ?: 0 + } fun borrowEntry(): Borrowed { val snapshot = entriesRef.value ?: throw ClosedMultiPoolException @@ -34,27 +38,28 @@ internal class Pool(internal val capacity: Int, private val produ } // Slowpath: Create a new entry if capacity limit has not been reached, or wait for the next available entry. - val nextAvailable = poolLock.withLock { - // Reload the list since it could've been updated by other threads concurrently. - val entries = entriesRef.value ?: throw ClosedMultiPoolException - - if (entries.count() < capacity) { - // Capacity hasn't been reached — create a new entry to serve this call. - val newEntry = Entry(producer()) - val done = newEntry.tryToAcquire() - check(done) - - entriesRef.value = (entries + listOf(newEntry)) - return@withLock newEntry - } else { - // Capacity is reached — wait for the next available entry. - return@withLock loopForConditionalResult { - // Reload the list, since the thread can be suspended here while the list of entries has been modified. - val innerEntries = entriesRef.value ?: throw ClosedMultiPoolException - innerEntries.firstOrNull { it.tryToAcquire() } + val nextAvailable = + poolLock.withLock { + // Reload the list since it could've been updated by other threads concurrently. + val entries = entriesRef.value ?: throw ClosedMultiPoolException + + if (entries.count() < capacity) { + // Capacity hasn't been reached — create a new entry to serve this call. + val newEntry = Entry(producer()) + val done = newEntry.tryToAcquire() + check(done) + + entriesRef.value = (entries + listOf(newEntry)) + return@withLock newEntry + } else { + // Capacity is reached — wait for the next available entry. + return@withLock loopForConditionalResult { + // Reload the list, since the thread can be suspended here while the list of entries has been modified. + val innerEntries = entriesRef.value ?: throw ClosedMultiPoolException + innerEntries.firstOrNull { it.tryToAcquire() } + } } } - } return nextAvailable.asBorrowed(poolLock) } @@ -80,40 +85,43 @@ internal class Pool(internal val capacity: Int, private val produ entries?.forEach { it.value.close() } } - inner class Entry(val value: T) { + inner class Entry( + val value: T, + ) { val isAvailable = AtomicBoolean(true) fun tryToAcquire(): Boolean = isAvailable.compareAndSet(expected = true, new = false) - fun asBorrowed(poolLock: PoolLock): Borrowed = object : Borrowed { - override val value: T - get() = this@Entry.value - - override fun release() { - /** - * Mark-as-available should be done before signalling blocked threads via [PoolLock.notifyConditionChanged], - * since the happens-before relationship guarantees the woken thread to see the - * available entry (if not having been taken by other threads during the wake-up lead time). - */ - - val done = isAvailable.compareAndSet(expected = false, new = true) - check(done) - - // While signalling blocked threads does not require locking, doing so avoids a subtle race - // condition in which: - // - // 1. a [loopForConditionalResult] iteration in [borrowEntry] slow path is happening concurrently; - // 2. the iteration fails to see the atomic `isAvailable = true` above; - // 3. we signal availability here but it is a no-op due to no waiting blocker; and finally - // 4. the iteration entered an indefinite blocking wait, not being aware of us having signalled availability here. - // - // By acquiring the pool lock first, signalling cannot happen concurrently with the loop - // iterations in [borrowEntry], thus eliminating the race condition. - poolLock.withLock { - poolLock.notifyConditionChanged() + fun asBorrowed(poolLock: PoolLock): Borrowed = + object : Borrowed { + override val value: T + get() = this@Entry.value + + override fun release() { + /** + * Mark-as-available should be done before signalling blocked threads via [PoolLock.notifyConditionChanged], + * since the happens-before relationship guarantees the woken thread to see the + * available entry (if not having been taken by other threads during the wake-up lead time). + */ + + val done = isAvailable.compareAndSet(expected = false, new = true) + check(done) + + // While signalling blocked threads does not require locking, doing so avoids a subtle race + // condition in which: + // + // 1. a [loopForConditionalResult] iteration in [borrowEntry] slow path is happening concurrently; + // 2. the iteration fails to see the atomic `isAvailable = true` above; + // 3. we signal availability here but it is a no-op due to no waiting blocker; and finally + // 4. the iteration entered an indefinite blocking wait, not being aware of us having signalled availability here. + // + // By acquiring the pool lock first, signalling cannot happen concurrently with the loop + // iterations in [borrowEntry], thus eliminating the race condition. + poolLock.withLock { + poolLock.notifyConditionChanged() + } } } - } } } diff --git a/persistence/src/iosMain/kotlin/com/powersync/persistence/driver/SqliterSqlCursor.kt b/persistence/src/iosMain/kotlin/com/powersync/persistence/driver/SqliterSqlCursor.kt index 727913ce..89dd41a9 100644 --- a/persistence/src/iosMain/kotlin/com/powersync/persistence/driver/SqliterSqlCursor.kt +++ b/persistence/src/iosMain/kotlin/com/powersync/persistence/driver/SqliterSqlCursor.kt @@ -12,7 +12,9 @@ import co.touchlab.sqliter.getStringOrNull * them. If dev closes the outer structure, this will get closed as well, which means it could start * throwing errors if you're trying to access it. */ -internal class SqliterSqlCursor(private val cursor: Cursor) : ColNamesSqlCursor { +internal class SqliterSqlCursor( + private val cursor: Cursor, +) : ColNamesSqlCursor { override fun getBytes(index: Int): ByteArray? = cursor.getBytesOrNull(index) override fun getDouble(index: Int): Double? = cursor.getDoubleOrNull(index) diff --git a/persistence/src/iosMain/kotlin/com/powersync/persistence/driver/SqliterStatement.kt b/persistence/src/iosMain/kotlin/com/powersync/persistence/driver/SqliterStatement.kt index f78f2ef1..624f2fc3 100644 --- a/persistence/src/iosMain/kotlin/com/powersync/persistence/driver/SqliterStatement.kt +++ b/persistence/src/iosMain/kotlin/com/powersync/persistence/driver/SqliterStatement.kt @@ -13,23 +13,38 @@ import co.touchlab.sqliter.bindString internal class SqliterStatement( private val statement: Statement, ) : SqlPreparedStatement { - override fun bindBytes(index: Int, bytes: ByteArray?) { + override fun bindBytes( + index: Int, + bytes: ByteArray?, + ) { statement.bindBlob(index + 1, bytes) } - override fun bindLong(index: Int, long: Long?) { + override fun bindLong( + index: Int, + long: Long?, + ) { statement.bindLong(index + 1, long) } - override fun bindDouble(index: Int, double: Double?) { + override fun bindDouble( + index: Int, + double: Double?, + ) { statement.bindDouble(index + 1, double) } - override fun bindString(index: Int, string: String?) { + override fun bindString( + index: Int, + string: String?, + ) { statement.bindString(index + 1, string) } - override fun bindBoolean(index: Int, boolean: Boolean?) { + override fun bindBoolean( + index: Int, + boolean: Boolean?, + ) { statement.bindLong( index + 1, when (boolean) { diff --git a/persistence/src/iosMain/kotlin/com/powersync/persistence/driver/util/PoolLock.kt b/persistence/src/iosMain/kotlin/com/powersync/persistence/driver/util/PoolLock.kt index 152210f8..cf8d5e08 100644 --- a/persistence/src/iosMain/kotlin/com/powersync/persistence/driver/util/PoolLock.kt +++ b/persistence/src/iosMain/kotlin/com/powersync/persistence/driver/util/PoolLock.kt @@ -22,24 +22,30 @@ import platform.posix.pthread_mutexattr_settype import platform.posix.pthread_mutexattr_t @OptIn(ExperimentalForeignApi::class) -internal class PoolLock constructor(reentrant: Boolean = false) { +internal class PoolLock constructor( + reentrant: Boolean = false, +) { private val isActive = AtomicBoolean(true) - private val attr = nativeHeap.alloc() - .apply { - pthread_mutexattr_init(ptr) - if (reentrant) { - pthread_mutexattr_settype(ptr, platform.posix.PTHREAD_MUTEX_RECURSIVE) + private val attr = + nativeHeap + .alloc() + .apply { + pthread_mutexattr_init(ptr) + if (reentrant) { + pthread_mutexattr_settype(ptr, platform.posix.PTHREAD_MUTEX_RECURSIVE) + } } - } - private val mutex = nativeHeap.alloc() - .apply { pthread_mutex_init(ptr, attr.ptr) } - private val cond = nativeHeap.alloc() - .apply { pthread_cond_init(ptr, null) } + private val mutex = + nativeHeap + .alloc() + .apply { pthread_mutex_init(ptr, attr.ptr) } + private val cond = + nativeHeap + .alloc() + .apply { pthread_cond_init(ptr, null) } - fun withLock( - action: CriticalSection.() -> R, - ): R { + fun withLock(action: CriticalSection.() -> R): R { check(isActive.value) pthread_mutex_lock(mutex.ptr) @@ -86,4 +92,4 @@ internal class PoolLock constructor(reentrant: Boolean = false) { return result } } -} \ No newline at end of file +} diff --git a/plugins/sonatype/build.gradle.kts b/plugins/sonatype/build.gradle.kts index fd10250b..bb33cf90 100644 --- a/plugins/sonatype/build.gradle.kts +++ b/plugins/sonatype/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { id("java-gradle-plugin") alias(libs.plugins.kotlin.jvm) @@ -11,10 +13,27 @@ gradlePlugin { } } +// The target release option here is the version of the JVM running the build by default, but Kotlin +// typically doesn't support the latest version yet, causing mismatch warnings. So, target the latest +// LTS java version to be safe. +val highestTargetVersion = JavaVersion.VERSION_21 +val currentVersion = JavaVersion.current() +val targetVersion = minOf(highestTargetVersion, currentVersion) + +java { + targetCompatibility = targetVersion +} + kotlin { - explicitApi() + kotlin { + explicitApi() + + compilerOptions { + jvmTarget.set(JvmTarget.valueOf("JVM_${targetVersion.majorVersion}")) + } + } } dependencies { implementation(libs.mavenPublishPlugin) -} \ No newline at end of file +}