Skip to content

Commit 8830a8f

Browse files
committed
Merge remote-tracking branch 'origin/main' into rust-sync-client
2 parents e002c26 + 4e95443 commit 8830a8f

File tree

16 files changed

+368
-123
lines changed

16 files changed

+368
-123
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# Changelog
22

3+
## 1.1.2 (pending)
4+
5+
* [Android, JVM] Use version `0.4.0` of `powersync-sqlite-core`.
6+
7+
## 1.1.1
8+
9+
* Fix reported progress around compactions / defrags on the sync service.
10+
* [Android] Set `temp_store_directory`, avoiding crashes for large materialized views.
11+
312
## 1.1.0
413

514
* Add `trackPreviousValues` option on `Table` which sets `CrudEntry.previousValues` to previous values on updates.

core-tests-android/src/androidTest/java/com/powersync/AndroidDatabaseTest.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,4 +222,14 @@ class AndroidDatabaseTest {
222222
// The exception messages differ slightly between drivers
223223
assertEquals(exception.message!!.contains("write a readonly database"), true)
224224
}
225+
226+
@Test
227+
fun canUseTempStore() = runTest {
228+
database.execute("PRAGMA temp_store = 1;") // Store temporary data as files
229+
database.execute("CREATE TEMP TABLE foo (bar TEXT);")
230+
val data = "new row".repeat(100);
231+
for (i in 0..10000) {
232+
database.execute("INSERT INTO foo VALUES (?)", parameters = listOf(data))
233+
}
234+
}
225235
}

core/src/androidMain/kotlin/com/powersync/DatabaseDriverFactory.android.kt

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import com.powersync.db.internal.InternalSchema
77
import com.powersync.db.migrateDriver
88
import kotlinx.coroutines.CoroutineScope
99
import org.sqlite.SQLiteCommitListener
10+
import java.util.concurrent.atomic.AtomicBoolean
1011

1112
@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
1213
public actual class DatabaseDriverFactory(
@@ -27,10 +28,22 @@ public actual class DatabaseDriverFactory(
2728
context.getDatabasePath(dbFilename)
2829
}
2930

31+
val properties = buildDefaultWalProperties(readOnly = readOnly)
32+
val isFirst = IS_FIRST_CONNECTION.getAndSet(false)
33+
if (isFirst) {
34+
// Make sure the temp_store_directory points towards a temporary directory we actually
35+
// have access to. Due to sandboxing, the default /tmp/ is inaccessible.
36+
// The temp_store_directory pragma is deprecated and not thread-safe, so we only set it
37+
// on the first connection (it sets a global field and will affect every connection
38+
// opened).
39+
val escapedPath = context.cacheDir.absolutePath.replace("\"", "\"\"")
40+
properties.setProperty("temp_store_directory", "\"$escapedPath\"")
41+
}
42+
3043
val driver =
3144
JdbcSqliteDriver(
3245
url = "jdbc:sqlite:$dbPath",
33-
properties = buildDefaultWalProperties(readOnly = readOnly),
46+
properties = properties,
3447
)
3548

3649
migrateDriver(driver, schema)
@@ -59,4 +72,8 @@ public actual class DatabaseDriverFactory(
5972

6073
return mappedDriver
6174
}
75+
76+
private companion object {
77+
val IS_FIRST_CONNECTION = AtomicBoolean(true)
78+
}
6279
}

core/src/commonIntegrationTest/kotlin/com/powersync/sync/SyncProgressTest.kt

Lines changed: 70 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@ import com.powersync.bucket.OplogEntry
1010
import com.powersync.testutils.ActiveDatabaseTest
1111
import com.powersync.testutils.databaseTest
1212
import com.powersync.testutils.waitFor
13+
import io.kotest.assertions.withClue
14+
import io.kotest.matchers.properties.shouldHaveValue
1315
import kotlinx.coroutines.channels.Channel
1416
import kotlin.test.BeforeTest
1517
import kotlin.test.Test
16-
import kotlin.test.assertEquals
1718
import kotlin.test.assertFalse
1819
import kotlin.test.assertNull
1920
import kotlin.test.assertTrue
@@ -80,6 +81,16 @@ abstract class BaseSyncProgressTest(
8081
}
8182
}
8283

