From 12d24fbe8c2c16642bd0adb6a24f886d2a564df1 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Tue, 11 Nov 2025 14:41:53 -0600 Subject: [PATCH 01/14] Improvements to testability of SyncEngine. --- .../CloudKit/Internal/MockSyncEngine.swift | 116 +++++++++++++++++- Sources/SQLiteData/CloudKit/SyncEngine.swift | 16 ++- .../Internal/CloudKitTestHelpers.swift | 97 --------------- 3 files changed, 122 insertions(+), 107 deletions(-) diff --git a/Sources/SQLiteData/CloudKit/Internal/MockSyncEngine.swift b/Sources/SQLiteData/CloudKit/Internal/MockSyncEngine.swift index 31f1c02c..b400c7ac 100644 --- a/Sources/SQLiteData/CloudKit/Internal/MockSyncEngine.swift +++ b/Sources/SQLiteData/CloudKit/Internal/MockSyncEngine.swift @@ -1,5 +1,6 @@ #if canImport(CloudKit) import CloudKit +import IssueReporting import OrderedCollections @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) @@ -57,11 +58,17 @@ } package func sendChanges(_ options: CKSyncEngine.SendChangesOptions) async throws { - guard - !parentSyncEngine.syncEngine(for: database.databaseScope).state.pendingRecordZoneChanges - .isEmpty - else { return } - try await parentSyncEngine.processPendingRecordZoneChanges(scope: database.databaseScope) + + if + !parentSyncEngine.syncEngine(for: database.databaseScope).state.pendingDatabaseChanges.isEmpty { + + try await parentSyncEngine.processPendingDatabaseChanges(scope: database.databaseScope) + } + if !parentSyncEngine.syncEngine(for: database.databaseScope).state.pendingRecordZoneChanges + .isEmpty { + + try await parentSyncEngine.processPendingRecordZoneChanges(scope: database.databaseScope) + } } package func recordZoneChangeBatch( @@ -270,6 +277,105 @@ ) } + func processPendingDatabaseChanges( + scope: CKDatabase.Scope, + fileID: StaticString = #fileID, + filePath: StaticString = #filePath, + line: UInt = #line, + column: UInt = #column + ) async throws { + let syncEngine = syncEngine(for: scope) + guard !syncEngine.state.pendingDatabaseChanges.isEmpty + else { + reportIssue("TODO") +// Issue.record( +// "Processing empty set of database changes.", +// sourceLocation: SourceLocation( +// fileID: String(describing: fileID), +// filePath: String(describing: filePath), +// line: Int(line), +// column: Int(column) +// ) +// ) + return + } + guard try await container.accountStatus() == .available + else { + reportIssue("TODO") +// Issue.record( +// """ +// User must be logged in to process pending changes. +// """, +// sourceLocation: SourceLocation( +// fileID: String(describing: fileID), +// filePath: String(describing: filePath), +// line: Int(line), +// column: Int(column) +// ) +// ) + return + } + + var zonesToSave: [CKRecordZone] = [] + var zoneIDsToDelete: [CKRecordZone.ID] = [] + for pendingDatabaseChange in syncEngine.state.pendingDatabaseChanges { + switch pendingDatabaseChange { + case .saveZone(let zone): + zonesToSave.append(zone) + case .deleteZone(let zoneID): + zoneIDsToDelete.append(zoneID) + @unknown default: + fatalError("Unsupported pendingDatabaseChange: \(pendingDatabaseChange)") + } + } + let results: + ( + saveResults: [CKRecordZone.ID: Result], + deleteResults: [CKRecordZone.ID: Result] + ) = try syncEngine.database.modifyRecordZones( + saving: zonesToSave, + deleting: zoneIDsToDelete + ) + var savedZones: [CKRecordZone] = [] + var failedZoneSaves: [(zone: CKRecordZone, error: CKError)] = [] + var deletedZoneIDs: [CKRecordZone.ID] = [] + var failedZoneDeletes: [CKRecordZone.ID: CKError] = [:] + for (zoneID, saveResult) in results.saveResults { + switch saveResult { + case .success(let zone): + savedZones.append(zone) + case .failure(let error as CKError): + failedZoneSaves.append((zonesToSave.first(where: { $0.zoneID == zoneID })!, error)) + case .failure(let error): + reportIssue("Error thrown not CKError: \(error)") + } + } + for (zoneID, deleteResult) in results.deleteResults { + switch deleteResult { + case .success: + deletedZoneIDs.append(zoneID) + case .failure(let error as CKError): + failedZoneDeletes[zoneID] = error + case .failure(let error): + reportIssue("Error thrown not CKError: \(error)") + } + } + + syncEngine.state.remove(pendingDatabaseChanges: savedZones.map { .saveZone($0) }) + syncEngine.state.remove(pendingDatabaseChanges: deletedZoneIDs.map { .deleteZone($0) }) + + await syncEngine.parentSyncEngine + .handleEvent( + .sentDatabaseChanges( + savedZones: savedZones, + failedZoneSaves: failedZoneSaves, + deletedZoneIDs: deletedZoneIDs, + failedZoneDeletes: failedZoneDeletes + ), + syncEngine: syncEngine + ) + } + package var `private`: MockSyncEngine { syncEngines.private as! MockSyncEngine } diff --git a/Sources/SQLiteData/CloudKit/SyncEngine.swift b/Sources/SQLiteData/CloudKit/SyncEngine.swift index 76bbbb71..d7ebc971 100644 --- a/Sources/SQLiteData/CloudKit/SyncEngine.swift +++ b/Sources/SQLiteData/CloudKit/SyncEngine.swift @@ -117,12 +117,15 @@ else { let privateDatabase = MockCloudDatabase(databaseScope: .private) let sharedDatabase = MockCloudDatabase(databaseScope: .shared) + let container = MockCloudContainer( + containerIdentifier: containerIdentifier ?? "iCloud.co.pointfree.SQLiteData.Tests", + privateCloudDatabase: privateDatabase, + sharedCloudDatabase: sharedDatabase + ) + privateDatabase.set(container: container) + sharedDatabase.set(container: container) try self.init( - container: MockCloudContainer( - containerIdentifier: containerIdentifier ?? "iCloud.co.pointfree.SQLiteData.Tests", - privateCloudDatabase: privateDatabase, - sharedCloudDatabase: sharedDatabase - ), + container: container, defaultZone: defaultZone, defaultSyncEngines: { _, syncEngine in ( @@ -439,6 +442,9 @@ ) } } + syncEngines.withValue { + $0.private?.state.add(pendingDatabaseChanges: [.saveZone(defaultZone)]) + } let previousRecordTypes = try metadatabase.read { db in try RecordType.all.fetchAll(db) diff --git a/Tests/SQLiteDataTests/Internal/CloudKitTestHelpers.swift b/Tests/SQLiteDataTests/Internal/CloudKitTestHelpers.swift index 70c082c0..4b22c99a 100644 --- a/Tests/SQLiteDataTests/Internal/CloudKitTestHelpers.swift +++ b/Tests/SQLiteDataTests/Internal/CloudKitTestHelpers.swift @@ -105,103 +105,6 @@ extension SyncEngine { return (saveResults, deleteResults) } } - - func processPendingDatabaseChanges( - scope: CKDatabase.Scope, - fileID: StaticString = #fileID, - filePath: StaticString = #filePath, - line: UInt = #line, - column: UInt = #column - ) async throws { - let syncEngine = syncEngine(for: scope) - guard !syncEngine.state.pendingDatabaseChanges.isEmpty - else { - Issue.record( - "Processing empty set of database changes.", - sourceLocation: SourceLocation( - fileID: String(describing: fileID), - filePath: String(describing: filePath), - line: Int(line), - column: Int(column) - ) - ) - return - } - guard try await container.accountStatus() == .available - else { - Issue.record( - """ - User must be logged in to process pending changes. - """, - sourceLocation: SourceLocation( - fileID: String(describing: fileID), - filePath: String(describing: filePath), - line: Int(line), - column: Int(column) - ) - ) - return - } - - var zonesToSave: [CKRecordZone] = [] - var zoneIDsToDelete: [CKRecordZone.ID] = [] - for pendingDatabaseChange in syncEngine.state.pendingDatabaseChanges { - switch pendingDatabaseChange { - case .saveZone(let zone): - zonesToSave.append(zone) - case .deleteZone(let zoneID): - zoneIDsToDelete.append(zoneID) - @unknown default: - fatalError("Unsupported pendingDatabaseChange: \(pendingDatabaseChange)") - } - } - let results: - ( - saveResults: [CKRecordZone.ID: Result], - deleteResults: [CKRecordZone.ID: Result] - ) = try syncEngine.database.modifyRecordZones( - saving: zonesToSave, - deleting: zoneIDsToDelete - ) - var savedZones: [CKRecordZone] = [] - var failedZoneSaves: [(zone: CKRecordZone, error: CKError)] = [] - var deletedZoneIDs: [CKRecordZone.ID] = [] - var failedZoneDeletes: [CKRecordZone.ID: CKError] = [:] - for (zoneID, saveResult) in results.saveResults { - switch saveResult { - case .success(let zone): - savedZones.append(zone) - case .failure(let error as CKError): - failedZoneSaves.append((zonesToSave.first(where: { $0.zoneID == zoneID })!, error)) - case .failure(let error): - reportIssue("Error thrown not CKError: \(error)") - } - } - for (zoneID, deleteResult) in results.deleteResults { - switch deleteResult { - case .success: - deletedZoneIDs.append(zoneID) - case .failure(let error as CKError): - failedZoneDeletes[zoneID] = error - case .failure(let error): - reportIssue("Error thrown not CKError: \(error)") - } - } - - syncEngine.state.remove(pendingDatabaseChanges: savedZones.map { .saveZone($0) }) - syncEngine.state.remove(pendingDatabaseChanges: deletedZoneIDs.map { .deleteZone($0) }) - - await syncEngine.parentSyncEngine - .handleEvent( - .sentDatabaseChanges( - savedZones: savedZones, - failedZoneSaves: failedZoneSaves, - deletedZoneIDs: deletedZoneIDs, - failedZoneDeletes: failedZoneDeletes - ), - syncEngine: syncEngine - ) - } } @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) From 726848f28a20262de040cc9b7eca877b9bab6bdb Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 17 Nov 2025 11:51:18 -0600 Subject: [PATCH 02/14] wip --- .../xcshareddata/swiftpm/Package.resolved | 20 ++++++- Examples/Reminders/Schema.swift | 6 +- .../RemindersTests/RemindersListsTests.swift | 55 +++++++++++++++++ .../CloudKit/Internal/MockSyncEngine.swift | 60 +++++++++---------- Sources/SQLiteData/CloudKit/SyncEngine.swift | 2 +- 5 files changed, 106 insertions(+), 37 deletions(-) diff --git a/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index a0d54872..587d0c8d 100644 --- a/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "c133bf7d10c8ce1e5d6506c3d2f080eac8b4c8c2827044d53a9b925e903564fd", + "originHash" : "41e7781e6c506773b6af84af513bcd6d3b1be59d635e6c4c4bd89638368e4629", "pins" : [ { "identity" : "combine-schedulers", @@ -73,6 +73,24 @@ "version" : "1.9.4" } }, + { + "identity" : "swift-docc-plugin", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-docc-plugin", + "state" : { + "revision" : "3e4f133a77e644a5812911a0513aeb7288b07d06", + "version" : "1.4.5" + } + }, + { + "identity" : "swift-docc-symbolkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-docc-symbolkit", + "state" : { + "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", + "version" : "1.0.0" + } + }, { "identity" : "swift-identified-collections", "kind" : "remoteSourceControl", diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index e9acd229..15cdcb87 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -387,8 +387,10 @@ nonisolated private let logger = Logger(subsystem: "Reminders", category: "Datab func seedSampleData() throws { @Dependency(\.date.now) var now @Dependency(\.uuid) var uuid - let remindersListIDs = (0...2).map { _ in uuid() } - let reminderIDs = (0...10).map { _ in uuid() } + let remindersListIDs = [uuid(), uuid(), uuid()] + let reminderIDs = [ + uuid(), uuid(), uuid(), uuid(), uuid(), uuid(), uuid(), uuid(), uuid(), uuid(), uuid() + ] try seed { RemindersList( id: remindersListIDs[0], diff --git a/Examples/RemindersTests/RemindersListsTests.swift b/Examples/RemindersTests/RemindersListsTests.swift index d0fbb73b..ac385a71 100644 --- a/Examples/RemindersTests/RemindersListsTests.swift +++ b/Examples/RemindersTests/RemindersListsTests.swift @@ -1,4 +1,6 @@ +import Dependencies import DependenciesTestSupport +import Foundation import InlineSnapshotTesting import SnapshotTestingCustomDump import Testing @@ -8,6 +10,9 @@ import Testing extension BaseTestSuite { @MainActor struct RemindersListsTests { + @Dependency(\.defaultDatabase) var database + @Dependency(\.defaultSyncEngine) var syncEngine + @Test func basics() async throws { let model = RemindersListsModel() try await model.$remindersLists.load() @@ -100,5 +105,55 @@ extension BaseTestSuite { """ } } + + @Test func share() async throws { + let model = RemindersListsModel() + + let personalRemindersList = try #require( + try await database.read { db in + try RemindersList.find(UUID(0)).fetchOne(db) + } + ) + try await syncEngine.sendChanges() + let _ = try await syncEngine.share(record: personalRemindersList, configure: { _ in }) + + try await model.$remindersLists.load() + assertInlineSnapshot(of: model.remindersLists, as: .customDump) { + """ + [ + [0]: RemindersListsModel.ReminderListState( + remindersCount: 4, + remindersList: RemindersList( + id: UUID(00000000-0000-0000-0000-000000000000), + color: 1218047999, + position: 1, + title: "Personal" + ), + share: CKShare() + ), + [1]: RemindersListsModel.ReminderListState( + remindersCount: 2, + remindersList: RemindersList( + id: UUID(00000000-0000-0000-0000-000000000001), + color: 3985191935, + position: 2, + title: "Family" + ), + share: nil + ), + [2]: RemindersListsModel.ReminderListState( + remindersCount: 2, + remindersList: RemindersList( + id: UUID(00000000-0000-0000-0000-000000000002), + color: 2992493567, + position: 3, + title: "Business" + ), + share: nil + ) + ] + """ + } + } } } diff --git a/Sources/SQLiteData/CloudKit/Internal/MockSyncEngine.swift b/Sources/SQLiteData/CloudKit/Internal/MockSyncEngine.swift index b400c7ac..31686fcd 100644 --- a/Sources/SQLiteData/CloudKit/Internal/MockSyncEngine.swift +++ b/Sources/SQLiteData/CloudKit/Internal/MockSyncEngine.swift @@ -1,6 +1,6 @@ #if canImport(CloudKit) import CloudKit -import IssueReporting + import IssueReporting import OrderedCollections @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) @@ -59,13 +59,15 @@ import IssueReporting package func sendChanges(_ options: CKSyncEngine.SendChangesOptions) async throws { - if - !parentSyncEngine.syncEngine(for: database.databaseScope).state.pendingDatabaseChanges.isEmpty { + if !parentSyncEngine.syncEngine(for: database.databaseScope).state.pendingDatabaseChanges + .isEmpty + { try await parentSyncEngine.processPendingDatabaseChanges(scope: database.databaseScope) } if !parentSyncEngine.syncEngine(for: database.databaseScope).state.pendingRecordZoneChanges - .isEmpty { + .isEmpty + { try await parentSyncEngine.processPendingRecordZoneChanges(scope: database.databaseScope) } @@ -287,32 +289,24 @@ import IssueReporting let syncEngine = syncEngine(for: scope) guard !syncEngine.state.pendingDatabaseChanges.isEmpty else { - reportIssue("TODO") -// Issue.record( -// "Processing empty set of database changes.", -// sourceLocation: SourceLocation( -// fileID: String(describing: fileID), -// filePath: String(describing: filePath), -// line: Int(line), -// column: Int(column) -// ) -// ) + reportIssue( + "Processing empty set of database changes.", + fileID: fileID, + filePath: filePath, + line: line, + column: column + ) return } guard try await container.accountStatus() == .available else { - reportIssue("TODO") -// Issue.record( -// """ -// User must be logged in to process pending changes. -// """, -// sourceLocation: SourceLocation( -// fileID: String(describing: fileID), -// filePath: String(describing: filePath), -// line: Int(line), -// column: Int(column) -// ) -// ) + reportIssue( + "User must be logged in to process pending changes.", + fileID: fileID, + filePath: filePath, + line: line, + column: column + ) return } @@ -329,13 +323,13 @@ import IssueReporting } } let results: - ( - saveResults: [CKRecordZone.ID: Result], - deleteResults: [CKRecordZone.ID: Result] - ) = try syncEngine.database.modifyRecordZones( - saving: zonesToSave, - deleting: zoneIDsToDelete - ) + ( + saveResults: [CKRecordZone.ID: Result], + deleteResults: [CKRecordZone.ID: Result] + ) = try syncEngine.database.modifyRecordZones( + saving: zonesToSave, + deleting: zoneIDsToDelete + ) var savedZones: [CKRecordZone] = [] var failedZoneSaves: [(zone: CKRecordZone, error: CKError)] = [] var deletedZoneIDs: [CKRecordZone.ID] = [] diff --git a/Sources/SQLiteData/CloudKit/SyncEngine.swift b/Sources/SQLiteData/CloudKit/SyncEngine.swift index d7ebc971..be9190b8 100644 --- a/Sources/SQLiteData/CloudKit/SyncEngine.swift +++ b/Sources/SQLiteData/CloudKit/SyncEngine.swift @@ -87,7 +87,7 @@ privateTables: repeat (each T2).Type, containerIdentifier: String? = nil, defaultZone: CKRecordZone = CKRecordZone(zoneName: "co.pointfree.SQLiteData.defaultZone"), - startImmediately: Bool = DependencyValues._current.context == .live, + startImmediately: Bool = true, delegate: (any SyncEngineDelegate)? = nil, logger: Logger = isTesting ? Logger(.disabled) : Logger(subsystem: "SQLiteData", category: "CloudKit") From ecc9dc0e209d46851462a272706c58408af7c41e Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 17 Nov 2025 12:28:11 -0600 Subject: [PATCH 03/14] wip --- .../xcshareddata/swiftpm/Package.resolved | 4 ++-- Examples/Reminders/Schema.swift | 6 ++---- Examples/RemindersTests/Internal.swift | 5 ++++- Examples/RemindersTests/RemindersListsTests.swift | 2 -- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 587d0c8d..38ef6022 100644 --- a/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -69,8 +69,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-dependencies", "state" : { - "revision" : "a501eebe552fd23691c560adf474fca2169ad8aa", - "version" : "1.9.4" + "revision" : "a10f9feeb214bc72b5337b6ef6d5a029360db4cc", + "version" : "1.10.0" } }, { diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index 15cdcb87..e9acd229 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -387,10 +387,8 @@ nonisolated private let logger = Logger(subsystem: "Reminders", category: "Datab func seedSampleData() throws { @Dependency(\.date.now) var now @Dependency(\.uuid) var uuid - let remindersListIDs = [uuid(), uuid(), uuid()] - let reminderIDs = [ - uuid(), uuid(), uuid(), uuid(), uuid(), uuid(), uuid(), uuid(), uuid(), uuid(), uuid() - ] + let remindersListIDs = (0...2).map { _ in uuid() } + let reminderIDs = (0...10).map { _ in uuid() } try seed { RemindersList( id: remindersListIDs[0], diff --git a/Examples/RemindersTests/Internal.swift b/Examples/RemindersTests/Internal.swift index b21916c3..61c1dd30 100644 --- a/Examples/RemindersTests/Internal.swift +++ b/Examples/RemindersTests/Internal.swift @@ -10,7 +10,10 @@ import Testing .dependency(\.continuousClock, ImmediateClock()), .dependency(\.date.now, Date(timeIntervalSince1970: 1_234_567_890)), .dependency(\.uuid, .incrementing), - .dependencies { try $0.bootstrapDatabase() }, + .dependencies { + try $0.bootstrapDatabase() + try await $0.defaultSyncEngine.sendChanges() + }, .snapshots(record: .failed) ) struct BaseTestSuite {} diff --git a/Examples/RemindersTests/RemindersListsTests.swift b/Examples/RemindersTests/RemindersListsTests.swift index ac385a71..6acdc097 100644 --- a/Examples/RemindersTests/RemindersListsTests.swift +++ b/Examples/RemindersTests/RemindersListsTests.swift @@ -108,13 +108,11 @@ extension BaseTestSuite { @Test func share() async throws { let model = RemindersListsModel() - let personalRemindersList = try #require( try await database.read { db in try RemindersList.find(UUID(0)).fetchOne(db) } ) - try await syncEngine.sendChanges() let _ = try await syncEngine.share(record: personalRemindersList, configure: { _ in }) try await model.$remindersLists.load() From e549af849587f290eb1c8791eae0ceb5bf3434a6 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 17 Nov 2025 13:39:43 -0600 Subject: [PATCH 04/14] wip --- Examples/Reminders/Schema.swift | 8 ++- .../RemindersTests/RemindersListsTests.swift | 1 + Sources/SQLiteData/CloudKit/SyncEngine.swift | 61 ++++++++++++++----- 3 files changed, 51 insertions(+), 19 deletions(-) diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index e9acd229..cd111114 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -142,7 +142,7 @@ func appDatabase() throws -> any DatabaseWriter { db.trace(options: .profile) { if context == .live { logger.debug("\($0.expandedDescription)") - } else { + } else if context == .preview { print("\($0.expandedDescription)") } } @@ -387,8 +387,10 @@ nonisolated private let logger = Logger(subsystem: "Reminders", category: "Datab func seedSampleData() throws { @Dependency(\.date.now) var now @Dependency(\.uuid) var uuid - let remindersListIDs = (0...2).map { _ in uuid() } - let reminderIDs = (0...10).map { _ in uuid() } + let remindersListIDs = [uuid(), uuid(), uuid()] + let reminderIDs = [ + uuid(), uuid(), uuid(), uuid(), uuid(), uuid(), uuid(), uuid(), uuid(), uuid(), uuid() + ] try seed { RemindersList( id: remindersListIDs[0], diff --git a/Examples/RemindersTests/RemindersListsTests.swift b/Examples/RemindersTests/RemindersListsTests.swift index 6acdc097..97d01089 100644 --- a/Examples/RemindersTests/RemindersListsTests.swift +++ b/Examples/RemindersTests/RemindersListsTests.swift @@ -108,6 +108,7 @@ extension BaseTestSuite { @Test func share() async throws { let model = RemindersListsModel() + let personalRemindersList = try #require( try await database.read { db in try RemindersList.find(UUID(0)).fetchOne(db) diff --git a/Sources/SQLiteData/CloudKit/SyncEngine.swift b/Sources/SQLiteData/CloudKit/SyncEngine.swift index be9190b8..f81aadc7 100644 --- a/Sources/SQLiteData/CloudKit/SyncEngine.swift +++ b/Sources/SQLiteData/CloudKit/SyncEngine.swift @@ -5,6 +5,7 @@ import OrderedCollections import OSLog import Observation + import Perception import StructuredQueriesCore import SwiftData import TabularData @@ -423,6 +424,19 @@ } } + private let _isReady = LockIsolated(false) + private var isReady: Bool { + get { + observationRegistrar.access(self, keyPath: \.isReady) + return _isReady.withValue(\.self) + } + set { + observationRegistrar.withMutation(of: self, keyPath: \.isReady) { + _isReady.withValue { $0 = newValue } + } + } + } + /// Determines if the sync engine is currently running or not. public var isRunning: Bool { observationRegistrar.access(self, keyPath: \.isRunning) @@ -440,11 +454,9 @@ private: privateSyncEngine, shared: sharedSyncEngine ) + privateSyncEngine.state.add(pendingDatabaseChanges: [.saveZone(defaultZone)]) } } - syncEngines.withValue { - $0.private?.state.add(pendingDatabaseChanges: [.saveZone(defaultZone)]) - } let previousRecordTypes = try metadatabase.read { db in try RecordType.all.fetchAll(db) @@ -492,23 +504,29 @@ ($0.tableName, $0) } ) + + withErrorReporting(.sqliteDataCloudKitFailure) { + try uploadRecordsToCloudKit( + previousRecordTypeByTableName: previousRecordTypeByTableName, + currentRecordTypeByTableName: currentRecordTypeByTableName + ) + } + return Task { await withErrorReporting(.sqliteDataCloudKitFailure) { guard try await container.accountStatus() == .available else { return } - try await uploadRecordsToCloudKit( - previousRecordTypeByTableName: previousRecordTypeByTableName, - currentRecordTypeByTableName: currentRecordTypeByTableName - ) + try await updateLocalFromSchemaChange( previousRecordTypeByTableName: previousRecordTypeByTableName, currentRecordTypeByTableName: currentRecordTypeByTableName ) try await cacheUserTables(recordTypes: currentRecordTypes) } + isReady = true } } - + /// Fetches pending remote changes from the server. /// /// Use this method to ensure the sync engine immediately fetches all pending remote changes @@ -520,6 +538,7 @@ public func fetchChanges( _ options: CKSyncEngine.FetchChangesOptions = CKSyncEngine.FetchChangesOptions() ) async throws { + await isReady() let (privateSyncEngine, sharedSyncEngine) = syncEngines.withValue { ($0.private, $0.shared) } @@ -529,7 +548,7 @@ async let shared: Void = sharedSyncEngine.fetchChanges(options) _ = try await (`private`, shared) } - + /// Sends pending local changes to the server. /// /// Use this method to ensure the sync engine sends all pending local changes to the server @@ -541,6 +560,7 @@ public func sendChanges( _ options: CKSyncEngine.SendChangesOptions = CKSyncEngine.SendChangesOptions() ) async throws { + await isReady() let (privateSyncEngine, sharedSyncEngine) = syncEngines.withValue { ($0.private, $0.shared) } @@ -550,7 +570,12 @@ async let shared: Void = sharedSyncEngine.sendChanges(options) _ = try await (`private`, shared) } - + + private func isReady() async { + guard !isRunning else { return } + _ = await Perceptions { self.isReady }.first(where: { $0 }) + } + /// Synchronizes local and remote pending changes. /// /// Use this method to ensure the sync engine immediately fetches all pending remote changes @@ -580,9 +605,9 @@ private func uploadRecordsToCloudKit( previousRecordTypeByTableName: [String: RecordType], currentRecordTypeByTableName: [String: RecordType] - ) async throws { - try await enqueueLocallyPendingChanges() - try await userDatabase.write { db in + ) throws { + try enqueueLocallyPendingChanges() + try userDatabase.write { db in try PendingRecordZoneChange.delete().execute(db) let newTableNames = currentRecordTypeByTableName.keys.filter { tableName in @@ -597,8 +622,8 @@ } } - private func enqueueLocallyPendingChanges() async throws { - let pendingRecordZoneChanges = try await metadatabase.read { db in + private func enqueueLocallyPendingChanges() throws { + let pendingRecordZoneChanges = try metadatabase.read { db in try PendingRecordZoneChange .select(\.pendingRecordZoneChange) .fetchAll(db) @@ -1858,7 +1883,8 @@ } private func refreshLastKnownServerRecord(_ record: CKRecord) async { - await withErrorReporting(.sqliteDataCloudKitFailure) { + //await withErrorReporting(.sqliteDataCloudKitFailure) { + do { try await metadatabase.write { db in let metadata = try SyncMetadata.find(record.recordID).fetchOne(db) func updateLastKnownServerRecord() throws { @@ -1876,6 +1902,9 @@ try updateLastKnownServerRecord() } } + } catch { + print(error) + print("!!!") } } From 88750873883759e2cd1e21a48776028378d219bd Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 17 Nov 2025 13:56:21 -0600 Subject: [PATCH 05/14] simplify --- Sources/SQLiteData/CloudKit/SyncEngine.swift | 29 ++++---------------- 1 file changed, 6 insertions(+), 23 deletions(-) diff --git a/Sources/SQLiteData/CloudKit/SyncEngine.swift b/Sources/SQLiteData/CloudKit/SyncEngine.swift index f81aadc7..72eb0446 100644 --- a/Sources/SQLiteData/CloudKit/SyncEngine.swift +++ b/Sources/SQLiteData/CloudKit/SyncEngine.swift @@ -5,7 +5,6 @@ import OrderedCollections import OSLog import Observation - import Perception import StructuredQueriesCore import SwiftData import TabularData @@ -38,6 +37,7 @@ private let observationRegistrar = ObservationRegistrar() private let notificationsObserver = LockIsolated<(any NSObjectProtocol)?>(nil) private let activityCounts = LockIsolated(ActivityCounts()) + private let startTask = LockIsolated?>(nil) /// The error message used when a write occurs to a record for which the current user does not /// have permission. @@ -424,19 +424,6 @@ } } - private let _isReady = LockIsolated(false) - private var isReady: Bool { - get { - observationRegistrar.access(self, keyPath: \.isReady) - return _isReady.withValue(\.self) - } - set { - observationRegistrar.withMutation(of: self, keyPath: \.isReady) { - _isReady.withValue { $0 = newValue } - } - } - } - /// Determines if the sync engine is currently running or not. public var isRunning: Bool { observationRegistrar.access(self, keyPath: \.isRunning) @@ -512,7 +499,7 @@ ) } - return Task { + let startTask = Task { await withErrorReporting(.sqliteDataCloudKitFailure) { guard try await container.accountStatus() == .available else { return } @@ -523,8 +510,9 @@ ) try await cacheUserTables(recordTypes: currentRecordTypes) } - isReady = true } + self.startTask.withValue { $0 = startTask } + return startTask } /// Fetches pending remote changes from the server. @@ -538,7 +526,7 @@ public func fetchChanges( _ options: CKSyncEngine.FetchChangesOptions = CKSyncEngine.FetchChangesOptions() ) async throws { - await isReady() + await startTask.withValue(\.self)?.value let (privateSyncEngine, sharedSyncEngine) = syncEngines.withValue { ($0.private, $0.shared) } @@ -560,7 +548,7 @@ public func sendChanges( _ options: CKSyncEngine.SendChangesOptions = CKSyncEngine.SendChangesOptions() ) async throws { - await isReady() + await startTask.withValue(\.self)?.value let (privateSyncEngine, sharedSyncEngine) = syncEngines.withValue { ($0.private, $0.shared) } @@ -571,11 +559,6 @@ _ = try await (`private`, shared) } - private func isReady() async { - guard !isRunning else { return } - _ = await Perceptions { self.isReady }.first(where: { $0 }) - } - /// Synchronizes local and remote pending changes. /// /// Use this method to ensure the sync engine immediately fetches all pending remote changes From 032988aa7bb0b42eb86670b6335f89c7ad785ab3 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 17 Nov 2025 14:00:27 -0600 Subject: [PATCH 06/14] more simplify --- Sources/SQLiteData/CloudKit/SyncEngine.swift | 24 +++++++++----------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/Sources/SQLiteData/CloudKit/SyncEngine.swift b/Sources/SQLiteData/CloudKit/SyncEngine.swift index 72eb0446..85b15e6b 100644 --- a/Sources/SQLiteData/CloudKit/SyncEngine.swift +++ b/Sources/SQLiteData/CloudKit/SyncEngine.swift @@ -492,18 +492,16 @@ } ) - withErrorReporting(.sqliteDataCloudKitFailure) { - try uploadRecordsToCloudKit( - previousRecordTypeByTableName: previousRecordTypeByTableName, - currentRecordTypeByTableName: currentRecordTypeByTableName - ) - } - let startTask = Task { await withErrorReporting(.sqliteDataCloudKitFailure) { guard try await container.accountStatus() == .available else { return } - + await withErrorReporting(.sqliteDataCloudKitFailure) { + try await uploadRecordsToCloudKit( + previousRecordTypeByTableName: previousRecordTypeByTableName, + currentRecordTypeByTableName: currentRecordTypeByTableName + ) + } try await updateLocalFromSchemaChange( previousRecordTypeByTableName: previousRecordTypeByTableName, currentRecordTypeByTableName: currentRecordTypeByTableName @@ -588,9 +586,9 @@ private func uploadRecordsToCloudKit( previousRecordTypeByTableName: [String: RecordType], currentRecordTypeByTableName: [String: RecordType] - ) throws { - try enqueueLocallyPendingChanges() - try userDatabase.write { db in + ) async throws { + try await enqueueLocallyPendingChanges() + try await userDatabase.write { db in try PendingRecordZoneChange.delete().execute(db) let newTableNames = currentRecordTypeByTableName.keys.filter { tableName in @@ -605,8 +603,8 @@ } } - private func enqueueLocallyPendingChanges() throws { - let pendingRecordZoneChanges = try metadatabase.read { db in + private func enqueueLocallyPendingChanges() async throws { + let pendingRecordZoneChanges = try await metadatabase.read { db in try PendingRecordZoneChange .select(\.pendingRecordZoneChange) .fetchAll(db) From 42944ee3939fdd103bce2cbaa8a396badb074d61 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 17 Nov 2025 14:01:08 -0600 Subject: [PATCH 07/14] wip --- Sources/SQLiteData/CloudKit/SyncEngine.swift | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Sources/SQLiteData/CloudKit/SyncEngine.swift b/Sources/SQLiteData/CloudKit/SyncEngine.swift index 85b15e6b..43c5be83 100644 --- a/Sources/SQLiteData/CloudKit/SyncEngine.swift +++ b/Sources/SQLiteData/CloudKit/SyncEngine.swift @@ -496,12 +496,10 @@ await withErrorReporting(.sqliteDataCloudKitFailure) { guard try await container.accountStatus() == .available else { return } - await withErrorReporting(.sqliteDataCloudKitFailure) { - try await uploadRecordsToCloudKit( - previousRecordTypeByTableName: previousRecordTypeByTableName, - currentRecordTypeByTableName: currentRecordTypeByTableName - ) - } + try await uploadRecordsToCloudKit( + previousRecordTypeByTableName: previousRecordTypeByTableName, + currentRecordTypeByTableName: currentRecordTypeByTableName + ) try await updateLocalFromSchemaChange( previousRecordTypeByTableName: previousRecordTypeByTableName, currentRecordTypeByTableName: currentRecordTypeByTableName From 2ab4df1c8420cfe015405dadab23d0d274243c6b Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 17 Nov 2025 14:01:43 -0600 Subject: [PATCH 08/14] clean up --- Sources/SQLiteData/CloudKit/SyncEngine.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Sources/SQLiteData/CloudKit/SyncEngine.swift b/Sources/SQLiteData/CloudKit/SyncEngine.swift index 43c5be83..84274245 100644 --- a/Sources/SQLiteData/CloudKit/SyncEngine.swift +++ b/Sources/SQLiteData/CloudKit/SyncEngine.swift @@ -1862,7 +1862,7 @@ } private func refreshLastKnownServerRecord(_ record: CKRecord) async { - //await withErrorReporting(.sqliteDataCloudKitFailure) { + await withErrorReporting(.sqliteDataCloudKitFailure) { do { try await metadatabase.write { db in let metadata = try SyncMetadata.find(record.recordID).fetchOne(db) @@ -1881,9 +1881,6 @@ try updateLastKnownServerRecord() } } - } catch { - print(error) - print("!!!") } } From c89ffe8f0403acf86b8e254e773967d37f8b73a7 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 17 Nov 2025 14:03:38 -0600 Subject: [PATCH 09/14] wip --- Sources/SQLiteData/CloudKit/SyncEngine.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/SQLiteData/CloudKit/SyncEngine.swift b/Sources/SQLiteData/CloudKit/SyncEngine.swift index 84274245..38bde1ce 100644 --- a/Sources/SQLiteData/CloudKit/SyncEngine.swift +++ b/Sources/SQLiteData/CloudKit/SyncEngine.swift @@ -1863,7 +1863,6 @@ private func refreshLastKnownServerRecord(_ record: CKRecord) async { await withErrorReporting(.sqliteDataCloudKitFailure) { - do { try await metadatabase.write { db in let metadata = try SyncMetadata.find(record.recordID).fetchOne(db) func updateLastKnownServerRecord() throws { From 76b33eaeaeb8a3e2ef4a51e3e9ee63e6c841f991 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 17 Nov 2025 14:39:40 -0600 Subject: [PATCH 10/14] wip --- .../RemindersTests/SearchRemindersTests.swift | 159 +++++------------- 1 file changed, 38 insertions(+), 121 deletions(-) diff --git a/Examples/RemindersTests/SearchRemindersTests.swift b/Examples/RemindersTests/SearchRemindersTests.swift index 8faeceb1..e1ab4589 100644 --- a/Examples/RemindersTests/SearchRemindersTests.swift +++ b/Examples/RemindersTests/SearchRemindersTests.swift @@ -8,9 +8,9 @@ import Testing extension BaseTestSuite { @MainActor - @Suite( - .snapshots(record: .missing) - ) + // @Suite( + // .snapshots(record: .missing) + // ) struct SearchRemindersTests { @Dependency(\.defaultDatabase) var database @@ -28,38 +28,34 @@ extension BaseTestSuite { model.searchText = "Take" try await model.searchTask?.value #expect(model.searchResults.completedCount == 1) - withKnownIssue( - "'@Fetch' introduces an escaping closure and loses the task-local dependency" - ) { - assertInlineSnapshot(of: model.searchResults.rows, as: .customDump) { - """ - [ - [0]: SearchRemindersModel.Row( - isPastDue: false, + assertInlineSnapshot(of: model.searchResults.rows, as: .customDump) { + """ + [ + [0]: SearchRemindersModel.Row( + isPastDue: true, + notes: "", + reminder: Reminder( + id: UUID(00000000-0000-0000-0000-00000000000A), + dueDate: Date(2009-02-17T23:31:30.000Z), + isFlagged: false, notes: "", - reminder: Reminder( - id: UUID(00000000-0000-0000-0000-00000000000A), - dueDate: Date(2009-02-17T23:31:30.000Z), - isCompleted: false, - isFlagged: false, - notes: "", - position: 8, - priority: .high, - remindersListID: UUID(00000000-0000-0000-0000-000000000001), - title: "Take out trash" - ), - remindersList: RemindersList( - id: UUID(00000000-0000-0000-0000-000000000001), - color: 3985191935, - position: 2, - title: "Family" - ), - tags: "", - title: "**Take** out trash" - ) - ] - """ - } + position: 8, + priority: .high, + remindersListID: UUID(00000000-0000-0000-0000-000000000001), + status: .incomplete, + title: "Take out trash" + ), + remindersList: RemindersList( + id: UUID(00000000-0000-0000-0000-000000000001), + color: 3985191935, + position: 2, + title: "Family" + ), + tags: "", + title: "**Take** out trash" + ) + ] + """ } } @@ -68,61 +64,10 @@ extension BaseTestSuite { model.searchText = "Take" try await model.showCompletedButtonTapped() - withKnownIssue( - "'@Fetch' introduces an escaping closure and loses the task-local dependency" - ) { - assertInlineSnapshot(of: model.searchResults.rows, as: .customDump) { - """ - [ - [0]: SearchRemindersModel.Row( - isPastDue: false, - notes: "", - reminder: Reminder( - id: UUID(00000000-0000-0000-0000-00000000000A), - dueDate: Date(2009-02-17T23:31:30.000Z), - isCompleted: false, - isFlagged: false, - notes: "", - position: 8, - priority: .high, - remindersListID: UUID(00000000-0000-0000-0000-000000000001), - title: "Take out trash" - ), - remindersList: RemindersList( - id: UUID(00000000-0000-0000-0000-000000000001), - color: 3985191935, - position: 2, - title: "Family" - ), - tags: "", - title: "**Take** out trash" - ), - [1]: SearchRemindersModel.Row( - isPastDue: false, - notes: "", - reminder: Reminder( - id: UUID(00000000-0000-0000-0000-000000000006), - dueDate: Date(2008-08-07T23:31:30.000Z), - isCompleted: true, - isFlagged: false, - notes: "", - position: 4, - priority: nil, - remindersListID: UUID(00000000-0000-0000-0000-000000000000), - title: "Take a walk" - ), - remindersList: RemindersList( - id: UUID(00000000-0000-0000-0000-000000000000), - color: 1218047999, - position: 1, - title: "Personal" - ), - tags: "#car #kids #social", - title: "**Take** a walk" - ) - ] - """ - } + assertInlineSnapshot(of: model.searchResults.rows, as: .customDump) { + """ + [] + """ } } @@ -133,38 +78,10 @@ extension BaseTestSuite { model.deleteCompletedReminders() try await model.$searchResults.load() #expect(model.searchResults.completedCount == 0) - withKnownIssue( - "'@Fetch' introduces an escaping closure and loses the task-local dependency" - ) { - assertInlineSnapshot(of: model.searchResults.rows, as: .customDump) { - """ - [ - [0]: SearchRemindersModel.Row( - isPastDue: false, - notes: "", - reminder: Reminder( - id: UUID(00000000-0000-0000-0000-00000000000A), - dueDate: Date(2009-02-17T23:31:30.000Z), - isCompleted: false, - isFlagged: false, - notes: "", - position: 8, - priority: .high, - remindersListID: UUID(00000000-0000-0000-0000-000000000001), - title: "Take out trash" - ), - remindersList: RemindersList( - id: UUID(00000000-0000-0000-0000-000000000001), - color: 3985191935, - position: 2, - title: "Family" - ), - tags: "", - title: "**Take** out trash" - ) - ] - """ - } + assertInlineSnapshot(of: model.searchResults.rows, as: .customDump) { + """ + [] + """ } } } From 6b8a04119dc3cf85033513745a16c2618ea88598 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 17 Nov 2025 20:12:58 -0600 Subject: [PATCH 11/14] wip --- Examples/RemindersTests/SearchRemindersTests.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Examples/RemindersTests/SearchRemindersTests.swift b/Examples/RemindersTests/SearchRemindersTests.swift index e1ab4589..c6a62e89 100644 --- a/Examples/RemindersTests/SearchRemindersTests.swift +++ b/Examples/RemindersTests/SearchRemindersTests.swift @@ -8,9 +8,9 @@ import Testing extension BaseTestSuite { @MainActor - // @Suite( - // .snapshots(record: .missing) - // ) + @Suite( + .snapshots(record: .missing) + ) struct SearchRemindersTests { @Dependency(\.defaultDatabase) var database From 8ac8f105a1b4f79f46d6cfa63cf6eee976a2f5f3 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 17 Nov 2025 20:13:17 -0600 Subject: [PATCH 12/14] wip --- Sources/SQLiteData/CloudKit/Internal/MockSyncEngine.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SQLiteData/CloudKit/Internal/MockSyncEngine.swift b/Sources/SQLiteData/CloudKit/Internal/MockSyncEngine.swift index 31686fcd..4f65532f 100644 --- a/Sources/SQLiteData/CloudKit/Internal/MockSyncEngine.swift +++ b/Sources/SQLiteData/CloudKit/Internal/MockSyncEngine.swift @@ -279,7 +279,7 @@ ) } - func processPendingDatabaseChanges( + package func processPendingDatabaseChanges( scope: CKDatabase.Scope, fileID: StaticString = #fileID, filePath: StaticString = #filePath, From 213a68efccad925b75c018e05110c34d83dbfcb4 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Thu, 27 Nov 2025 18:49:13 -0600 Subject: [PATCH 13/14] wip --- Examples/Reminders/Schema.swift | 7 +++++-- Sources/SQLiteData/CloudKit/SyncEngine.swift | 4 +++- Tests/SQLiteDataTests/CloudKitTests/RecordTypeTests.swift | 2 ++ Tests/SQLiteDataTests/CloudKitTests/SharingTests.swift | 1 + .../CloudKitTests/SyncEngineLifecycleTests.swift | 6 ++++++ 5 files changed, 17 insertions(+), 3 deletions(-) diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index 1c754ed0..629c0a67 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -140,10 +140,13 @@ func appDatabase() throws -> any DatabaseWriter { db.add(function: $handleReminderStatusUpdate) #if DEBUG db.trace(options: .profile) { - if context == .live { + switch context { + case .live: logger.debug("\($0.expandedDescription)") - } else if context == .preview { + case .preview: print("\($0.expandedDescription)") + case .test: + break } } #endif diff --git a/Sources/SQLiteData/CloudKit/SyncEngine.swift b/Sources/SQLiteData/CloudKit/SyncEngine.swift index ac9c1dd4..f6756fe5 100644 --- a/Sources/SQLiteData/CloudKit/SyncEngine.swift +++ b/Sources/SQLiteData/CloudKit/SyncEngine.swift @@ -442,7 +442,6 @@ private: privateSyncEngine, shared: sharedSyncEngine ) - privateSyncEngine.state.add(pendingDatabaseChanges: [.saveZone(defaultZone)]) } } @@ -497,6 +496,9 @@ await withErrorReporting(.sqliteDataCloudKitFailure) { guard try await container.accountStatus() == .available else { return } + syncEngines.withValue { + $0.private?.state.add(pendingDatabaseChanges: [.saveZone(defaultZone)]) + } try await uploadRecordsToCloudKit( previousRecordTypeByTableName: previousRecordTypeByTableName, currentRecordTypeByTableName: currentRecordTypeByTableName diff --git a/Tests/SQLiteDataTests/CloudKitTests/RecordTypeTests.swift b/Tests/SQLiteDataTests/CloudKitTests/RecordTypeTests.swift index e893bf3e..a7190930 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/RecordTypeTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/RecordTypeTests.swift @@ -399,6 +399,7 @@ try syncEngine.tearDownSyncEngine() try syncEngine.setUpSyncEngine() try await syncEngine.start() + try await syncEngine.processPendingDatabaseChanges(scope: .private) let recordTypesAfterReSetup = try await syncEngine.metadatabase.read { db in try RecordType.all.fetchAll(db) } @@ -422,6 +423,7 @@ } try syncEngine.setUpSyncEngine() try await syncEngine.start() + try await syncEngine.processPendingDatabaseChanges(scope: .private) let recordTypesAfterMigration = try await syncEngine.metadatabase.read { db in try RecordType.order(by: \.tableName).fetchAll(db) diff --git a/Tests/SQLiteDataTests/CloudKitTests/SharingTests.swift b/Tests/SQLiteDataTests/CloudKitTests/SharingTests.swift index 5b1b5ffa..a8a8f8c5 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/SharingTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/SharingTests.swift @@ -2676,6 +2676,7 @@ } try await syncEngine.start() + try await syncEngine.processPendingDatabaseChanges(scope: .private) try await syncEngine.processPendingRecordZoneChanges(scope: .private) try await syncEngine.processPendingRecordZoneChanges(scope: .shared) diff --git a/Tests/SQLiteDataTests/CloudKitTests/SyncEngineLifecycleTests.swift b/Tests/SQLiteDataTests/CloudKitTests/SyncEngineLifecycleTests.swift index b65e984c..0f06707d 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/SyncEngineLifecycleTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/SyncEngineLifecycleTests.swift @@ -93,6 +93,7 @@ } try await syncEngine.start() + try await syncEngine.processPendingDatabaseChanges(scope: .private) try await syncEngine.processPendingRecordZoneChanges(scope: .private) assertInlineSnapshot(of: container, as: .customDump) { @@ -153,6 +154,7 @@ try await Task.sleep(for: .seconds(1)) try await syncEngine.start() + try await syncEngine.processPendingDatabaseChanges(scope: .private) try await syncEngine.processPendingRecordZoneChanges(scope: .private) assertInlineSnapshot(of: container, as: .customDump) { @@ -216,6 +218,7 @@ } try await syncEngine.start() + try await syncEngine.processPendingDatabaseChanges(scope: .private) try await syncEngine.processPendingRecordZoneChanges(scope: .private) assertInlineSnapshot(of: container, as: .customDump) { @@ -342,6 +345,7 @@ try await Task.sleep(for: .seconds(0.5)) try await syncEngine.start() + try await syncEngine.processPendingDatabaseChanges(scope: .private) try await syncEngine.processPendingRecordZoneChanges(scope: .shared) assertInlineSnapshot(of: container, as: .customDump) { @@ -409,6 +413,7 @@ try await Task.sleep(for: .seconds(1)) try await syncEngine.start() + try await syncEngine.processPendingDatabaseChanges(scope: .private) try await syncEngine.processPendingRecordZoneChanges(scope: .shared) assertInlineSnapshot(of: container, as: .customDump) { @@ -474,6 +479,7 @@ try await Task.sleep(for: .seconds(0.5)) try await syncEngine.start() + try await syncEngine.processPendingDatabaseChanges(scope: .private) try await syncEngine.processPendingRecordZoneChanges(scope: .private) assertInlineSnapshot(of: container, as: .customDump) { From bda6d54c1cc66093b5c1548f1b479eb0c7fe7247 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Fri, 5 Dec 2025 10:16:09 -0600 Subject: [PATCH 14/14] wip --- .../RemindersTests/SearchRemindersTests.swift | 153 ++++++++++++++---- 1 file changed, 118 insertions(+), 35 deletions(-) diff --git a/Examples/RemindersTests/SearchRemindersTests.swift b/Examples/RemindersTests/SearchRemindersTests.swift index c6a62e89..8faeceb1 100644 --- a/Examples/RemindersTests/SearchRemindersTests.swift +++ b/Examples/RemindersTests/SearchRemindersTests.swift @@ -28,34 +28,38 @@ extension BaseTestSuite { model.searchText = "Take" try await model.searchTask?.value #expect(model.searchResults.completedCount == 1) - assertInlineSnapshot(of: model.searchResults.rows, as: .customDump) { - """ - [ - [0]: SearchRemindersModel.Row( - isPastDue: true, - notes: "", - reminder: Reminder( - id: UUID(00000000-0000-0000-0000-00000000000A), - dueDate: Date(2009-02-17T23:31:30.000Z), - isFlagged: false, + withKnownIssue( + "'@Fetch' introduces an escaping closure and loses the task-local dependency" + ) { + assertInlineSnapshot(of: model.searchResults.rows, as: .customDump) { + """ + [ + [0]: SearchRemindersModel.Row( + isPastDue: false, notes: "", - position: 8, - priority: .high, - remindersListID: UUID(00000000-0000-0000-0000-000000000001), - status: .incomplete, - title: "Take out trash" - ), - remindersList: RemindersList( - id: UUID(00000000-0000-0000-0000-000000000001), - color: 3985191935, - position: 2, - title: "Family" - ), - tags: "", - title: "**Take** out trash" - ) - ] - """ + reminder: Reminder( + id: UUID(00000000-0000-0000-0000-00000000000A), + dueDate: Date(2009-02-17T23:31:30.000Z), + isCompleted: false, + isFlagged: false, + notes: "", + position: 8, + priority: .high, + remindersListID: UUID(00000000-0000-0000-0000-000000000001), + title: "Take out trash" + ), + remindersList: RemindersList( + id: UUID(00000000-0000-0000-0000-000000000001), + color: 3985191935, + position: 2, + title: "Family" + ), + tags: "", + title: "**Take** out trash" + ) + ] + """ + } } } @@ -64,10 +68,61 @@ extension BaseTestSuite { model.searchText = "Take" try await model.showCompletedButtonTapped() - assertInlineSnapshot(of: model.searchResults.rows, as: .customDump) { - """ - [] - """ + withKnownIssue( + "'@Fetch' introduces an escaping closure and loses the task-local dependency" + ) { + assertInlineSnapshot(of: model.searchResults.rows, as: .customDump) { + """ + [ + [0]: SearchRemindersModel.Row( + isPastDue: false, + notes: "", + reminder: Reminder( + id: UUID(00000000-0000-0000-0000-00000000000A), + dueDate: Date(2009-02-17T23:31:30.000Z), + isCompleted: false, + isFlagged: false, + notes: "", + position: 8, + priority: .high, + remindersListID: UUID(00000000-0000-0000-0000-000000000001), + title: "Take out trash" + ), + remindersList: RemindersList( + id: UUID(00000000-0000-0000-0000-000000000001), + color: 3985191935, + position: 2, + title: "Family" + ), + tags: "", + title: "**Take** out trash" + ), + [1]: SearchRemindersModel.Row( + isPastDue: false, + notes: "", + reminder: Reminder( + id: UUID(00000000-0000-0000-0000-000000000006), + dueDate: Date(2008-08-07T23:31:30.000Z), + isCompleted: true, + isFlagged: false, + notes: "", + position: 4, + priority: nil, + remindersListID: UUID(00000000-0000-0000-0000-000000000000), + title: "Take a walk" + ), + remindersList: RemindersList( + id: UUID(00000000-0000-0000-0000-000000000000), + color: 1218047999, + position: 1, + title: "Personal" + ), + tags: "#car #kids #social", + title: "**Take** a walk" + ) + ] + """ + } } } @@ -78,10 +133,38 @@ extension BaseTestSuite { model.deleteCompletedReminders() try await model.$searchResults.load() #expect(model.searchResults.completedCount == 0) - assertInlineSnapshot(of: model.searchResults.rows, as: .customDump) { - """ - [] - """ + withKnownIssue( + "'@Fetch' introduces an escaping closure and loses the task-local dependency" + ) { + assertInlineSnapshot(of: model.searchResults.rows, as: .customDump) { + """ + [ + [0]: SearchRemindersModel.Row( + isPastDue: false, + notes: "", + reminder: Reminder( + id: UUID(00000000-0000-0000-0000-00000000000A), + dueDate: Date(2009-02-17T23:31:30.000Z), + isCompleted: false, + isFlagged: false, + notes: "", + position: 8, + priority: .high, + remindersListID: UUID(00000000-0000-0000-0000-000000000001), + title: "Take out trash" + ), + remindersList: RemindersList( + id: UUID(00000000-0000-0000-0000-000000000001), + color: 3985191935, + position: 2, + title: "Family" + ), + tags: "", + title: "**Take** out trash" + ) + ] + """ + } } } }