Skip to content

Commit 3e156bf

Browse files
committed
Use separate interface
1 parent c0f4c21 commit 3e156bf

File tree

7 files changed

+114
-74
lines changed

7 files changed

+114
-74
lines changed

CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
## 1.4.0 (unreleased)
44

5-
* `CrudEntry`: Add `data` and `typedPreviousValues` fields as typed variants of
6-
`opData` and `previousValues`, respectively.
5+
* `CrudEntry`: Introduce `SqliteRow` interface for `opData` and `previousValues`, providing typed
6+
access to the underlying values.
77

88
## 1.3.1
99

connectors/supabase/src/commonMain/kotlin/com/powersync/connector/supabase/SupabaseConnector.kt

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import io.ktor.client.statement.bodyAsText
2626
import io.ktor.utils.io.InternalAPI
2727
import kotlinx.coroutines.flow.StateFlow
2828
import kotlinx.serialization.json.Json
29+
import kotlinx.serialization.json.JsonPrimitive
2930

3031
/**
3132
* Get a Supabase token to authenticate against the PowerSync instance.
@@ -190,19 +191,20 @@ public class SupabaseConnector(
190191

191192
when (entry.op) {
192193
UpdateType.PUT -> {
193-
val data = entry.opData?.toMutableMap() ?: mutableMapOf()
194-
data["id"] = entry.id
194+
val data =
195+
buildMap {
196+
put("id", JsonPrimitive(entry.id))
197+
entry.opData?.jsonValues?.let { putAll(it) }
198+
}
195199
table.upsert(data)
196200
}
197-
198201
UpdateType.PATCH -> {
199-
table.update(entry.opData!!) {
202+
table.update(entry.opData!!.jsonValues) {
200203
filter {
201204
eq("id", entry.id)
202205
}
203206
}
204207
}
205-
206208
UpdateType.DELETE -> {
207209
table.delete {
208210
filter {

core/src/commonIntegrationTest/kotlin/com/powersync/CrudTest.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -126,20 +126,20 @@ class CrudTest {
126126
}
127127

128128
var batch = database.getNextCrudTransaction()!!
129-
batch.crud[0].data shouldBe
129+
batch.crud[0].opData?.typed shouldBe
130130
mapOf(
131131
"a" to "text",
132132
"b" to 42,
133133
"c" to 13.37,
134134
)
135-
batch.crud[0].typedPreviousValues shouldBe null
135+
batch.crud[0].previousValues shouldBe null
136136

137-
batch.crud[1].data shouldBe
137+
batch.crud[1].opData?.typed shouldBe
138138
mapOf(
139139
"a" to "te\"xt",
140140
"b" to null,
141141
)
142-
batch.crud[1].typedPreviousValues shouldBe
142+
batch.crud[1].previousValues?.typed shouldBe
143143
mapOf(
144144
"a" to "text",
145145
"b" to 42,
@@ -152,7 +152,7 @@ class CrudTest {
152152
)
153153

154154
batch = database.getNextCrudTransaction()!!
155-
batch.crud[0].data shouldBe
155+
batch.crud[0].opData?.typed shouldBe
156156
mapOf(
157157
"a" to "42", // Not an integer!
158158
)

core/src/commonMain/kotlin/com/powersync/db/crud/CrudEntry.kt

Lines changed: 5 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
package com.powersync.db.crud
22

3+
import com.powersync.PowerSyncDatabase
34
import com.powersync.db.schema.Table
45
import com.powersync.utils.JsonUtil
5-
import kotlinx.serialization.json.JsonElement
6-
import kotlinx.serialization.json.JsonNull
76
import kotlinx.serialization.json.jsonObject
87
import kotlinx.serialization.json.jsonPrimitive
98

@@ -58,75 +57,29 @@ public data class CrudEntry internal constructor(
5857
*
5958
* For DELETE, this is null.
6059
*/
61-
@Deprecated("Use data instead", replaceWith = ReplaceWith("data"))
62-
val opData: Map<String, String?>?,
63-
/**
64-
* Data associated with the change.
65-
*
66-
* For PUT, this is contains all non-null columns of the row.
67-
*
68-
* For PATCH, this is contains the columns that changed.
69-
*
70-
* For DELETE, this is null.
71-
*/
72-
val data: Map<String, Any?>?,
60+
val opData: SqliteRow?,
7361
/**
7462
* Previous values before this change.
7563
*
7664
* These values can be tracked for `UPDATE` statements when [Table.trackPreviousValues] is
7765
* enabled.
7866
*/
79-
@Deprecated("Use typedPreviousValues instead", replaceWith = ReplaceWith("typedPreviousValues"))
80-
val previousValues: Map<String, String?>? = null,
81-
/**
82-
* Previous values before this change.
83-
*
84-
* These values can be tracked for `UPDATE` statements when [Table.trackPreviousValues] is
85-
* enabled.
86-
*/
87-
val typedPreviousValues: Map<String, Any?>? = null,
67+
val previousValues: SqliteRow? = null,
8868
) {
8969
public companion object {
9070
public fun fromRow(row: CrudRow): CrudEntry {
9171
val data = JsonUtil.json.parseToJsonElement(row.data).jsonObject
92-
val opData = data["data"]?.asData()
93-
val previousValues = data["old"]?.asData()
94-
9572
return CrudEntry(
9673
id = data["id"]!!.jsonPrimitive.content,
9774
clientId = row.id.toInt(),
9875
op = UpdateType.fromJsonChecked(data["op"]!!.jsonPrimitive.content),
99-
opData = opData?.toStringMap(),
100-
data = opData,
76+
opData = data["data"]?.let { SerializedRow(it.jsonObject) },
10177
table = data["type"]!!.jsonPrimitive.content,
10278
transactionId = row.txId,
10379
metadata = data["metadata"]?.jsonPrimitive?.content,
104-
typedPreviousValues = previousValues,
105-
previousValues = previousValues?.toStringMap(),
80+
previousValues = data["old"]?.let { SerializedRow(it.jsonObject) },
10681
)
10782
}
108-
109-
private fun JsonElement.asData(): Map<String, Any?> =
110-
jsonObject.mapValues { (_, value) ->
111-
val primitive = value.jsonPrimitive
112-
if (primitive === JsonNull) {
113-
null
114-
} else if (primitive.isString) {
115-
primitive.content
116-
} else {
117-
primitive.content.jsonNumberOrBoolean()
118-
}
119-
}
120-
121-
private fun String.jsonNumberOrBoolean(): Any =
122-
when {
123-
this == "true" -> true
124-
this == "false" -> false
125-
this.any { char -> char == '.' || char == 'e' || char == 'E' } -> this.toDouble()
126-
else -> this.toInt()
127-
}
128-
129-
private fun Map<String, Any?>.toStringMap(): Map<String, String> = mapValues { (_, v) -> v.toString() }
13083
}
13184

13285
override fun toString(): String = "CrudEntry<$transactionId/$clientId ${op.toJson()} $table/$id $opData>"
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package com.powersync.db.crud
2+
3+
import kotlinx.serialization.json.JsonElement
4+
import kotlinx.serialization.json.JsonNull
5+
import kotlinx.serialization.json.JsonObject
6+
import kotlinx.serialization.json.JsonPrimitive
7+
import kotlinx.serialization.json.jsonPrimitive
8+
import kotlin.experimental.ExperimentalObjCRefinement
9+
import kotlin.native.HiddenFromObjC
10+
11+
/**
12+
* A named collection of values as they appear in a SQLite row.
13+
*
14+
* We represent values as a `Map<String, String?>` to ensure compatible with earlier versions of the
15+
* SDK, but the [typed] getter can be used to obtain a `Map<String, Any>` where values are either
16+
* [String]s, [Int]s or [Double]s.
17+
*/
18+
@OptIn(ExperimentalObjCRefinement::class)
19+
public interface SqliteRow : Map<String, String?> {
20+
/**
21+
* A typed view of the SQLite row.
22+
*/
23+
public val typed: Map<String, Any?>
24+
25+
/**
26+
* A [JsonObject] of all values in this row that can be represented as JSON.
27+
*/
28+
@HiddenFromObjC
29+
public val jsonValues: JsonObject
30+
}
31+
32+
/**
33+
* A [SqliteRow] implemented over a [JsonObject] view.
34+
*/
35+
internal class SerializedRow(
36+
override val jsonValues: JsonObject,
37+
) : AbstractMap<String, String?>(),
38+
SqliteRow {
39+
override val entries: Set<Map.Entry<String, String?>> =
40+
jsonValues.entries.mapTo(
41+
mutableSetOf(),
42+
::ToStringEntry,
43+
)
44+
45+
override val typed: Map<String, Any?> = TypedRow(jsonValues)
46+
}
47+
48+
private data class ToStringEntry(
49+
val inner: Map.Entry<String, JsonElement>,
50+
) : Map.Entry<String, String> {
51+
override val key: String
52+
get() = inner.key
53+
override val value: String
54+
get() = inner.value.jsonPrimitive.content
55+
}
56+
57+
private class TypedRow(
58+
inner: JsonObject,
59+
) : AbstractMap<String, Any?>() {
60+
override val entries: Set<Map.Entry<String, Any?>> =
61+
inner.entries.mapTo(
62+
mutableSetOf(),
63+
::ToTypedEntry,
64+
)
65+
}
66+
67+
private data class ToTypedEntry(
68+
val inner: Map.Entry<String, JsonElement>,
69+
) : Map.Entry<String, Any?> {
70+
override val key: String
71+
get() = inner.key
72+
override val value: Any?
73+
get() = inner.value.jsonPrimitive.asData()
74+
75+
companion object {
76+
private fun JsonPrimitive.asData(): Any? =
77+
if (this === JsonNull) {
78+
null
79+
} else if (isString) {
80+
content
81+
} else {
82+
content.jsonNumberOrBoolean()
83+
}
84+
85+
private fun String.jsonNumberOrBoolean(): Any =
86+
when {
87+
this == "true" -> true
88+
this == "false" -> false
89+
this.any { char -> char == '.' || char == 'e' || char == 'E' } -> this.toDouble()
90+
else -> this.toInt()
91+
}
92+
}
93+
}

core/src/commonTest/kotlin/com/powersync/bucket/BucketStorageTest.kt

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,7 @@ class BucketStorageTest {
7575
op = UpdateType.PUT,
7676
table = "table1",
7777
transactionId = 1,
78-
opData =
79-
mapOf(
80-
"key" to "value",
81-
),
82-
data = mapOf("key" to "value"),
78+
opData = null,
8379
)
8480
mockDb =
8581
mock<InternalDatabase> {

core/src/commonTest/kotlin/com/powersync/sync/SyncStreamTest.kt

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,7 @@ class SyncStreamTest {
9595
op = UpdateType.PUT,
9696
table = "table1",
9797
transactionId = 1,
98-
opData =
99-
mapOf(
100-
"key" to "value",
101-
),
102-
data = mapOf("key" to "value"),
98+
opData = null,
10399
)
104100
bucketStorage =
105101
mock<BucketStorage> {

0 commit comments

Comments
 (0)