diff --git a/CHANGELOG.md b/CHANGELOG.md index e6ffcb87..4072080e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # Changelog +## 1.0.0-BETA21 + +* Improve error handling for Swift by adding @Throws annotation so errors can be handled in Swift +* Throw PowerSync exceptions for all public facing methods + ## 1.0.0-BETA20 + * Add cursor optional functions: `getStringOptional`, `getLongOptional`, `getDoubleOptional`, `getBooleanOptional` and `getBytesOptional` when using the column name which allow for optional return types * Throw errors for invalid column on all cursor functions * `getString`, `getLong`, `getBytes`, `getDouble` and `getBoolean` used with the column name will now throw an error for non-null values and expect a non optional return type @@ -22,8 +28,6 @@ import com.powersync.db.SqlCursor ``` - - ## 1.0.0-BETA18 * BREAKING CHANGE: Move from async sqldelight calls to synchronous calls. This will only affect `readTransaction` and `writeTransaction`where the callback function is no longer asynchronous. diff --git a/Package.swift b/Package.swift index f67490a2..a6c2ad52 100644 --- a/Package.swift +++ b/Package.swift @@ -21,4 +21,4 @@ let package = Package( ) , ] -) +) \ No newline at end of file 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 1ada35eb..f4c33adb 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 @@ -6,6 +6,7 @@ import com.powersync.connectors.PowerSyncBackendConnector import com.powersync.connectors.PowerSyncCredentials import com.powersync.db.crud.CrudEntry import com.powersync.db.crud.UpdateType +import com.powersync.db.runWrappedSuspending import io.github.jan.supabase.SupabaseClient import io.github.jan.supabase.annotations.SupabaseInternal import io.github.jan.supabase.auth.Auth @@ -94,9 +95,11 @@ public class SupabaseConnector( email: String, password: String, ) { - supabaseClient.auth.signInWith(Email) { - this.email = email - this.password = password + runWrappedSuspending { + supabaseClient.auth.signInWith(Email) { + this.email = email + this.password = password + } } } @@ -104,14 +107,18 @@ public class SupabaseConnector( email: String, password: String, ) { - supabaseClient.auth.signUpWith(Email) { - this.email = email - this.password = password + runWrappedSuspending { + supabaseClient.auth.signUpWith(Email) { + this.email = email + this.password = password + } } } public suspend fun signOut() { - supabaseClient.auth.signOut() + runWrappedSuspending { + supabaseClient.auth.signOut() + } } public fun session(): UserSession? = supabaseClient.auth.currentSessionOrNull() @@ -119,27 +126,30 @@ public class SupabaseConnector( public val sessionStatus: StateFlow = supabaseClient.auth.sessionStatus public suspend fun loginAnonymously() { - supabaseClient.auth.signInAnonymously() + runWrappedSuspending { + supabaseClient.auth.signInAnonymously() + } } /** * Get credentials for PowerSync. */ - override suspend fun fetchCredentials(): PowerSyncCredentials { - check(supabaseClient.auth.sessionStatus.value is SessionStatus.Authenticated) { "Supabase client is not authenticated" } + override suspend fun fetchCredentials(): PowerSyncCredentials = + runWrappedSuspending { + check(supabaseClient.auth.sessionStatus.value is SessionStatus.Authenticated) { "Supabase client is not authenticated" } - // Use Supabase token for PowerSync - val session = supabaseClient.auth.currentSessionOrNull() ?: error("Could not fetch Supabase credentials") + // Use Supabase token for PowerSync + val session = supabaseClient.auth.currentSessionOrNull() ?: error("Could not fetch Supabase credentials") - check(session.user != null) { "No user data" } + check(session.user != null) { "No user data" } - // userId is for debugging purposes only - return PowerSyncCredentials( - endpoint = powerSyncEndpoint, - token = session.accessToken, // Use the access token to authenticate against PowerSync - userId = session.user!!.id, - ) - } + // userId is for debugging purposes only + PowerSyncCredentials( + endpoint = powerSyncEndpoint, + token = session.accessToken, // Use the access token to authenticate against PowerSync + userId = session.user!!.id, + ) + } /** * Upload local changes to the app backend (in this case Supabase). @@ -148,59 +158,61 @@ public class SupabaseConnector( * If this call throws an error, it is retried periodically. */ override suspend fun uploadData(database: PowerSyncDatabase) { - val transaction = database.getNextCrudTransaction() ?: return + return runWrappedSuspending { + val transaction = database.getNextCrudTransaction() ?: return@runWrappedSuspending - var lastEntry: CrudEntry? = null - try { - for (entry in transaction.crud) { - lastEntry = entry + var lastEntry: CrudEntry? = null + try { + for (entry in transaction.crud) { + lastEntry = entry - val table = supabaseClient.from(entry.table) + val table = supabaseClient.from(entry.table) - when (entry.op) { - UpdateType.PUT -> { - val data = entry.opData?.toMutableMap() ?: mutableMapOf() - data["id"] = entry.id - table.upsert(data) - } + when (entry.op) { + UpdateType.PUT -> { + val data = entry.opData?.toMutableMap() ?: mutableMapOf() + data["id"] = entry.id + table.upsert(data) + } - UpdateType.PATCH -> { - table.update(entry.opData!!) { - filter { - eq("id", entry.id) + UpdateType.PATCH -> { + table.update(entry.opData!!) { + filter { + eq("id", entry.id) + } } } - } - UpdateType.DELETE -> { - table.delete { - filter { - eq("id", entry.id) + UpdateType.DELETE -> { + table.delete { + filter { + eq("id", entry.id) + } } } } } - } - transaction.complete(null) - } catch (e: Exception) { - if (errorCode != null && PostgresFatalCodes.isFatalError(errorCode.toString())) { - /** - * Instead of blocking the queue with these errors, - * discard the (rest of the) transaction. - * - * Note that these errors typically indicate a bug in the application. - * If protecting against data loss is important, save the failing records - * elsewhere instead of discarding, and/or notify the user. - */ - Logger.e("Data upload error: ${e.message}") - Logger.e("Discarding entry: $lastEntry") transaction.complete(null) - return - } + } catch (e: Exception) { + if (errorCode != null && PostgresFatalCodes.isFatalError(errorCode.toString())) { + /** + * Instead of blocking the queue with these errors, + * discard the (rest of the) transaction. + * + * Note that these errors typically indicate a bug in the application. + * If protecting against data loss is important, save the failing records + * elsewhere instead of discarding, and/or notify the user. + */ + Logger.e("Data upload error: ${e.message}") + Logger.e("Discarding entry: $lastEntry") + transaction.complete(null) + return@runWrappedSuspending + } - Logger.e("Data upload error - retrying last entry: $lastEntry, $e") - throw e + Logger.e("Data upload error - retrying last entry: $lastEntry, $e") + throw e + } } } } diff --git a/core/src/commonMain/kotlin/com/powersync/Exceptions.kt b/core/src/commonMain/kotlin/com/powersync/Exceptions.kt new file mode 100644 index 00000000..0ac7a40d --- /dev/null +++ b/core/src/commonMain/kotlin/com/powersync/Exceptions.kt @@ -0,0 +1,6 @@ +package com.powersync + +public class PowerSyncException( + message: String, + cause: Throwable, +) : Exception(message, cause) diff --git a/core/src/commonMain/kotlin/com/powersync/PowerSyncDatabase.kt b/core/src/commonMain/kotlin/com/powersync/PowerSyncDatabase.kt index 888751e2..60fa5575 100644 --- a/core/src/commonMain/kotlin/com/powersync/PowerSyncDatabase.kt +++ b/core/src/commonMain/kotlin/com/powersync/PowerSyncDatabase.kt @@ -6,6 +6,7 @@ import com.powersync.db.crud.CrudBatch import com.powersync.db.crud.CrudTransaction import com.powersync.sync.SyncStatus import com.powersync.utils.JsonParam +import kotlin.coroutines.cancellation.CancellationException /** * A PowerSync managed database. @@ -25,6 +26,7 @@ public interface PowerSyncDatabase : Queries { /** * Suspend function that resolves when the first sync has occurred */ + @Throws(PowerSyncException::class, CancellationException::class) public suspend fun waitForFirstSync() /** @@ -56,7 +58,7 @@ public interface PowerSyncDatabase : Queries { * ``` * TODO: Internal Team - Status changes are reported on [statusStream]. */ - + @Throws(PowerSyncException::class, CancellationException::class) public suspend fun connect( connector: PowerSyncBackendConnector, crudThrottleMs: Long = 1000L, @@ -81,6 +83,7 @@ public interface PowerSyncDatabase : Queries { * data by transaction. One batch may contain data from multiple transactions, * and a single transaction may be split over multiple batches. */ + @Throws(PowerSyncException::class, CancellationException::class) public suspend fun getCrudBatch(limit: Int = 100): CrudBatch? /** @@ -96,12 +99,13 @@ public interface PowerSyncDatabase : Queries { * Unlike [getCrudBatch], this only returns data from a single transaction at a time. * All data for the transaction is loaded into memory. */ - + @Throws(PowerSyncException::class, CancellationException::class) public suspend fun getNextCrudTransaction(): CrudTransaction? /** * Convenience method to get the current version of PowerSync. */ + @Throws(PowerSyncException::class, CancellationException::class) public suspend fun getPowerSyncVersion(): String /** @@ -109,6 +113,7 @@ public interface PowerSyncDatabase : Queries { * * Use [connect] to connect again. */ + @Throws(PowerSyncException::class, CancellationException::class) public suspend fun disconnect() /** @@ -119,6 +124,7 @@ public interface PowerSyncDatabase : Queries { * * To preserve data in local-only tables, set clearLocal to false. */ + @Throws(PowerSyncException::class, CancellationException::class) public suspend fun disconnectAndClear(clearLocal: Boolean = true) /** @@ -127,5 +133,6 @@ public interface PowerSyncDatabase : Queries { * * Once close is called, this database cannot be used again - a new one must be constructed. */ + @Throws(PowerSyncException::class, CancellationException::class) public suspend fun close() } diff --git a/core/src/commonMain/kotlin/com/powersync/bucket/BucketStorageImpl.kt b/core/src/commonMain/kotlin/com/powersync/bucket/BucketStorageImpl.kt index 1255e22c..11b21524 100644 --- a/core/src/commonMain/kotlin/com/powersync/bucket/BucketStorageImpl.kt +++ b/core/src/commonMain/kotlin/com/powersync/bucket/BucketStorageImpl.kt @@ -53,8 +53,7 @@ internal class BucketStorageImpl( return id ?: throw IllegalStateException("Client ID not found") } - override suspend fun nextCrudItem(): CrudEntry? = - db.getOptional(sql = nextCrudQuery, mapper = nextCrudMapper) + override suspend fun nextCrudItem(): CrudEntry? = db.getOptional(sql = nextCrudQuery, mapper = nextCrudMapper) override fun nextCrudItem(transaction: PowerSyncTransaction): CrudEntry? = transaction.getOptional(sql = nextCrudQuery, mapper = nextCrudMapper) @@ -81,7 +80,7 @@ internal class BucketStorageImpl( } private val hasCrudQuery = "SELECT 1 FROM ps_crud LIMIT 1" - private val hasCrudMapper:(SqlCursor) -> Long = { + private val hasCrudMapper: (SqlCursor) -> Long = { it.getLong(0)!! } diff --git a/core/src/commonMain/kotlin/com/powersync/connectors/PowerSyncBackendConnector.kt b/core/src/commonMain/kotlin/com/powersync/connectors/PowerSyncBackendConnector.kt index c20488a2..e1ce40af 100644 --- a/core/src/commonMain/kotlin/com/powersync/connectors/PowerSyncBackendConnector.kt +++ b/core/src/commonMain/kotlin/com/powersync/connectors/PowerSyncBackendConnector.kt @@ -1,11 +1,14 @@ package com.powersync.connectors import com.powersync.PowerSyncDatabase +import com.powersync.PowerSyncException +import com.powersync.db.runWrappedSuspending import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch +import kotlin.coroutines.cancellation.CancellationException /** * Implement this to connect an app backend. @@ -26,10 +29,13 @@ public abstract class PowerSyncBackendConnector { * * These credentials may have expired already. */ + @Throws(PowerSyncException::class, CancellationException::class) public open suspend fun getCredentialsCached(): PowerSyncCredentials? { - cachedCredentials?.let { return it } - prefetchCredentials()?.join() - return cachedCredentials + return runWrappedSuspending { + cachedCredentials?.let { return@runWrappedSuspending it } + prefetchCredentials()?.join() + cachedCredentials + } } /** @@ -49,6 +55,7 @@ public abstract class PowerSyncBackendConnector { * * This may be called before the current credentials have expired. */ + @Throws(PowerSyncException::class, CancellationException::class) public open suspend fun prefetchCredentials(): Job? { fetchRequest?.takeIf { it.isActive }?.let { return it } @@ -74,6 +81,7 @@ public abstract class PowerSyncBackendConnector { * * This token is kept for the duration of a sync connection. */ + @Throws(PowerSyncException::class, CancellationException::class) public abstract suspend fun fetchCredentials(): PowerSyncCredentials? /** @@ -83,5 +91,6 @@ public abstract class PowerSyncBackendConnector { * * Any thrown errors will result in a retry after the configured wait period (default: 5 seconds). */ + @Throws(PowerSyncException::class, CancellationException::class) public abstract suspend fun uploadData(database: PowerSyncDatabase) } diff --git a/core/src/commonMain/kotlin/com/powersync/db/Functions.kt b/core/src/commonMain/kotlin/com/powersync/db/Functions.kt new file mode 100644 index 00000000..77194fd1 --- /dev/null +++ b/core/src/commonMain/kotlin/com/powersync/db/Functions.kt @@ -0,0 +1,30 @@ +package com.powersync.db + +import co.touchlab.kermit.Logger +import com.powersync.PowerSyncException + +public fun runWrapped(block: () -> R): R = + try { + block() + } catch (t: Throwable) { + if (t is PowerSyncException) { + Logger.e("PowerSyncException: ${t.message}") + throw t + } else { + Logger.e("PowerSyncException: ${t.message}") + throw PowerSyncException(t.message ?: "Unknown internal exception", t) + } + } + +public suspend fun runWrappedSuspending(block: suspend () -> R): R = + try { + block() + } catch (t: Throwable) { + if (t is PowerSyncException) { + Logger.e("PowerSyncException: ${t.message}") + throw t + } else { + Logger.e("PowerSyncException: ${t.message}") + throw PowerSyncException(t.message ?: "Unknown internal exception", t) + } + } diff --git a/core/src/commonMain/kotlin/com/powersync/db/PowerSyncDatabaseImpl.kt b/core/src/commonMain/kotlin/com/powersync/db/PowerSyncDatabaseImpl.kt index cefd591f..b1905def 100644 --- a/core/src/commonMain/kotlin/com/powersync/db/PowerSyncDatabaseImpl.kt +++ b/core/src/commonMain/kotlin/com/powersync/db/PowerSyncDatabaseImpl.kt @@ -222,11 +222,9 @@ internal class PowerSyncDatabaseImpl( mapper: (SqlCursor) -> RowType, ): Flow> = internalDb.watch(sql, parameters, mapper) - override suspend fun readTransaction(callback: (tx: PowerSyncTransaction) -> R): R = - internalDb.writeTransaction(callback) + override suspend fun readTransaction(callback: (tx: PowerSyncTransaction) -> R): R = internalDb.writeTransaction(callback) - override suspend fun writeTransaction(callback: (tx: PowerSyncTransaction) -> R): R = - internalDb.writeTransaction(callback) + override suspend fun writeTransaction(callback: (tx: PowerSyncTransaction) -> R): R = internalDb.writeTransaction(callback) override suspend fun execute( sql: String, @@ -282,24 +280,16 @@ 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 hasSynced = timestamp != null - if (hasSynced != currentStatus.hasSynced) { - val formattedDateTime = "${timestamp!!.replace(" ","T").toLocalDateTime()}Z" - val lastSyncedAt = Instant.parse(formattedDateTime) - currentStatus.update(hasSynced = hasSynced, lastSyncedAt = lastSyncedAt) - } - } catch (e: Exception) { - if (e is NullPointerException) { - // No data has been synced which results in a null pointer exception - // and can be safely ignored. - return + val timestamp = + internalDb.getOptional("SELECT powersync_last_synced_at() as synced_at", null) { cursor -> + cursor.getString(0) ?: "" } + + val hasSynced = timestamp != "" + if (hasSynced != currentStatus.hasSynced) { + val formattedDateTime = "${timestamp!!.replace(" ", "T").toLocalDateTime()}Z" + val lastSyncedAt = Instant.parse(formattedDateTime) + currentStatus.update(hasSynced = hasSynced, lastSyncedAt = lastSyncedAt) } } diff --git a/core/src/commonMain/kotlin/com/powersync/db/Queries.kt b/core/src/commonMain/kotlin/com/powersync/db/Queries.kt index 90c72e19..b4e8a806 100644 --- a/core/src/commonMain/kotlin/com/powersync/db/Queries.kt +++ b/core/src/commonMain/kotlin/com/powersync/db/Queries.kt @@ -1,12 +1,15 @@ package com.powersync.db +import com.powersync.PowerSyncException import com.powersync.db.internal.PowerSyncTransaction import kotlinx.coroutines.flow.Flow +import kotlin.coroutines.cancellation.CancellationException public interface Queries { /** * Execute a write query (INSERT, UPDATE, DELETE) */ + @Throws(PowerSyncException::class, CancellationException::class) public suspend fun execute( sql: String, parameters: List? = listOf(), @@ -17,6 +20,7 @@ public interface Queries { * If there is no result, throws an [IllegalArgumentException]. * See [getOptional] for queries where the result might be empty. */ + @Throws(PowerSyncException::class, CancellationException::class) public suspend fun get( sql: String, parameters: List? = listOf(), @@ -26,6 +30,7 @@ public interface Queries { /** * Execute a read-only (SELECT) query and return the results. */ + @Throws(PowerSyncException::class, CancellationException::class) public suspend fun getAll( sql: String, parameters: List? = listOf(), @@ -35,6 +40,7 @@ public interface Queries { /** * Execute a read-only (SELECT) query and return a single optional result. */ + @Throws(PowerSyncException::class, CancellationException::class) public suspend fun getOptional( sql: String, parameters: List? = listOf(), @@ -44,13 +50,16 @@ public interface Queries { /** * Execute a read-only (SELECT) query every time the source tables are modified and return the results as a List in [Flow]. */ + @Throws(PowerSyncException::class, CancellationException::class) public fun watch( sql: String, parameters: List? = listOf(), mapper: (SqlCursor) -> RowType, ): Flow> + @Throws(PowerSyncException::class, CancellationException::class) public suspend fun writeTransaction(callback: (PowerSyncTransaction) -> R): R + @Throws(PowerSyncException::class, CancellationException::class) public suspend fun readTransaction(callback: (PowerSyncTransaction) -> R): R } diff --git a/core/src/commonMain/kotlin/com/powersync/db/SqlCursor.kt b/core/src/commonMain/kotlin/com/powersync/db/SqlCursor.kt index d1a7f1bb..bca14a55 100644 --- a/core/src/commonMain/kotlin/com/powersync/db/SqlCursor.kt +++ b/core/src/commonMain/kotlin/com/powersync/db/SqlCursor.kt @@ -1,6 +1,7 @@ package com.powersync.db import co.touchlab.skie.configuration.annotations.FunctionInterop +import com.powersync.PowerSyncException public interface SqlCursor { public fun getBoolean(index: Int): Boolean? @@ -35,27 +36,37 @@ private inline fun SqlCursor.getColumnValueOptional( // This causes a collision the functions created in Swift and there we need to disable this conversion @FunctionInterop.FileScopeConversion.Disabled +@Throws(PowerSyncException::class, IllegalArgumentException::class) public fun SqlCursor.getBoolean(name: String): Boolean = getColumnValue(name) { getBoolean(it) } @FunctionInterop.FileScopeConversion.Disabled +@Throws(PowerSyncException::class, IllegalArgumentException::class) public fun SqlCursor.getBytes(name: String): ByteArray = getColumnValue(name) { getBytes(it) } @FunctionInterop.FileScopeConversion.Disabled +@Throws(PowerSyncException::class, IllegalArgumentException::class) public fun SqlCursor.getDouble(name: String): Double = getColumnValue(name) { getDouble(it) } @FunctionInterop.FileScopeConversion.Disabled +@Throws(PowerSyncException::class, IllegalArgumentException::class) public fun SqlCursor.getLong(name: String): Long = getColumnValue(name) { getLong(it) } @FunctionInterop.FileScopeConversion.Disabled +@Throws(PowerSyncException::class, IllegalArgumentException::class) public fun SqlCursor.getString(name: String): String = getColumnValue(name) { getString(it) } +@Throws(PowerSyncException::class, IllegalArgumentException::class) public fun SqlCursor.getBooleanOptional(name: String): Boolean? = getColumnValueOptional(name) { getBoolean(it) } +@Throws(PowerSyncException::class, IllegalArgumentException::class) public fun SqlCursor.getBytesOptional(name: String): ByteArray? = getColumnValueOptional(name) { getBytes(it) } +@Throws(PowerSyncException::class, IllegalArgumentException::class) public fun SqlCursor.getDoubleOptional(name: String): Double? = getColumnValueOptional(name) { getDouble(it) } +@Throws(PowerSyncException::class, IllegalArgumentException::class) public fun SqlCursor.getLongOptional(name: String): Long? = getColumnValueOptional(name) { getLong(it) } @FunctionInterop.FileScopeConversion.Disabled +@Throws(PowerSyncException::class, IllegalArgumentException::class) public fun SqlCursor.getStringOptional(name: String): String? = getColumnValueOptional(name) { getString(it) } 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 ff9dda40..54a9cd16 100644 --- a/core/src/commonMain/kotlin/com/powersync/db/internal/InternalDatabase.kt +++ b/core/src/commonMain/kotlin/com/powersync/db/internal/InternalDatabase.kt @@ -6,7 +6,9 @@ import com.powersync.db.Queries import com.powersync.persistence.PsDatabase import kotlinx.coroutines.flow.Flow -internal interface InternalDatabase : Queries, Closeable { +internal interface InternalDatabase : + Queries, + Closeable { val transactor: PsDatabase val queries: PowersyncQueries 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 59ae6d57..c609010f 100644 --- a/core/src/commonMain/kotlin/com/powersync/db/internal/InternalDatabaseImpl.kt +++ b/core/src/commonMain/kotlin/com/powersync/db/internal/InternalDatabaseImpl.kt @@ -9,6 +9,7 @@ import app.cash.sqldelight.db.SqlPreparedStatement import com.persistence.PowersyncQueries import com.powersync.PsSqlDriver import com.powersync.db.SqlCursor +import com.powersync.db.runWrapped import com.powersync.persistence.PsDatabase import com.powersync.utils.JsonUtil import kotlinx.coroutines.CoroutineScope @@ -186,7 +187,9 @@ internal class InternalDatabaseImpl( ): ExecutableQuery = object : ExecutableQuery(wrapperMapper(mapper)) { override fun execute(mapper: (app.cash.sqldelight.db.SqlCursor) -> QueryResult): QueryResult = - driver.executeQuery(null, query, mapper, parameters, binders) + runWrapped { + driver.executeQuery(null, query, mapper, parameters, binders) + } } private fun watchQuery( @@ -198,7 +201,9 @@ internal class InternalDatabaseImpl( ): Query = object : Query(wrapperMapper(mapper)) { override fun execute(mapper: (app.cash.sqldelight.db.SqlCursor) -> QueryResult): QueryResult = - driver.executeQuery(null, query, mapper, parameters, binders) + runWrapped { + driver.executeQuery(null, query, mapper, parameters, binders) + } override fun addListener(listener: Listener) { driver.addListener(queryKeys = tables.toTypedArray(), listener = listener) @@ -293,7 +298,7 @@ internal class InternalDatabaseImpl( } override fun close() { - this.driver.close() + runWrapped { this.driver.close() } } internal data class ExplainQueryResult( diff --git a/core/src/commonMain/kotlin/com/powersync/db/internal/SqlCursorWrapper.kt b/core/src/commonMain/kotlin/com/powersync/db/internal/SqlCursorWrapper.kt index 90cb8e0c..bdb0c298 100644 --- a/core/src/commonMain/kotlin/com/powersync/db/internal/SqlCursorWrapper.kt +++ b/core/src/commonMain/kotlin/com/powersync/db/internal/SqlCursorWrapper.kt @@ -3,7 +3,9 @@ package com.powersync.db.internal import app.cash.sqldelight.db.SqlCursor import com.powersync.persistence.driver.ColNamesSqlCursor -internal class SqlCursorWrapper(val realCursor: ColNamesSqlCursor) : com.powersync.db.SqlCursor { +internal class SqlCursorWrapper( + val realCursor: ColNamesSqlCursor, +) : com.powersync.db.SqlCursor { override fun getBoolean(index: Int): Boolean? = realCursor.getBoolean(index) override fun getBytes(index: Int): ByteArray? = realCursor.getBytes(index) @@ -42,6 +44,5 @@ internal class SqlCursorWrapper(val realCursor: ColNamesSqlCursor) : com.powersy } } -internal fun wrapperMapper(mapper: (com.powersync.db.SqlCursor) -> T): (SqlCursor) -> T { - return { realCursor -> mapper(SqlCursorWrapper(realCursor as ColNamesSqlCursor)) } -} \ No newline at end of file +internal fun wrapperMapper(mapper: (com.powersync.db.SqlCursor) -> T): (SqlCursor) -> T = + { realCursor -> mapper(SqlCursorWrapper(realCursor as ColNamesSqlCursor)) } diff --git a/core/src/commonMain/kotlin/com/powersync/sync/SyncStream.kt b/core/src/commonMain/kotlin/com/powersync/sync/SyncStream.kt index c16025a9..e1bf50a1 100644 --- a/core/src/commonMain/kotlin/com/powersync/sync/SyncStream.kt +++ b/core/src/commonMain/kotlin/com/powersync/sync/SyncStream.kt @@ -100,7 +100,7 @@ internal class SyncStream( throw e } - logger.e(e) { "Error in streamingSync: $e" } + logger.e("Error in streamingSync: ${e.message}") status.update( downloadError = e, ) @@ -152,7 +152,7 @@ internal class SyncStream( break } } catch (e: Exception) { - logger.e(e) { "Error uploading crud" } + logger.e { "Error uploading crud: ${e.message}" } status.update(uploading = false, uploadError = e) delay(retryDelayMs) break diff --git a/core/src/commonTest/kotlin/com/powersync/sync/SyncStreamTest.kt b/core/src/commonTest/kotlin/com/powersync/sync/SyncStreamTest.kt index 8284dce8..26cf99b7 100644 --- a/core/src/commonTest/kotlin/com/powersync/sync/SyncStreamTest.kt +++ b/core/src/commonTest/kotlin/com/powersync/sync/SyncStreamTest.kt @@ -112,7 +112,7 @@ class SyncStreamTest { } with(testLogWriter.logs[1]) { - assertEquals(message, "Error uploading crud") + assertEquals(message, "Error uploading crud: Delaying due to previously encountered CRUD item.") assertEquals(Severity.Error, severity) } } diff --git a/core/src/iosMain/kotlin/com/powersync/DatabaseDriverFactory.ios.kt b/core/src/iosMain/kotlin/com/powersync/DatabaseDriverFactory.ios.kt index cec18926..2e6ad189 100644 --- a/core/src/iosMain/kotlin/com/powersync/DatabaseDriverFactory.ios.kt +++ b/core/src/iosMain/kotlin/com/powersync/DatabaseDriverFactory.ios.kt @@ -1,7 +1,9 @@ package com.powersync import co.touchlab.sqliter.DatabaseConfiguration +import co.touchlab.sqliter.DatabaseConfiguration.Logging import co.touchlab.sqliter.DatabaseConnection +import co.touchlab.sqliter.interop.Logger import com.powersync.db.internal.InternalSchema import com.powersync.persistence.driver.NativeSqliteDriver import com.powersync.persistence.driver.wrapConnection @@ -51,6 +53,23 @@ public actual class DatabaseDriverFactory { dbFilename: String, ): PsSqlDriver { val schema = InternalSchema + val sqlLogger = + object : Logger { + override val eActive: Boolean + get() = false + override val vActive: Boolean + get() = false + + override fun eWrite( + message: String, + exception: Throwable?, + ) {} + + override fun trace(message: String) {} + + override fun vWrite(message: String) {} + } + this.driver = PsSqlDriver( scope = scope, @@ -61,6 +80,7 @@ public actual class DatabaseDriverFactory { name = dbFilename, version = schema.version.toInt(), create = { connection -> wrapConnection(connection) { schema.create(it) } }, + loggingConfig = Logging(logger = sqlLogger), lifecycleConfig = DatabaseConfiguration.Lifecycle( onCreateConnection = { connection -> diff --git a/gradle.properties b/gradle.properties index 547d7be7..3edba3b4 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-BETA20 +LIBRARY_VERSION=1.0.0-BETA21 GITHUB_REPO=https://github.com/powersync-ja/powersync-kotlin.git # POM POM_URL=https://github.com/powersync-ja/powersync-kotlin/