From 53227f5439602262a00e4dacf7cb8ec4b1e1a018 Mon Sep 17 00:00:00 2001 From: Dominic Gunther Bauer <46312751+DominicGBauer@users.noreply.github.com> Date: Thu, 30 Jan 2025 16:17:26 +0200 Subject: [PATCH] feat: update to latest kotlin and apply breaking changes (#15) --- CHANGELOG.md | 21 ++++ .../xcshareddata/swiftpm/Package.resolved | 8 +- .../Components/AddListView.swift | 2 +- .../Components/AddTodoListView.swift | 2 +- .../Components/ListView.swift | 8 +- .../Components/TodoListView.swift | 8 +- .../PowerSync/SystemManager.swift | 21 ++-- Package.resolved | 8 +- Package.swift | 5 +- .../Kotlin/KotlinPowerSyncDatabaseImpl.swift | 33 +----- Sources/PowerSync/QueriesProtocol.swift | 14 +-- .../KotlinPowerSyncDatabaseImplTests.swift | 103 +++++++++++------- 12 files changed, 125 insertions(+), 108 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1eeaa90..d3392b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog +## 1.0.0-Beta.3 + +* BREAKING CHANGE: Update underlying powersync-kotlin package to BETA18.0 which requires transactions to become synchronous as opposed to asynchronous. + ```swift + try await database.writeTransaction { transaction in + try await transaction.execute( + sql: "INSERT INTO users (id, name, email) VALUES (?, ?, ?)", + parameters: ["1", "Test User", "test@example.com"] + ) + } + ``` + to + ```swift + try await database.writeTransaction { transaction in + transaction.execute( // <- This has become synchronous + sql: "INSERT INTO users (id, name, email) VALUES (?, ?, ?)", + parameters: ["1", "Test User", "test@example.com"] + ) + } + ``` + ## 1.0.0-Beta.2 * Upgrade PowerSyncSqliteCore to 0.3.8 diff --git a/Demo/PowerSyncExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Demo/PowerSyncExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 930e30e..ca7e556 100644 --- a/Demo/PowerSyncExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Demo/PowerSyncExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/powersync-ja/powersync-kotlin.git", "state" : { - "revision" : "bedbece7be3576010830e0a7240979ec47de5526", - "version" : "1.0.0-BETA15.0" + "revision" : "074001ad7d02b2b70c77168cdf4958c08dd6121b", + "version" : "1.0.0-BETA18.0" } }, { @@ -24,8 +24,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/powersync-ja/powersync-sqlite-core-swift.git", "state" : { - "revision" : "a43d8c855a5461d0176135445eb6f425a3b38964", - "version" : "0.3.8" + "revision" : "5de629f7ddc649a1e89c64fde6113fe113fe14de", + "version" : "0.3.9" } }, { diff --git a/Demo/PowerSyncExample/Components/AddListView.swift b/Demo/PowerSyncExample/Components/AddListView.swift index ed125d7..b95f810 100644 --- a/Demo/PowerSyncExample/Components/AddListView.swift +++ b/Demo/PowerSyncExample/Components/AddListView.swift @@ -10,7 +10,7 @@ struct AddListView: View { Section { TextField("Name", text: $newList.name) Button("Save") { - Task.detached { + Task { do { try await system.insertList(newList) await completion(.success(true)) diff --git a/Demo/PowerSyncExample/Components/AddTodoListView.swift b/Demo/PowerSyncExample/Components/AddTodoListView.swift index b8eb9d1..0f37a0b 100644 --- a/Demo/PowerSyncExample/Components/AddTodoListView.swift +++ b/Demo/PowerSyncExample/Components/AddTodoListView.swift @@ -12,7 +12,7 @@ struct AddTodoListView: View { Section { TextField("Description", text: $newTodo.description) Button("Save") { - Task.detached { + Task{ do { try await system.insertTodo(newTodo, listId) await completion(.success(true)) diff --git a/Demo/PowerSyncExample/Components/ListView.swift b/Demo/PowerSyncExample/Components/ListView.swift index 3f1c4a3..e14c274 100644 --- a/Demo/PowerSyncExample/Components/ListView.swift +++ b/Demo/PowerSyncExample/Components/ListView.swift @@ -63,11 +63,9 @@ struct ListView: View { } } .task { - Task { - await system.watchLists { ls in - withAnimation { - self.lists = IdentifiedArrayOf(uniqueElements: ls) - } + await system.watchLists { ls in + withAnimation { + self.lists = IdentifiedArrayOf(uniqueElements: ls) } } } diff --git a/Demo/PowerSyncExample/Components/TodoListView.swift b/Demo/PowerSyncExample/Components/TodoListView.swift index e92dfdf..18718c6 100644 --- a/Demo/PowerSyncExample/Components/TodoListView.swift +++ b/Demo/PowerSyncExample/Components/TodoListView.swift @@ -64,11 +64,9 @@ struct TodoListView: View { } } .task { - Task { - await system.watchTodos(listId) { tds in - withAnimation { - self.todos = IdentifiedArrayOf(uniqueElements: tds) - } + await system.watchTodos(listId) { tds in + withAnimation { + self.todos = IdentifiedArrayOf(uniqueElements: tds) } } } diff --git a/Demo/PowerSyncExample/PowerSync/SystemManager.swift b/Demo/PowerSyncExample/PowerSync/SystemManager.swift index 21687b2..af1d38f 100644 --- a/Demo/PowerSyncExample/PowerSync/SystemManager.swift +++ b/Demo/PowerSyncExample/PowerSync/SystemManager.swift @@ -11,7 +11,7 @@ class SystemManager { func openDb() { db = PowerSyncDatabase(schema: schema, dbFilename: "powersync-swift.sqlite") } - + func connect() async { do { try await db.connect(connector: connector) @@ -58,12 +58,12 @@ class SystemManager { } func deleteList(id: String) async throws { - try await db.writeTransaction(callback: { transaction in - _ = try await transaction.execute( + _ = try await db.writeTransaction(callback: { transaction in + _ = transaction.execute( sql: "DELETE FROM \(LISTS_TABLE) WHERE id = ?", parameters: [id] ) - _ = try await transaction.execute( + _ = transaction.execute( sql: "DELETE FROM \(TODOS_TABLE) WHERE list_id = ?", parameters: [id] ) @@ -116,14 +116,11 @@ class SystemManager { } func deleteTodo(id: String) async throws { - try await db.writeTransaction(callback: { transaction in - _ = try await transaction.execute( - sql: "DELETE FROM \(TODOS_TABLE) WHERE id = ?", - parameters: [id] - ) - return + _ = try await db.writeTransaction(callback: { transaction in + transaction.execute( + sql: "DELETE FROM \(TODOS_TABLE) WHERE id = ?", + parameters: [id] + ) }) } } - - diff --git a/Package.resolved b/Package.resolved index ad5061e..ee1fe27 100644 --- a/Package.resolved +++ b/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/powersync-ja/powersync-kotlin.git", "state" : { - "revision" : "bedbece7be3576010830e0a7240979ec47de5526", - "version" : "1.0.0-BETA15.0" + "revision" : "074001ad7d02b2b70c77168cdf4958c08dd6121b", + "version" : "1.0.0-BETA18.0" } }, { @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/powersync-ja/powersync-sqlite-core-swift.git", "state" : { - "revision" : "a43d8c855a5461d0176135445eb6f425a3b38964", - "version" : "0.3.8" + "revision" : "5de629f7ddc649a1e89c64fde6113fe113fe14de", + "version" : "0.3.9" } } ], diff --git a/Package.swift b/Package.swift index 4ed9122..12396db 100644 --- a/Package.swift +++ b/Package.swift @@ -8,7 +8,6 @@ let package = Package( name: packageName, platforms: [ .iOS(.v13), - .macOS(.v10_13) ], products: [ // Products define the executables and libraries a package produces, making them visible to other packages. @@ -17,8 +16,8 @@ let package = Package( targets: ["PowerSync"]), ], dependencies: [ - .package(url: "https://github.com/powersync-ja/powersync-kotlin.git", exact: "1.0.0-BETA15.0"), - .package(url: "https://github.com/powersync-ja/powersync-sqlite-core-swift.git", "0.3.8"..<"0.4.0") + .package(url: "https://github.com/powersync-ja/powersync-kotlin.git", exact: "1.0.0-BETA18.0"), + .package(url: "https://github.com/powersync-ja/powersync-sqlite-core-swift.git", "0.3.9"..<"0.4.0") ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. diff --git a/Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift b/Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift index 8607767..ccf0244 100644 --- a/Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift +++ b/Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift @@ -122,39 +122,16 @@ final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol { } } } - - public func writeTransaction(callback: @escaping (any PowerSyncTransaction) async throws -> R) async throws -> R { - return try await kotlinDatabase.writeTransaction(callback: SuspendTaskWrapper { transaction in - return try await callback(transaction) - }) as! R + + public func writeTransaction(callback: @escaping (any PowerSyncTransaction) -> R) async throws -> R { + return try await kotlinDatabase.writeTransaction(callback: callback) as! R } - public func readTransaction(callback: @escaping (any PowerSyncTransaction) async throws -> R) async throws -> R { - return try await kotlinDatabase.writeTransaction(callback: SuspendTaskWrapper { transaction in - return try await callback(transaction) - }) as! R + public func readTransaction(callback: @escaping (any PowerSyncTransaction) -> R) async throws -> R { + return try await kotlinDatabase.readTransaction(callback: callback) as! R } } enum PowerSyncError: Error { case invalidTransaction } - -class SuspendTaskWrapper: KotlinSuspendFunction1 { - let handle: (any PowerSyncTransaction) async throws -> Any - - init(_ handle: @escaping (any PowerSyncTransaction) async throws -> Any) { - self.handle = handle - } - - func __invoke(p1: Any?, completionHandler: @escaping (Any?, Error?) -> Void) { - Task { - do { - let result = try await self.handle(p1 as! any PowerSyncTransaction) - completionHandler(result, nil) - } catch { - completionHandler(nil, error) - } - } - } -} diff --git a/Sources/PowerSync/QueriesProtocol.swift b/Sources/PowerSync/QueriesProtocol.swift index 3c0564a..5ea28bb 100644 --- a/Sources/PowerSync/QueriesProtocol.swift +++ b/Sources/PowerSync/QueriesProtocol.swift @@ -6,7 +6,7 @@ public protocol Queries { /// Execute a write query (INSERT, UPDATE, DELETE) /// Using `RETURNING *` will result in an error. func execute(sql: String, parameters: [Any]?) async throws -> Int64 - + /// Execute a read-only (SELECT) query and return a single result. /// If there is no result, throws an IllegalArgumentException. /// See `getOptional` for queries where the result might be empty. @@ -15,21 +15,21 @@ public protocol Queries { parameters: [Any]?, mapper: @escaping (SqlCursor) -> RowType ) async throws -> RowType - + /// Execute a read-only (SELECT) query and return the results. func getAll( sql: String, parameters: [Any]?, mapper: @escaping (SqlCursor) -> RowType ) async throws -> [RowType] - + /// Execute a read-only (SELECT) query and return a single optional result. func getOptional( sql: String, parameters: [Any]?, mapper: @escaping (SqlCursor) -> RowType ) async throws -> RowType? - + /// Execute a read-only (SELECT) query every time the source tables are modified /// and return the results as an array in a Publisher. func watch( @@ -37,12 +37,12 @@ public protocol Queries { parameters: [Any]?, mapper: @escaping (SqlCursor) -> RowType ) -> AsyncStream<[RowType]> - + /// Execute a write transaction with the given callback - func writeTransaction(callback: @escaping (any PowerSyncTransaction) async throws -> R) async throws -> R + func writeTransaction(callback: @escaping (any PowerSyncTransaction) -> R) async throws -> R /// Execute a read transaction with the given callback - func readTransaction(callback: @escaping (any PowerSyncTransaction) async throws -> R) async throws -> R + func readTransaction(callback: @escaping (any PowerSyncTransaction) -> R) async throws -> R } extension Queries { diff --git a/Tests/PowerSyncTests/Kotlin/KotlinPowerSyncDatabaseImplTests.swift b/Tests/PowerSyncTests/Kotlin/KotlinPowerSyncDatabaseImplTests.swift index 96678c3..1885f64 100644 --- a/Tests/PowerSyncTests/Kotlin/KotlinPowerSyncDatabaseImplTests.swift +++ b/Tests/PowerSyncTests/Kotlin/KotlinPowerSyncDatabaseImplTests.swift @@ -4,7 +4,7 @@ import XCTest final class KotlinPowerSyncDatabaseImplTests: XCTestCase { private var database: KotlinPowerSyncDatabaseImpl! private var schema: Schema! - + override func setUp() async throws { try await super.setUp() schema = Schema(tables: [ @@ -13,26 +13,26 @@ final class KotlinPowerSyncDatabaseImplTests: XCTestCase { .text("email") ]) ]) - + database = KotlinPowerSyncDatabaseImpl( schema: schema, dbFilename: ":memory:" ) try await database.disconnectAndClear() } - + override func tearDown() async throws { try await database.disconnectAndClear() database = nil try await super.tearDown() } - + func testInsertAndGet() async throws { _ = try await database.execute( sql: "INSERT INTO users (id, name, email) VALUES (?, ?, ?)", parameters: ["1", "Test User", "test@example.com"] ) - + let user: (String, String, String) = try await database.get( sql: "SELECT id, name, email FROM users WHERE id = ?", parameters: ["1"] @@ -43,12 +43,12 @@ final class KotlinPowerSyncDatabaseImplTests: XCTestCase { cursor.getString(index: 2)! ) } - + XCTAssertEqual(user.0, "1") XCTAssertEqual(user.1, "Test User") XCTAssertEqual(user.2, "test@example.com") } - + func testGetOptional() async throws { let nonExistent: String? = try await database.getOptional( sql: "SELECT name FROM users WHERE id = ?", @@ -56,73 +56,73 @@ final class KotlinPowerSyncDatabaseImplTests: XCTestCase { ) { cursor in cursor.getString(index: 0)! } - + XCTAssertNil(nonExistent) - + _ = try await database.execute( sql: "INSERT INTO users (id, name, email) VALUES (?, ?, ?)", parameters: ["1", "Test User", "test@example.com"] ) - + let existing: String? = try await database.getOptional( sql: "SELECT name FROM users WHERE id = ?", parameters: ["1"] ) { cursor in cursor.getString(index: 0)! } - + XCTAssertEqual(existing, "Test User") } - + func testGetAll() async throws { _ = try await database.execute( sql: "INSERT INTO users (id, name, email) VALUES (?, ?, ?), (?, ?, ?)", parameters: ["1", "User 1", "user1@example.com", "2", "User 2", "user2@example.com"] ) - + let users: [(String, String)] = try await database.getAll( sql: "SELECT id, name FROM users ORDER BY id", parameters: nil ) { cursor in (cursor.getString(index: 0)!, cursor.getString(index: 1)!) } - + XCTAssertEqual(users.count, 2) XCTAssertEqual(users[0].0, "1") XCTAssertEqual(users[0].1, "User 1") XCTAssertEqual(users[1].0, "2") XCTAssertEqual(users[1].1, "User 2") } - + func testWatchTableChanges() async throws { let expectation = XCTestExpectation(description: "Watch changes") - + // Create an actor to handle concurrent mutations actor ResultsStore { private var results: [[String]] = [] - + func append(_ names: [String]) { results.append(names) } - + func getResults() -> [[String]] { results } - + func count() -> Int { results.count } } - + let resultsStore = ResultsStore() - + let stream = database.watch( sql: "SELECT name FROM users ORDER BY id", parameters: nil ) { cursor in cursor.getString(index: 0)! } - + let watchTask = Task { for await names in stream { await resultsStore.append(names) @@ -131,38 +131,38 @@ final class KotlinPowerSyncDatabaseImplTests: XCTestCase { } } } - + _ = try await database.execute( sql: "INSERT INTO users (id, name, email) VALUES (?, ?, ?)", parameters: ["1", "User 1", "user1@example.com"] ) - + _ = try await database.execute( sql: "INSERT INTO users (id, name, email) VALUES (?, ?, ?)", parameters: ["2", "User 2", "user2@example.com"] ) - + await fulfillment(of: [expectation], timeout: 5) watchTask.cancel() - + let finalResults = await resultsStore.getResults() XCTAssertEqual(finalResults.count, 2) XCTAssertEqual(finalResults[1], ["User 1", "User 2"]) } - + func testWriteTransaction() async throws { - try await database.writeTransaction { transaction in - _ = try await transaction.execute( + _ = try await database.writeTransaction { transaction in + _ = transaction.execute( sql: "INSERT INTO users (id, name, email) VALUES (?, ?, ?)", parameters: ["1", "Test User", "test@example.com"] ) - - _ = try await transaction.execute( + + _ = transaction.execute( sql: "INSERT INTO users (id, name, email) VALUES (?, ?, ?)", parameters: ["2", "Test User 2", "test2@example.com"] ) } - + let result = try await database.get( sql: "SELECT COUNT(*) FROM users", @@ -170,25 +170,52 @@ final class KotlinPowerSyncDatabaseImplTests: XCTestCase { ) { cursor in cursor.getLong(index: 0) } - + XCTAssertEqual(result as! Int, 2) } + func testWriteLongerTransaction() async throws { + let loopCount = 100 + + _ = try await database.writeTransaction { transaction in + for i in 1...loopCount { + _ = transaction.execute( + sql: "INSERT INTO users (id, name, email) VALUES (?, ?, ?)", + parameters: [String(i), "Test User \(i)", "test\(i)@example.com"] + ) + + _ = transaction.execute( + sql: "INSERT INTO users (id, name, email) VALUES (?, ?, ?)", + parameters: [String(i*10000), "Test User \(i)-2", "test\(i)-2@example.com"] + ) + } + } + + let result = try await database.get( + sql: "SELECT COUNT(*) FROM users", + parameters: [] + ) { cursor in + cursor.getLong(index: 0) + } + + XCTAssertEqual(result as! Int, 2 * loopCount) + } + func testReadTransaction() async throws { _ = try await database.execute( sql: "INSERT INTO users (id, name, email) VALUES (?, ?, ?)", parameters: ["1", "Test User", "test@example.com"] ) - - - try await database.readTransaction { transaction in - let result = try await transaction.get( + + + _ = try await database.readTransaction { transaction in + let result = transaction.get( sql: "SELECT COUNT(*) FROM users", parameters: [] ) { cursor in cursor.getLong(index: 0) } - + XCTAssertEqual(result as! Int, 1) } }