Skip to content

Commit ac296e3

Browse files
feat: add SqlCursor function improvements (#113)
1 parent ce6c28b commit ac296e3

File tree

8 files changed

+95
-46
lines changed

8 files changed

+95
-46
lines changed

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
# Changelog
22

3+
## 1.0.0-BETA20
4+
* Add cursor optional functions: `getStringOptional`, `getLongOptional`, `getDoubleOptional`, `getBooleanOptional` and `getBytesOptional` when using the column name which allow for optional return types
5+
* Throw errors for invalid column on all cursor functions
6+
* `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
7+
38
## 1.0.0-BETA19
49

5-
* Allow cursor to columns by name
10+
* Allow cursor to get values by column name e.g. `getStringOptional("id")`
611
* BREAKING CHANGE: If you were using `SqlCursor` from SqlDelight previously for your own custom mapper then you must now change to `SqlCursor` exported by the PowerSync module.
712

813
Previously you would import it like this:

core/src/commonMain/kotlin/com/powersync/db/SqlCursor.kt

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

3+
import co.touchlab.skie.configuration.annotations.FunctionInterop
4+
35
public interface SqlCursor {
46
public fun getBoolean(index: Int): Boolean?
57

@@ -18,8 +20,42 @@ public interface SqlCursor {
1820
public val columnNames: Map<String, Int>
1921
}
2022

21-
public fun SqlCursor.getBoolean(name: String): Boolean? = columnNames[name]?.let { getBoolean(it) }
22-
public fun SqlCursor.getBytes(name: String): ByteArray? = columnNames[name]?.let { getBytes(it) }
23-
public fun SqlCursor.getDouble(name: String): Double? = columnNames[name]?.let { getDouble(it) }
24-
public fun SqlCursor.getLong(name: String): Long? = columnNames[name]?.let { getLong(it) }
25-
public fun SqlCursor.getString(name: String): String? = columnNames[name]?.let { getString(it) }
23+
private inline fun <T> SqlCursor.getColumnValue(
24+
name: String,
25+
getValue: (Int) -> T?,
26+
): T {
27+
val index = columnNames[name] ?: throw IllegalArgumentException("Column '$name' not found")
28+
return getValue(index) ?: throw IllegalArgumentException("Null value found for column '$name'")
29+
}
30+
31+
private inline fun <T> SqlCursor.getColumnValueOptional(
32+
name: String,
33+
getValue: (Int) -> T?,
34+
): T? = columnNames[name]?.let { getValue(it) }
35+
36+
// This causes a collision the functions created in Swift and there we need to disable this conversion
37+
@FunctionInterop.FileScopeConversion.Disabled
38+
public fun SqlCursor.getBoolean(name: String): Boolean = getColumnValue(name) { getBoolean(it) }
39+
40+
@FunctionInterop.FileScopeConversion.Disabled
41+
public fun SqlCursor.getBytes(name: String): ByteArray = getColumnValue(name) { getBytes(it) }
42+
43+
@FunctionInterop.FileScopeConversion.Disabled
44+
public fun SqlCursor.getDouble(name: String): Double = getColumnValue(name) { getDouble(it) }
45+
46+
@FunctionInterop.FileScopeConversion.Disabled
47+
public fun SqlCursor.getLong(name: String): Long = getColumnValue(name) { getLong(it) }
48+
49+
@FunctionInterop.FileScopeConversion.Disabled
50+
public fun SqlCursor.getString(name: String): String = getColumnValue(name) { getString(it) }
51+
52+
public fun SqlCursor.getBooleanOptional(name: String): Boolean? = getColumnValueOptional(name) { getBoolean(it) }
53+
54+
public fun SqlCursor.getBytesOptional(name: String): ByteArray? = getColumnValueOptional(name) { getBytes(it) }
55+
56+
public fun SqlCursor.getDoubleOptional(name: String): Double? = getColumnValueOptional(name) { getDouble(it) }
57+
58+
public fun SqlCursor.getLongOptional(name: String): Long? = getColumnValueOptional(name) { getLong(it) }
59+
60+
@FunctionInterop.FileScopeConversion.Disabled
61+
public fun SqlCursor.getStringOptional(name: String): String? = getColumnValueOptional(name) { getString(it) }

demos/android-supabase-todolist/app/src/main/java/com/powersync/androidexample/powersync/List.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ package com.powersync.demos.powersync
33
import androidx.lifecycle.ViewModel
44
import androidx.lifecycle.viewModelScope
55
import com.powersync.PowerSyncDatabase
6+
import com.powersync.db.getString
67
import kotlinx.coroutines.flow.Flow
78
import kotlinx.coroutines.flow.MutableStateFlow
89
import kotlinx.coroutines.flow.StateFlow
910
import kotlinx.coroutines.launch
10-
import kotlinx.coroutines.runBlocking
1111

1212
internal class ListContent(
1313
private val db: PowerSyncDatabase,
@@ -30,10 +30,10 @@ internal class ListContent(
3030
GROUP BY $LISTS_TABLE.id
3131
""") { cursor ->
3232
ListItem(
33-
id = cursor.getString(0)!!,
34-
createdAt = cursor.getString(1)!!,
35-
name = cursor.getString(2)!!,
36-
ownerId = cursor.getString(3)!!
33+
id = cursor.getString("id"),
34+
createdAt = cursor.getString("created_at"),
35+
name = cursor.getString("name"),
36+
ownerId = cursor.getString("owner_id")
3737
)
3838
}
3939
}

demos/android-supabase-todolist/app/src/main/java/com/powersync/androidexample/powersync/Todo.kt

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import androidx.lifecycle.ViewModel
44
import androidx.lifecycle.viewModelScope
55
import co.touchlab.kermit.Logger
66
import com.powersync.PowerSyncDatabase
7+
import com.powersync.db.getLongOptional
8+
import com.powersync.db.getString
9+
import com.powersync.db.getStringOptional
710
import kotlinx.coroutines.flow.Flow
811
import kotlinx.coroutines.flow.MutableStateFlow
912
import kotlinx.coroutines.flow.StateFlow
@@ -31,15 +34,15 @@ internal class Todo(
3134
if(listId != null) listOf(listId) else null
3235
) { cursor ->
3336
TodoItem(
34-
id = cursor.getString(0)!!,
35-
createdAt = cursor.getString(1),
36-
completedAt = cursor.getString(2),
37-
description = cursor.getString(3)!!,
38-
createdBy = cursor.getString(4),
39-
completedBy = cursor.getString(5),
40-
completed = cursor.getLong(6) == 1L,
41-
listId = cursor.getString(7)!!,
42-
photoId = cursor.getString(8)
37+
id = cursor.getString("id"),
38+
createdAt = cursor.getStringOptional("created_at"),
39+
completedAt = cursor.getStringOptional("completed_at"),
40+
description = cursor.getString("description"),
41+
createdBy = cursor.getStringOptional("created_by"),
42+
completedBy = cursor.getStringOptional("completed_by"),
43+
completed = cursor.getLongOptional("completed") == 1L,
44+
listId = cursor.getString("list_id"),
45+
photoId = cursor.getStringOptional("photo_id")
4346
)
4447
}
4548
}

demos/hello-powersync/composeApp/src/commonMain/kotlin/com/powersync/demos/PowerSync.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.powersync.demos
33
import com.powersync.DatabaseDriverFactory
44
import com.powersync.PowerSyncDatabase
55
import com.powersync.connector.supabase.SupabaseConnector
6+
import com.powersync.db.getString
67
import kotlinx.coroutines.flow.Flow
78
import kotlinx.coroutines.runBlocking
89

@@ -18,7 +19,7 @@ class PowerSync(
1819
private val database = PowerSyncDatabase(driverFactory, AppSchema)
1920

2021
val db: PowerSyncDatabase
21-
get() = database;
22+
get() = database
2223

2324
init {
2425
runBlocking {
@@ -38,9 +39,9 @@ class PowerSync(
3839
fun watchUsers(): Flow<List<User>> {
3940
return database.watch("SELECT * FROM customers", mapper = { cursor ->
4041
User(
41-
id = cursor.getString(0)!!,
42-
name = cursor.getString(1)!!,
43-
email = cursor.getString(2)!!
42+
id = cursor.getString("id"),
43+
name = cursor.getString("name"),
44+
email = cursor.getString("email")
4445
)
4546
})
4647
}

demos/supabase-todolist/shared/src/commonMain/kotlin/com/powersync/demos/powersync/List.kt

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,40 +3,41 @@ package com.powersync.demos.powersync
33
import androidx.lifecycle.ViewModel
44
import androidx.lifecycle.viewModelScope
55
import com.powersync.PowerSyncDatabase
6+
import com.powersync.db.getString
67
import kotlinx.coroutines.flow.Flow
78
import kotlinx.coroutines.flow.MutableStateFlow
89
import kotlinx.coroutines.flow.StateFlow
910
import kotlinx.coroutines.launch
10-
import kotlinx.coroutines.runBlocking
1111

1212
internal class ListContent(
1313
private val db: PowerSyncDatabase,
14-
private val userId: String?
15-
): ViewModel() {
14+
private val userId: String?,
15+
) : ViewModel() {
1616
private val _selectedListId = MutableStateFlow<String?>(null)
1717
val selectedListId: StateFlow<String?> = _selectedListId
1818

1919
private val _inputText = MutableStateFlow<String>("")
2020
val inputText: StateFlow<String> = _inputText
2121

22-
fun watchItems(): Flow<List<ListItem>> {
23-
return db.watch("""
22+
fun watchItems(): Flow<List<ListItem>> =
23+
db.watch(
24+
"""
2425
SELECT
2526
*
2627
FROM
2728
$LISTS_TABLE
2829
LEFT JOIN $TODOS_TABLE
2930
ON $LISTS_TABLE.id = $TODOS_TABLE.list_id
3031
GROUP BY $LISTS_TABLE.id
31-
""") { cursor ->
32+
""",
33+
) { cursor ->
3234
ListItem(
33-
id = cursor.getString(0)!!,
34-
createdAt = cursor.getString(1)!!,
35-
name = cursor.getString(2)!!,
36-
ownerId = cursor.getString(3)!!
35+
id = cursor.getString("id"),
36+
createdAt = cursor.getString("created_at"),
37+
name = cursor.getString("name"),
38+
ownerId = cursor.getString("owner_id"),
3739
)
3840
}
39-
}
4041

4142
fun onItemDeleteClicked(item: ListItem) {
4243
viewModelScope.launch {
@@ -54,7 +55,7 @@ internal class ListContent(
5455
db.writeTransaction { tx ->
5556
tx.execute(
5657
"INSERT INTO $LISTS_TABLE (id, created_at, name, owner_id) VALUES (uuid(), datetime(), ?, ?)",
57-
listOf(_inputText.value, userId)
58+
listOf(_inputText.value, userId),
5859
)
5960
}
6061
_inputText.value = ""

demos/supabase-todolist/shared/src/commonMain/kotlin/com/powersync/demos/powersync/Todo.kt

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import androidx.lifecycle.ViewModel
44
import androidx.lifecycle.viewModelScope
55
import co.touchlab.kermit.Logger
66
import com.powersync.PowerSyncDatabase
7+
import com.powersync.db.getLongOptional
8+
import com.powersync.db.getString
9+
import com.powersync.db.getStringOptional
710
import kotlinx.coroutines.flow.Flow
811
import kotlinx.coroutines.flow.MutableStateFlow
912
import kotlinx.coroutines.flow.StateFlow
@@ -31,15 +34,15 @@ internal class Todo(
3134
if(listId != null) listOf(listId) else null
3235
) { cursor ->
3336
TodoItem(
34-
id = cursor.getString(0)!!,
35-
createdAt = cursor.getString(1),
36-
completedAt = cursor.getString(2),
37-
description = cursor.getString(3)!!,
38-
createdBy = cursor.getString(4),
39-
completedBy = cursor.getString(5),
40-
completed = cursor.getLong(6) == 1L,
41-
listId = cursor.getString(7)!!,
42-
photoId = cursor.getString(8)
37+
id = cursor.getString("id"),
38+
createdAt = cursor.getStringOptional("created_at"),
39+
completedAt = cursor.getStringOptional("completed_at"),
40+
description = cursor.getString("description"),
41+
createdBy = cursor.getStringOptional("created_by"),
42+
completedBy = cursor.getStringOptional("completed_by"),
43+
completed = cursor.getLongOptional("completed") == 1L,
44+
listId = cursor.getString("list_id"),
45+
photoId = cursor.getStringOptional("photo_id"),
4346
)
4447
}
4548
}

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ development=true
1717
RELEASE_SIGNING_ENABLED=true
1818
# Library config
1919
GROUP=com.powersync
20-
LIBRARY_VERSION=1.0.0-BETA19
20+
LIBRARY_VERSION=1.0.0-BETA20
2121
GITHUB_REPO=https://github.com/powersync-ja/powersync-kotlin.git
2222
# POM
2323
POM_URL=https://github.com/powersync-ja/powersync-kotlin/

0 commit comments

Comments
 (0)