Skip to content

Commit 435344a

Browse files
committed
feat: add SqlCursor function improvements
1 parent 53227f5 commit 435344a

File tree

7 files changed

+283
-23
lines changed

7 files changed

+283
-23
lines changed

Demo/PowerSyncExample/PowerSync/SystemManager.swift

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,10 @@ class SystemManager {
3939
parameters: [],
4040
mapper: { cursor in
4141
ListContent(
42-
id: cursor.getString(index: 0)!,
43-
name: cursor.getString(index: 1)!,
44-
createdAt: cursor.getString(index: 2)!,
45-
ownerId: cursor.getString(index: 3)!
42+
id: try cursor.getString(name: "id"),
43+
name: try cursor.getString(name: "name"),
44+
createdAt: try cursor.getString(name: "created_at"),
45+
ownerId: try cursor.getString(name: "owner_id")
4646
)
4747
}
4848
) {
@@ -77,15 +77,15 @@ class SystemManager {
7777
parameters: [listId],
7878
mapper: { cursor in
7979
return Todo(
80-
id: cursor.getString(index: 0)!,
81-
listId: cursor.getString(index: 1)!,
82-
photoId: cursor.getString(index: 2),
83-
description: cursor.getString(index: 3)!,
84-
isComplete: cursor.getBoolean(index: 4)! as! Bool,
85-
createdAt: cursor.getString(index: 5),
86-
completedAt: cursor.getString(index: 6),
87-
createdBy: cursor.getString(index: 7),
88-
completedBy: cursor.getString(index: 8)
80+
id: try cursor.getString(name: "id"),
81+
listId: try cursor.getString(name: "list_id"),
82+
photoId: try cursor.getStringOptional(name: "photo_id"),
83+
description: try cursor.getString(name: "description"),
84+
isComplete: try cursor.getBoolean(name: "completed"),
85+
createdAt: try cursor.getString(name: "created_at"),
86+
completedAt: try cursor.getStringOptional(name: "completed_at"),
87+
createdBy: try cursor.getStringOptional(name: "created_by"),
88+
completedBy: try cursor.getStringOptional(name: "completed_by")
8989
)
9090
}
9191
) {

Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,21 @@ final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol {
7979
mapper: mapper
8080
) as! RowType
8181
}
82-
82+
83+
func get<RowType>(
84+
sql: String,
85+
parameters: [Any]?,
86+
mapper: @escaping (SqlCursor) throws -> RowType
87+
) async throws -> RowType {
88+
try await kotlinDatabase.get(
89+
sql: sql,
90+
parameters: parameters,
91+
mapper: { cursor in
92+
try! mapper(cursor)
93+
}
94+
) as! RowType
95+
}
96+
8397
func getAll<RowType>(
8498
sql: String,
8599
parameters: [Any]?,
@@ -91,6 +105,20 @@ final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol {
91105
mapper: mapper
92106
) as! [RowType]
93107
}
108+
109+
func getAll<RowType>(
110+
sql: String,
111+
parameters: [Any]?,
112+
mapper: @escaping (SqlCursor) throws -> RowType
113+
) async throws -> [RowType] {
114+
try await kotlinDatabase.getAll(
115+
sql: sql,
116+
parameters: parameters,
117+
mapper: { cursor in
118+
try! mapper(cursor)
119+
}
120+
) as! [RowType]
121+
}
94122

95123
func getOptional<RowType>(
96124
sql: String,
@@ -103,6 +131,20 @@ final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol {
103131
mapper: mapper
104132
) as! RowType?
105133
}
134+
135+
func getOptional<RowType>(
136+
sql: String,
137+
parameters: [Any]?,
138+
mapper: @escaping (SqlCursor) throws -> RowType
139+
) async throws -> RowType? {
140+
try await kotlinDatabase.getOptional(
141+
sql: sql,
142+
parameters: parameters,
143+
mapper: { cursor in
144+
try! mapper(cursor)
145+
}
146+
) as! RowType?
147+
}
106148

107149
func watch<RowType>(
108150
sql: String,
@@ -123,6 +165,27 @@ final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol {
123165
}
124166
}
125167

168+
func watch<RowType>(
169+
sql: String,
170+
parameters: [Any]?,
171+
mapper: @escaping (SqlCursor) throws -> RowType
172+
) -> AsyncStream<[RowType]> {
173+
AsyncStream { continuation in
174+
Task {
175+
for await values in self.kotlinDatabase.watch(
176+
sql: sql,
177+
parameters: parameters,
178+
mapper: { cursor in
179+
try! mapper(cursor)
180+
}
181+
) {
182+
continuation.yield(values as! [RowType])
183+
}
184+
continuation.finish()
185+
}
186+
}
187+
}
188+
126189
public func writeTransaction<R>(callback: @escaping (any PowerSyncTransaction) -> R) async throws -> R {
127190
return try await kotlinDatabase.writeTransaction(callback: callback) as! R
128191
}
Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import PowerSyncKotlin
22

3-
public typealias KotlinPowerSyncBackendConnector = PowerSyncKotlin.PowerSyncBackendConnector
3+
typealias KotlinPowerSyncBackendConnector = PowerSyncKotlin.PowerSyncBackendConnector
44
public typealias CrudEntry = PowerSyncKotlin.CrudEntry
55
public typealias CrudBatch = PowerSyncKotlin.CrudBatch
66
public typealias SyncStatus = PowerSyncKotlin.SyncStatus
7-
public typealias SqlCursor = PowerSyncKotlin.RuntimeSqlCursor
7+
public typealias SqlCursor = PowerSyncKotlin.SqlCursor
88
public typealias JsonParam = PowerSyncKotlin.JsonParam
99
public typealias CrudTransaction = PowerSyncKotlin.CrudTransaction
10-
public typealias KotlinPowerSyncCredentials = PowerSyncKotlin.PowerSyncCredentials
11-
public typealias KotlinPowerSyncDatabase = PowerSyncKotlin.PowerSyncDatabase
12-
10+
typealias KotlinPowerSyncCredentials = PowerSyncKotlin.PowerSyncCredentials
11+
typealias KotlinPowerSyncDatabase = PowerSyncKotlin.PowerSyncDatabase
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import Foundation
2+
import PowerSyncKotlin
3+
4+
extension SqlCursor {
5+
private func getColumnIndex(name: String) throws -> Int32 {
6+
guard let columnIndex = columnNames[name]?.int32Value else {
7+
throw SqlCursorError.columnNotFound(name)
8+
}
9+
return columnIndex
10+
}
11+
12+
private func getValue<T>(name: String, getter: (Int32) -> T?) throws -> T {
13+
let columnIndex = try getColumnIndex(name: name)
14+
guard let value = getter(columnIndex) else {
15+
throw SqlCursorError.nullValueFound(name)
16+
}
17+
return value
18+
}
19+
20+
private func getOptionalValue<T>(name: String, getter: (String) -> T?) throws -> T? {
21+
_ = try getColumnIndex(name: name)
22+
return getter(name)
23+
}
24+
25+
public func getBoolean(name: String) throws -> Bool {
26+
try getValue(name: name) { getBoolean(index: $0)?.boolValue }
27+
}
28+
29+
public func getDouble(name: String) throws -> Double {
30+
try getValue(name: name) { getDouble(index: $0)?.doubleValue }
31+
}
32+
33+
public func getLong(name: String) throws -> Int {
34+
try getValue(name: name) { getLong(index: $0)?.intValue }
35+
}
36+
37+
public func getString(name: String) throws -> String {
38+
try getValue(name: name) { getString(index: $0) }
39+
}
40+
41+
public func getBooleanOptional(name: String) throws -> Bool? {
42+
try getOptionalValue(name: name) { getBooleanOptional(name: $0)?.boolValue }
43+
}
44+
45+
public func getDoubleOptional(name: String) throws -> Double? {
46+
try getOptionalValue(name: name) { getDoubleOptional(name: $0)?.doubleValue }
47+
}
48+
49+
public func getLongOptional(name: String) throws -> Int? {
50+
try getOptionalValue(name: name) { getLongOptional(name: $0)?.intValue }
51+
}
52+
53+
public func getStringOptional(name: String) throws -> String? {
54+
try getOptionalValue(name: name) { PowerSyncKotlin.SqlCursorKt.getStringOptional(self, name: $0) }
55+
}
56+
}
57+
58+
enum SqlCursorError: Error {
59+
case nullValue(message: String)
60+
61+
static func columnNotFound(_ name: String) -> SqlCursorError {
62+
.nullValue(message: "Column '\(name)' not found")
63+
}
64+
65+
static func nullValueFound(_ name: String) -> SqlCursorError {
66+
.nullValue(message: "Null value found for column \(name)")
67+
}
68+
}

Sources/PowerSync/QueriesProtocol.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,43 @@ public protocol Queries {
1616
mapper: @escaping (SqlCursor) -> RowType
1717
) async throws -> RowType
1818

19+
/// Execute a read-only (SELECT) query and return a single result.
20+
/// If there is no result, throws an IllegalArgumentException.
21+
/// See `getOptional` for queries where the result might be empty.
22+
func get<RowType>(
23+
sql: String,
24+
parameters: [Any]?,
25+
mapper: @escaping (SqlCursor) throws -> RowType
26+
) async throws -> RowType
27+
1928
/// Execute a read-only (SELECT) query and return the results.
2029
func getAll<RowType>(
2130
sql: String,
2231
parameters: [Any]?,
2332
mapper: @escaping (SqlCursor) -> RowType
2433
) async throws -> [RowType]
2534

35+
/// Execute a read-only (SELECT) query and return the results.
36+
func getAll<RowType>(
37+
sql: String,
38+
parameters: [Any]?,
39+
mapper: @escaping (SqlCursor) throws -> RowType
40+
) async throws -> [RowType]
41+
2642
/// Execute a read-only (SELECT) query and return a single optional result.
2743
func getOptional<RowType>(
2844
sql: String,
2945
parameters: [Any]?,
3046
mapper: @escaping (SqlCursor) -> RowType
3147
) async throws -> RowType?
3248

49+
/// Execute a read-only (SELECT) query and return a single optional result.
50+
func getOptional<RowType>(
51+
sql: String,
52+
parameters: [Any]?,
53+
mapper: @escaping (SqlCursor) throws -> RowType
54+
) async throws -> RowType?
55+
3356
/// Execute a read-only (SELECT) query every time the source tables are modified
3457
/// and return the results as an array in a Publisher.
3558
func watch<RowType>(
@@ -38,6 +61,14 @@ public protocol Queries {
3861
mapper: @escaping (SqlCursor) -> RowType
3962
) -> AsyncStream<[RowType]>
4063

64+
/// Execute a read-only (SELECT) query every time the source tables are modified
65+
/// and return the results as an array in a Publisher.
66+
func watch<RowType>(
67+
sql: String,
68+
parameters: [Any]?,
69+
mapper: @escaping (SqlCursor) throws -> RowType
70+
) -> AsyncStream<[RowType]>
71+
4172
/// Execute a write transaction with the given callback
4273
func writeTransaction<R>(callback: @escaping (any PowerSyncTransaction) -> R) async throws -> R
4374

Tests/PowerSyncTests/Kotlin/KotlinPowerSyncDatabaseImplTests.swift

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,9 @@ final class KotlinPowerSyncDatabaseImplTests: XCTestCase {
3838
parameters: ["1"]
3939
) { cursor in
4040
(
41-
cursor.getString(index: 0)!,
42-
cursor.getString(index: 1)!,
43-
cursor.getString(index: 2)!
41+
try cursor.getString(name: "id"),
42+
try cursor.getString(name: "name"),
43+
try cursor.getString(name: "email")
4444
)
4545
}
4646

@@ -84,7 +84,10 @@ final class KotlinPowerSyncDatabaseImplTests: XCTestCase {
8484
sql: "SELECT id, name FROM users ORDER BY id",
8585
parameters: nil
8686
) { cursor in
87-
(cursor.getString(index: 0)!, cursor.getString(index: 1)!)
87+
(
88+
try cursor.getString(name: "id"),
89+
try cursor.getString(name: "name")
90+
)
8891
}
8992

9093
XCTAssertEqual(users.count, 2)
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import XCTest
2+
@testable import PowerSync
3+
4+
struct User {
5+
let id: String
6+
let count: Int
7+
let isActive: Bool
8+
let weight: Double
9+
}
10+
11+
struct UserOptional {
12+
let id: String
13+
let count: Int?
14+
let isActive: Bool?
15+
let weight: Double?
16+
let description: String?
17+
}
18+
19+
final class SqlCursorTests: XCTestCase {
20+
private var database: KotlinPowerSyncDatabaseImpl!
21+
private var schema: Schema!
22+
23+
override func setUp() async throws {
24+
try await super.setUp()
25+
schema = Schema(tables: [
26+
Table(name: "users", columns: [
27+
.text("count"),
28+
.integer("is_active"),
29+
.real("weight"),
30+
.text("description")
31+
])
32+
])
33+
34+
database = KotlinPowerSyncDatabaseImpl(
35+
schema: schema,
36+
dbFilename: ":memory:"
37+
)
38+
try await database.disconnectAndClear()
39+
}
40+
41+
override func tearDown() async throws {
42+
try await database.disconnectAndClear()
43+
database = nil
44+
try await super.tearDown()
45+
}
46+
47+
func testValidValues() async throws {
48+
_ = try await database.execute(
49+
sql: "INSERT INTO users (id, count, is_active, weight) VALUES (?, ?, ?, ?)",
50+
parameters: ["1", 110, 0, 1.1111]
51+
)
52+
53+
let user: User = try await database.get(
54+
sql: "SELECT id, count, is_active, weight FROM users WHERE id = ?",
55+
parameters: ["1"]
56+
) { cursor in
57+
User(
58+
id: try cursor.getString(name: "id"),
59+
count: try cursor.getLong(name: "count"),
60+
isActive: try cursor.getBoolean(name: "is_active"),
61+
weight: try cursor.getDouble(name: "weight")
62+
)
63+
}
64+
65+
XCTAssertEqual(user.id, "1")
66+
XCTAssertEqual(user.count, 110)
67+
XCTAssertEqual(user.isActive, false)
68+
XCTAssertEqual(user.weight, 1.1111)
69+
}
70+
71+
func testOptionalValues() async throws {
72+
_ = try await database.execute(
73+
sql: "INSERT INTO users (id, count, is_active, weight, description) VALUES (?, ?, ?, ?, ?)",
74+
parameters: ["1", nil, nil, nil, nil, nil]
75+
)
76+
77+
let user: UserOptional = try await database.get(
78+
sql: "SELECT id, count, is_active, weight, description FROM users WHERE id = ?",
79+
parameters: ["1"]
80+
) { cursor in
81+
UserOptional(
82+
id: try cursor.getString(name: "id"),
83+
count: try cursor.getLongOptional(name: "count"),
84+
isActive: try cursor.getBooleanOptional(name: "is_active"),
85+
weight: try cursor.getDoubleOptional(name: "weight"),
86+
description: try cursor.getStringOptional(name: "description")
87+
)
88+
}
89+
90+
XCTAssertEqual(user.id, "1")
91+
XCTAssertNil(user.count)
92+
XCTAssertNil(user.isActive)
93+
XCTAssertNil(user.weight)
94+
XCTAssertNil(user.description)
95+
}
96+
}

0 commit comments

Comments
 (0)