84+
private fun ProgressWithOperations.shouldBe(
85+
downloaded: Int,
86+
total: Int,
87+
) {
88+
withClue("progress $downloadedOperations/$totalOperations should be $downloaded/$total") {
89+
this::downloadedOperations shouldHaveValue downloaded
90+
this::totalOperations shouldHaveValue total
91+
}
92+
}
93+
8394
private suspend fun ReceiveTurbine<SyncStatusData>.expectProgress(
8495
total: Pair<Int, Int>,
8596
priorities: Map<BucketPriority, Pair<Int, Int>> = emptyMap(),
@@ -88,14 +99,12 @@ abstract class BaseSyncProgressTest(
8899
val progress = item.downloadProgress ?: error("Expected download progress on $item")
89100

90101
assertTrue { item.downloading }
91-
assertEquals(total.first, progress.downloadedOperations)
92-
assertEquals(total.second, progress.totalOperations)
102+
progress.shouldBe(total.first, total.second)
93103

94104
priorities.forEach { (priority, expected) ->
95105
val (expectedDownloaded, expectedTotal) = expected
96-
val progress = progress.untilPriority(priority)
97-
assertEquals(expectedDownloaded, progress.downloadedOperations)
98-
assertEquals(expectedTotal, progress.totalOperations)
106+
val actualProgress = progress.untilPriority(priority)
107+
actualProgress.shouldBe(expectedDownloaded, expectedTotal)
99108
}
100109
}
101110

@@ -278,6 +287,61 @@ abstract class BaseSyncProgressTest(
278287
syncLines.close()
279288
}
280289

290+
@Test
291+
fun interruptedWithDefrag() =
292+
databaseTest {
293+
database.connect(connector)
294+
295+
turbineScope {
296+
val turbine = database.currentStatus.asFlow().testIn(this)
297+
turbine.waitFor { it.connected && !it.downloading }
298+
syncLines.send(
299+
SyncLine.FullCheckpoint(
300+
Checkpoint(
301+
lastOpId = "10",
302+
checksums = listOf(bucket("a", 10)),
303+
),
304+
),
305+
)
306+
turbine.expectProgress(0 to 10)
307+
308+
addDataLine("a", 5)
309+
turbine.expectProgress(5 to 10)
310+
311+
turbine.cancel()
312+
}
313+
314+
// Close and re-connect
315+
database.close()
316+
syncLines.close()
317+
database = openDatabase()
318+
syncLines = Channel()
319+
database.connect(connector)
320+
321+
turbineScope {
322+
val turbine = database.currentStatus.asFlow().testIn(this)
323+
turbine.waitFor { it.connected && !it.downloading }
324+
325+
// A sync rule deploy could reset buckets, making the new bucket smaller than the
326+
// existing one.
327+
syncLines.send(
328+
SyncLine.FullCheckpoint(
329+
Checkpoint(
330+
lastOpId = "14",
331+
checksums = listOf(bucket("a", 4)),
332+
),
333+
),
334+
)
335+
336+
// In this special case, don't report 5/4 as progress
337+
turbine.expectProgress(0 to 4)
338+
turbine.cancel()
339+
}
340+
341+
database.close()
342+
syncLines.close()
343+
}
344+
281345
@Test
282346
fun differentPriorities() =
283347
databaseTest {

core/src/commonMain/kotlin/com/powersync/sync/Progress.kt

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.powersync.sync
33
import com.powersync.bucket.BucketPriority
44
import com.powersync.bucket.Checkpoint
55
import com.powersync.bucket.LocalOperationCounters
6+
import kotlin.math.min
67

78
/**
89
* Information about a progressing download.
@@ -77,11 +78,19 @@ public data class SyncDownloadProgress internal constructor(
7778
downloadedOperations = completed
7879
}
7980

81+
/**
82+
* Creates download progress information from the local progress counters since the last full sync and the target
83+
* checkpoint.
84+
*/
8085
@LegacySyncImplementation
8186
internal constructor(localProgress: Map<String, LocalOperationCounters>, target: Checkpoint) : this(
8287
buildMap {
88+
var invalidated = false
89+
8390
for (entry in target.checksums) {
8491
val savedProgress = localProgress[entry.bucket]
92+
val atLast = savedProgress?.atLast ?: 0
93+
val sinceLast = savedProgress?.sinceLast ?: 0
8594

8695
put(
8796
entry.bucket,
@@ -92,6 +101,22 @@ public data class SyncDownloadProgress internal constructor(
92101
targetCount = (entry.count ?: 0).toLong(),
93102
),
94103
)
104+
105+
entry.count?.let { knownCount ->
106+
if (knownCount < atLast + sinceLast) {
107+
// Either due to a defrag / sync rule deploy or a compaction operation, the
108+
// size of the bucket shrank so much that the local ops exceed the ops in
109+
// the updated bucket. We can't possibly report progress in this case (it
110+
// would overshoot 100%).
111+
invalidated = true
112+
}
113+
}
114+
}
115+
116+
if (invalidated) {
117+
for ((key, value) in entries) {
118+
put(key, value.copy(sinceLast = 0, atLast = 0))
119+
}
95120
}
96121
},
97122
)
@@ -118,7 +143,7 @@ public data class SyncDownloadProgress internal constructor(
118143
put(
119144
bucket.bucket,
120145
previous.copy(
121-
sinceLast = previous.sinceLast + bucket.data.size,
146+
sinceLast = min(previous.sinceLast + bucket.data.size, previous.total),
122147
),
123148
)
124149
}

core/src/commonMain/kotlin/com/powersync/sync/SyncStream.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import kotlinx.coroutines.flow.flow
4848
import kotlinx.coroutines.launch
4949
import kotlinx.coroutines.withContext
5050
import kotlinx.datetime.Clock
51+
import kotlinx.serialization.encodeToString
5152
import kotlinx.serialization.json.JsonElement
5253
import kotlinx.serialization.json.JsonObject
5354
import kotlinx.serialization.json.encodeToJsonElement
@@ -635,8 +636,8 @@ internal class SyncStream(
635636
state: SyncStreamState,
636637
): SyncStreamState {
637638
val batch = SyncDataBatch(listOf(data))
638-
status.update { copy(downloading = true, downloadProgress = downloadProgress?.incrementDownloaded(batch)) }
639639
bucketStorage.saveSyncData(batch)
640+
status.update { copy(downloading = true, downloadProgress = downloadProgress?.incrementDownloaded(batch)) }
640641
return state
641642
}
642643

demos/hello-powersync/composeApp/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ kotlin {
2929

3030
pod("powersync-sqlite-core") {
3131
linkOnly = true
32-
version = "0.3.12"
32+
version = "0.4.0"
3333
}
3434

3535
framework {

demos/supabase-todolist/gradle.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
kotlin.code.style=official
22
xcodeproj=./iosApp
33
android.useAndroidX=true
4+
org.gradle.caching=true
45
org.gradle.jvmargs=-Xmx3g
56
org.jetbrains.compose.experimental.jscanvas.enabled=true
67
org.jetbrains.compose.experimental.macos.enabled=true

demos/supabase-todolist/shared/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ kotlin {
2727
ios.deploymentTarget = "14.1"
2828
podfile = project.file("../iosApp/Podfile")
2929
pod("powersync-sqlite-core") {
30-
version = "0.3.12"
30+
version = "0.4.0"
3131
linkOnly = true
3232
}
3333

gradle.properties

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
kotlin.code.style=official
22
# Gradle
33
org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M"
4+
org.gradle.caching=true
45
# Compose
56
org.jetbrains.compose.experimental.uikit.enabled=true
67
# Android
@@ -17,7 +18,7 @@ development=true
1718
RELEASE_SIGNING_ENABLED=true
1819
# Library config
1920
GROUP=com.powersync
20-
LIBRARY_VERSION=1.1.0
21+
LIBRARY_VERSION=1.1.1
2122
GITHUB_REPO=https://github.com/powersync-ja/powersync-kotlin.git
2223
# POM
2324
POM_URL=https://github.com/powersync-ja/powersync-kotlin/

gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ kotlinx-io = "0.5.4"
1818
ktor = "3.1.0"
1919
rsocket = "0.20.0"
2020
uuid = "0.8.2"
21-
powersync-core = "0.3.14"
21+
powersync-core = "0.4.0"
2222
sqlite-jdbc = "3.49.1.0"
2323
sqliter = "1.3.1"
2424
turbine = "1.2.0"

0 commit comments

Comments
 (0)