From 433a2dc26516da84e432aadbc7a5f4248f20c800 Mon Sep 17 00:00:00 2001 From: DominicGBauer Date: Tue, 3 Dec 2024 13:08:03 +0200 Subject: [PATCH 1/6] fix: tranasactions resulting in error when using different threads --- .../Kotlin/KotlinPowerSyncDatabaseImpl.swift | 54 +++--- Sources/PowerSyncSwift/QueriesProtocol.swift | 41 ++++- .../KotlinPowerSyncDatabaseImplTests.swift | 172 ++++++++++++++++++ 3 files changed, 232 insertions(+), 35 deletions(-) create mode 100644 Tests/PowerSyncSwiftTests/Kotlin/KotlinPowerSyncDatabaseImplTests.swift diff --git a/Sources/PowerSyncSwift/Kotlin/KotlinPowerSyncDatabaseImpl.swift b/Sources/PowerSyncSwift/Kotlin/KotlinPowerSyncDatabaseImpl.swift index 6fd3b22..b5e8886 100644 --- a/Sources/PowerSyncSwift/Kotlin/KotlinPowerSyncDatabaseImpl.swift +++ b/Sources/PowerSyncSwift/Kotlin/KotlinPowerSyncDatabaseImpl.swift @@ -117,30 +117,18 @@ final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol { } } - func writeTransaction(callback: @escaping (any PowerSyncTransactionProtocol) async throws -> R) async throws -> R { - let wrappedCallback = SuspendTaskWrapper { [kmpDatabase] in - // Create a wrapper that converts the KMP transaction to our Swift protocol - if let kmpTransaction = kmpDatabase as? PowerSyncTransactionProtocol { - return try await callback(kmpTransaction) - } else { - throw PowerSyncError.invalidTransaction - } - } - - return try await kmpDatabase.writeTransaction(callback: wrappedCallback) as! R + @MainActor + public func writeTransaction(callback: @escaping (any PowerSyncTransaction) async throws -> R) async throws -> R { + return try await kmpDatabase.writeTransaction(callback: SuspendTaskWrapper { transaction in + return try await callback(transaction) + }) as! R } - func readTransaction(callback: @escaping (any PowerSyncTransactionProtocol) async throws -> R) async throws -> R { - let wrappedCallback = SuspendTaskWrapper { [kmpDatabase] in - // Create a wrapper that converts the KMP transaction to our Swift protocol - if let kmpTransaction = kmpDatabase as? PowerSyncTransactionProtocol { - return try await callback(kmpTransaction) - } else { - throw PowerSyncError.invalidTransaction - } - } - - return try await kmpDatabase.readTransaction(callback: wrappedCallback) as! R + @MainActor + public func readTransaction(callback: @escaping (any PowerSyncTransaction) async throws -> R) async throws -> R { + return try await kmpDatabase.writeTransaction(callback: SuspendTaskWrapper { transaction in + return try await callback(transaction) + }) as! R } } @@ -148,21 +136,23 @@ enum PowerSyncError: Error { case invalidTransaction } +@MainActor class SuspendTaskWrapper: KotlinSuspendFunction1 { - let handle: () async throws -> Any + let handle: (any PowerSyncTransaction) async throws -> Any - init(_ handle: @escaping () async throws -> Any) { + init(_ handle: @escaping (any PowerSyncTransaction) async throws -> Any) { self.handle = handle } - @MainActor - func invoke(p1: Any?, completionHandler: @escaping (Any?, Error?) -> Void) { - Task { - do { - let result = try await self.handle() - completionHandler(result, nil) - } catch { - completionHandler(nil, error) + nonisolated func __invoke(p1: Any?, completionHandler: @escaping (Any?, Error?) -> Void) { + DispatchQueue.main.async { + Task { @MainActor in + do { + let result = try await self.handle(p1 as! any PowerSyncTransaction) + completionHandler(result, nil) + } catch { + completionHandler(nil, error) + } } } } diff --git a/Sources/PowerSyncSwift/QueriesProtocol.swift b/Sources/PowerSyncSwift/QueriesProtocol.swift index 2aa69ee..1b3f9cf 100644 --- a/Sources/PowerSyncSwift/QueriesProtocol.swift +++ b/Sources/PowerSyncSwift/QueriesProtocol.swift @@ -1,5 +1,6 @@ import Foundation import Combine +import PowerSync public protocol Queries { /// Execute a write query (INSERT, UPDATE, DELETE) @@ -37,8 +38,42 @@ public protocol Queries { ) -> AsyncStream<[RowType]> /// Execute a write transaction with the given callback - func writeTransaction(callback: @escaping (PowerSyncTransactionProtocol) async throws -> R) async throws -> R - + func writeTransaction(callback: @escaping (any PowerSyncTransaction) async throws -> R) async throws -> R + /// Execute a read transaction with the given callback - func readTransaction(callback: @escaping (PowerSyncTransactionProtocol) async throws -> R) async throws -> R + func readTransaction(callback: @escaping (any PowerSyncTransaction) async throws -> R) async throws -> R +} + +extension Queries { + public func execute(_ sql: String) async throws -> Int64 { + return try await execute(sql: sql, parameters: []) + } + + public func get( + _ sql: String, + mapper: @escaping (SqlCursor) -> RowType + ) async throws -> RowType { + return try await get(sql: sql, parameters: [], mapper: mapper) + } + + public func getAll( + _ sql: String, + mapper: @escaping (SqlCursor) -> RowType + ) async throws -> [RowType] { + return try await getAll(sql: sql, parameters: [], mapper: mapper) + } + + public func getOptional( + _ sql: String, + mapper: @escaping (SqlCursor) -> RowType + ) async throws -> RowType? { + return try await getOptional(sql: sql, parameters: [], mapper: mapper) + } + + public func watch( + _ sql: String, + mapper: @escaping (SqlCursor) -> RowType + ) -> AsyncStream<[RowType]> { + return watch(sql: sql, parameters: [], mapper: mapper) + } } diff --git a/Tests/PowerSyncSwiftTests/Kotlin/KotlinPowerSyncDatabaseImplTests.swift b/Tests/PowerSyncSwiftTests/Kotlin/KotlinPowerSyncDatabaseImplTests.swift new file mode 100644 index 0000000..15d6732 --- /dev/null +++ b/Tests/PowerSyncSwiftTests/Kotlin/KotlinPowerSyncDatabaseImplTests.swift @@ -0,0 +1,172 @@ +import XCTest +@testable import PowerSyncSwift + +final class KotlinPowerSyncDatabaseImplTests: XCTestCase { + private var database: KotlinPowerSyncDatabaseImpl! + private var schema: Schema! + + override func setUp() async throws { + try await super.setUp() + schema = Schema(tables: [ + Table(name: "users", columns: [ + .text("name"), + .text("email") + ]) + ]) + + database = KotlinPowerSyncDatabaseImpl( + schema: schema, + dbFilename: ":memory:" + ) + } + + 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"] + ) { cursor in + ( + cursor.getString(index: 0)!, + cursor.getString(index: 1)!, + 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 = ?", + parameters: ["999"] + ) { 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") + var results: [[String]] = [] + + 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 { + results.append(names) + if results.count == 2 { + expectation.fulfill() + } + } + } + + _ = 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() + + XCTAssertEqual(results.count, 2) + XCTAssertEqual(results[1], ["User 1", "User 2"]) + } + + @MainActor + func testWriteTransaction() async throws { + 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"] + ) + } + + + let result = try await database.get( + sql: "SELECT COUNT(*) FROM users", + parameters: [] + ) { cursor in + cursor.getLong(index: 0) + } + + XCTAssertEqual(result as! Int, 1) + } + + @MainActor + 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( + sql: "SELECT COUNT(*) FROM users", + parameters: [] + ) { cursor in + cursor.getLong(index: 0) + } + + XCTAssertEqual(result as! Int, 1) + } + } +} From 3eacb0f7cd960bae9ce729e158c722f6f383daa3 Mon Sep 17 00:00:00 2001 From: DominicGBauer Date: Wed, 4 Dec 2024 13:00:30 +0200 Subject: [PATCH 2/6] fix: remove need for main thread --- .../Kotlin/KotlinPowerSyncDatabaseImpl.swift | 20 ++++++++----------- .../PowerSyncSwift/PowerSyncDatabase.swift | 1 - .../KotlinPowerSyncDatabaseImplTests.swift | 10 +++++++--- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/Sources/PowerSyncSwift/Kotlin/KotlinPowerSyncDatabaseImpl.swift b/Sources/PowerSyncSwift/Kotlin/KotlinPowerSyncDatabaseImpl.swift index b5e8886..de6b327 100644 --- a/Sources/PowerSyncSwift/Kotlin/KotlinPowerSyncDatabaseImpl.swift +++ b/Sources/PowerSyncSwift/Kotlin/KotlinPowerSyncDatabaseImpl.swift @@ -117,14 +117,12 @@ final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol { } } - @MainActor public func writeTransaction(callback: @escaping (any PowerSyncTransaction) async throws -> R) async throws -> R { return try await kmpDatabase.writeTransaction(callback: SuspendTaskWrapper { transaction in return try await callback(transaction) }) as! R } - @MainActor public func readTransaction(callback: @escaping (any PowerSyncTransaction) async throws -> R) async throws -> R { return try await kmpDatabase.writeTransaction(callback: SuspendTaskWrapper { transaction in return try await callback(transaction) @@ -136,7 +134,6 @@ enum PowerSyncError: Error { case invalidTransaction } -@MainActor class SuspendTaskWrapper: KotlinSuspendFunction1 { let handle: (any PowerSyncTransaction) async throws -> Any @@ -144,16 +141,15 @@ class SuspendTaskWrapper: KotlinSuspendFunction1 { self.handle = handle } - nonisolated func __invoke(p1: Any?, completionHandler: @escaping (Any?, Error?) -> Void) { - DispatchQueue.main.async { - Task { @MainActor in - do { - let result = try await self.handle(p1 as! any PowerSyncTransaction) - completionHandler(result, nil) - } catch { - completionHandler(nil, error) - } + 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/PowerSyncSwift/PowerSyncDatabase.swift b/Sources/PowerSyncSwift/PowerSyncDatabase.swift index 600e346..454e7e3 100644 --- a/Sources/PowerSyncSwift/PowerSyncDatabase.swift +++ b/Sources/PowerSyncSwift/PowerSyncDatabase.swift @@ -8,7 +8,6 @@ public let DEFAULT_DB_FILENAME = "powersync.db" /// - schema: The database schema /// - dbFilename: The database filename. Defaults to "powersync.db" /// - Returns: A configured PowerSyncDatabase instance -@MainActor public func PowerSyncDatabase( schema: Schema, dbFilename: String = DEFAULT_DB_FILENAME diff --git a/Tests/PowerSyncSwiftTests/Kotlin/KotlinPowerSyncDatabaseImplTests.swift b/Tests/PowerSyncSwiftTests/Kotlin/KotlinPowerSyncDatabaseImplTests.swift index 15d6732..b8fed6f 100644 --- a/Tests/PowerSyncSwiftTests/Kotlin/KotlinPowerSyncDatabaseImplTests.swift +++ b/Tests/PowerSyncSwiftTests/Kotlin/KotlinPowerSyncDatabaseImplTests.swift @@ -18,6 +18,7 @@ final class KotlinPowerSyncDatabaseImplTests: XCTestCase { schema: schema, dbFilename: ":memory:" ) + try await database.disconnectAndClear() } override func tearDown() async throws { @@ -130,13 +131,17 @@ final class KotlinPowerSyncDatabaseImplTests: XCTestCase { XCTAssertEqual(results[1], ["User 1", "User 2"]) } - @MainActor func testWriteTransaction() async throws { 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"] ) + + _ = try await transaction.execute( + sql: "INSERT INTO users (id, name, email) VALUES (?, ?, ?)", + parameters: ["2", "Test User 2", "test2@example.com"] + ) } @@ -147,10 +152,9 @@ final class KotlinPowerSyncDatabaseImplTests: XCTestCase { cursor.getLong(index: 0) } - XCTAssertEqual(result as! Int, 1) + XCTAssertEqual(result as! Int, 2) } - @MainActor func testReadTransaction() async throws { _ = try await database.execute( sql: "INSERT INTO users (id, name, email) VALUES (?, ?, ?)", From 56a2e0678f3fe6eeaa01c4436b8d2bf2d454fb14 Mon Sep 17 00:00:00 2001 From: DominicGBauer Date: Wed, 4 Dec 2024 13:00:48 +0200 Subject: [PATCH 3/6] fix: remove need for main thread --- Demo/PowerSyncExample.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Demo/PowerSyncExample.xcodeproj/project.pbxproj b/Demo/PowerSyncExample.xcodeproj/project.pbxproj index 6ebc829..d0ebf2c 100644 --- a/Demo/PowerSyncExample.xcodeproj/project.pbxproj +++ b/Demo/PowerSyncExample.xcodeproj/project.pbxproj @@ -14,7 +14,7 @@ 6A7315882B9854220004CB17 /* PowerSyncExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A7315872B9854220004CB17 /* PowerSyncExampleApp.swift */; }; 6A73158C2B9854240004CB17 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6A73158B2B9854240004CB17 /* Assets.xcassets */; }; 6A73158F2B9854240004CB17 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6A73158E2B9854240004CB17 /* Preview Assets.xcassets */; }; - 6A7315BB2B98BDD30004CB17 /* PowerSyncManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A7315BA2B98BDD30004CB17 /* PowerSyncManager.swift */; }; + 6A7315BB2B98BDD30004CB17 /* SystemManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A7315BA2B98BDD30004CB17 /* SystemManager.swift */; }; 6A9668FE2B9EE4FE00B05DCF /* Auth in Frameworks */ = {isa = PBXBuildFile; productRef = 6A9668FD2B9EE4FE00B05DCF /* Auth */; }; 6A9669002B9EE4FE00B05DCF /* PostgREST in Frameworks */ = {isa = PBXBuildFile; productRef = 6A9668FF2B9EE4FE00B05DCF /* PostgREST */; }; 6A9669022B9EE69500B05DCF /* Supabase in Frameworks */ = {isa = PBXBuildFile; productRef = 6A9669012B9EE69500B05DCF /* Supabase */; }; @@ -64,7 +64,7 @@ 6A7315872B9854220004CB17 /* PowerSyncExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PowerSyncExampleApp.swift; sourceTree = ""; }; 6A73158B2B9854240004CB17 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 6A73158E2B9854240004CB17 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; - 6A7315BA2B98BDD30004CB17 /* PowerSyncManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PowerSyncManager.swift; sourceTree = ""; }; + 6A7315BA2B98BDD30004CB17 /* SystemManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemManager.swift; sourceTree = ""; }; 6A9669032B9EE6FA00B05DCF /* SignInScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInScreen.swift; sourceTree = ""; }; 6ABD78662B9F2B4800558A41 /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = ""; }; 6ABD786A2B9F2C1500558A41 /* TodoListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodoListView.swift; sourceTree = ""; }; @@ -218,7 +218,7 @@ B65C4D6F2C60D58500176007 /* PowerSync */ = { isa = PBXGroup; children = ( - 6A7315BA2B98BDD30004CB17 /* PowerSyncManager.swift */, + 6A7315BA2B98BDD30004CB17 /* SystemManager.swift */, 6A4AD3842B9EE763005CBFD4 /* SupabaseConnector.swift */, 6ABD78772B9F2D2800558A41 /* Schema.swift */, B66658642C62314B00159A81 /* Lists.swift */, @@ -567,7 +567,7 @@ B66658632C621CA700159A81 /* AddTodoListView.swift in Sources */, B666585D2C620E9E00159A81 /* WifiIcon.swift in Sources */, 6A9669042B9EE6FA00B05DCF /* SignInScreen.swift in Sources */, - 6A7315BB2B98BDD30004CB17 /* PowerSyncManager.swift in Sources */, + 6A7315BB2B98BDD30004CB17 /* SystemManager.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; From d6c22bbff222fda6b46769c64425f324de0f57f0 Mon Sep 17 00:00:00 2001 From: DominicGBauer Date: Wed, 4 Dec 2024 18:21:53 +0200 Subject: [PATCH 4/6] chore: update kotlin package: --- .../xcshareddata/swiftpm/Package.resolved | 6 +++--- Package.resolved | 4 ++-- Package.swift | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Demo/PowerSyncExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Demo/PowerSyncExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 2fdd706..5000863 100644 --- a/Demo/PowerSyncExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Demo/PowerSyncExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "5d7fb7f47b01e814cbc6b4a65dfe62c7af5a96a435a0288b747750c370fcd28a", + "originHash" : "25b8cd5d97789d7e497d6a5e0b04419a426018d83f0e80ab6817b213aa976748", "pins" : [ { "identity" : "anycodable", @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/powersync-ja/powersync-kotlin.git", "state" : { - "revision" : "4186fa9a2004a4bc85a22c3f37bce4f3ebd4ff81", - "version" : "1.0.0-BETA5.0" + "revision" : "b547389faf77d0c79f30887b5d82489ee3f4de4b", + "version" : "1.0.0-BETA9.0" } }, { diff --git a/Package.resolved b/Package.resolved index b070bf8..6be4ee5 100644 --- a/Package.resolved +++ b/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/powersync-ja/powersync-kotlin.git", "state" : { - "revision" : "4186fa9a2004a4bc85a22c3f37bce4f3ebd4ff81", - "version" : "1.0.0-BETA5.0" + "revision" : "b547389faf77d0c79f30887b5d82489ee3f4de4b", + "version" : "1.0.0-BETA9.0" } }, { diff --git a/Package.swift b/Package.swift index a8e6a22..13871d4 100644 --- a/Package.swift +++ b/Package.swift @@ -17,7 +17,7 @@ let package = Package( targets: ["PowerSyncSwift"]), ], dependencies: [ - .package(url: "https://github.com/powersync-ja/powersync-kotlin.git", exact: "1.0.0-BETA5.0"), + .package(url: "https://github.com/powersync-ja/powersync-kotlin.git", exact: "1.0.0-BETA9.0"), .package(url: "https://github.com/powersync-ja/powersync-sqlite-core-swift.git", "0.3.1"..<"0.4.0"), ], targets: [ From 39e32a748ef8f0eabe8e25acb612b40619d04312 Mon Sep 17 00:00:00 2001 From: DominicGBauer Date: Wed, 4 Dec 2024 18:40:17 +0200 Subject: [PATCH 5/6] fix: failing test --- .../KotlinPowerSyncDatabaseImplTests.swift | 89 +++++++++++-------- 1 file changed, 54 insertions(+), 35 deletions(-) diff --git a/Tests/PowerSyncSwiftTests/Kotlin/KotlinPowerSyncDatabaseImplTests.swift b/Tests/PowerSyncSwiftTests/Kotlin/KotlinPowerSyncDatabaseImplTests.swift index b8fed6f..ef1e085 100644 --- a/Tests/PowerSyncSwiftTests/Kotlin/KotlinPowerSyncDatabaseImplTests.swift +++ b/Tests/PowerSyncSwiftTests/Kotlin/KotlinPowerSyncDatabaseImplTests.swift @@ -95,41 +95,60 @@ final class KotlinPowerSyncDatabaseImplTests: XCTestCase { } func testWatchTableChanges() async throws { - let expectation = XCTestExpectation(description: "Watch changes") - var results: [[String]] = [] - - 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 { - results.append(names) - if results.count == 2 { - expectation.fulfill() - } - } - } - - _ = 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() - - XCTAssertEqual(results.count, 2) - XCTAssertEqual(results[1], ["User 1", "User 2"]) - } + 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) + if await resultsStore.count() == 2 { + expectation.fulfill() + } + } + } + + _ = 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 From edc8a9aaaef6b32cb5ff1fab2612b2a1fb237131 Mon Sep 17 00:00:00 2001 From: DominicGBauer Date: Wed, 4 Dec 2024 19:51:50 +0200 Subject: [PATCH 6/6] chore: remove unused protocol --- .../PowerSyncTransactionProtocol.swift | 29 ------------------- 1 file changed, 29 deletions(-) delete mode 100644 Sources/PowerSyncSwift/PowerSyncTransactionProtocol.swift diff --git a/Sources/PowerSyncSwift/PowerSyncTransactionProtocol.swift b/Sources/PowerSyncSwift/PowerSyncTransactionProtocol.swift deleted file mode 100644 index b4b4d81..0000000 --- a/Sources/PowerSyncSwift/PowerSyncTransactionProtocol.swift +++ /dev/null @@ -1,29 +0,0 @@ -public protocol PowerSyncTransactionProtocol { - /// Execute a write query and return the number of affected rows - func execute( - sql: String, - parameters: [Any]? - ) async throws -> Int64 - - /// Execute a read-only query and return a single optional result - func getOptional( - sql: String, - parameters: [Any]?, - mapper: @escaping (SqlCursor) -> RowType - ) async throws -> RowType? - - /// Execute a read-only query and return all results - func getAll( - sql: String, - parameters: [Any]?, - mapper: @escaping (SqlCursor) -> RowType - ) async throws -> [RowType] - - /// Execute a read-only query and return a single result - /// Throws if no result is found - func get( - sql: String, - parameters: [Any]?, - mapper: @escaping (SqlCursor) -> RowType - ) async throws -> RowType -}