diff --git a/Sources/SQLiteData/CloudKit/SyncEngine.swift b/Sources/SQLiteData/CloudKit/SyncEngine.swift index 70ecca8a..6d035ae5 100644 --- a/Sources/SQLiteData/CloudKit/SyncEngine.swift +++ b/Sources/SQLiteData/CloudKit/SyncEngine.swift @@ -1203,9 +1203,9 @@ for (recordType, recordIDs) in deletedRecordIDsByRecordType { let recordPrimaryKeys = recordIDs.compactMap(\.recordPrimaryKey) if let table = tablesByName[recordType] { - func open(_: T.Type) { - withErrorReporting(.sqliteDataCloudKitFailure) { - try userDatabase.write { db in + func open(_: T.Type) async { + await withErrorReporting(.sqliteDataCloudKitFailure) { + try await userDatabase.write { db in try T .where { $0.primaryKey.in( @@ -1222,7 +1222,7 @@ } } } - open(table) + await open(table) } else if recordType == CKRecord.SystemType.share { for recordID in recordIDs { await withErrorReporting(.sqliteDataCloudKitFailure) { @@ -1286,17 +1286,24 @@ case share(CKShare) case reference(CKShare.Reference) } - var shares: [ShareOrReference] = [] - for record in modifications { - if let share = record as? CKShare { - shares.append(.share(share)) - } else { - await upsertFromServerRecord(record) - if let shareReference = record.share { - shares.append(.reference(shareReference)) + let shares: [ShareOrReference] = + await withErrorReporting(.sqliteDataCloudKitFailure) { + try await userDatabase.write { db in + var shares: [ShareOrReference] = [] + for record in modifications { + if let share = record as? CKShare { + shares.append(.share(share)) + } else { + upsertFromServerRecord(record, db: db) + if let shareReference = record.share { + shares.append(.reference(shareReference)) + } + } + } + return shares } } - } + ?? [] await withTaskGroup(of: Void.self) { group in for share in shares { @@ -1524,93 +1531,97 @@ force: Bool = false ) async { await withErrorReporting(.sqliteDataCloudKitFailure) { + try await userDatabase.write { db in + upsertFromServerRecord(serverRecord, force: force, db: db) + } + } + } + + private func upsertFromServerRecord( + _ serverRecord: CKRecord, + force: Bool = false, + db: Database + ) { + withErrorReporting(.sqliteDataCloudKitFailure) { guard let table = tablesByName[serverRecord.recordType] else { guard let recordPrimaryKey = serverRecord.recordID.recordPrimaryKey else { return } - try await userDatabase.write { db in - try SyncMetadata.insert { - SyncMetadata( - recordPrimaryKey: recordPrimaryKey, - recordType: serverRecord.recordType, - parentRecordPrimaryKey: serverRecord.parent?.recordID.recordPrimaryKey, - parentRecordType: serverRecord.parent?.recordID.tableName, - lastKnownServerRecord: serverRecord, - _lastKnownServerRecordAllFields: serverRecord, - share: nil, + try SyncMetadata.insert { + SyncMetadata( + recordPrimaryKey: recordPrimaryKey, + recordType: serverRecord.recordType, + parentRecordPrimaryKey: serverRecord.parent?.recordID.recordPrimaryKey, + parentRecordType: serverRecord.parent?.recordID.tableName, + lastKnownServerRecord: serverRecord, + _lastKnownServerRecordAllFields: serverRecord, + share: nil, userModificationTime: serverRecord.userModificationTime - ) - } onConflict: { - ($0.recordPrimaryKey, $0.recordType) - } doUpdate: { - $0.setLastKnownServerRecord(serverRecord) - } - .execute(db) + ) + } onConflict: { + ($0.recordPrimaryKey, $0.recordType) + } doUpdate: { + $0.setLastKnownServerRecord(serverRecord) } + .execute(db) return } - let metadata = try await metadatabase.read { db in + let metadata = try SyncMetadata - .where { $0.recordName.eq(serverRecord.recordID.recordName) } - .fetchOne(db) - } + .where { $0.recordName.eq(serverRecord.recordID.recordName) } + .fetchOne(db) serverRecord.userModificationTime = metadata?.userModificationTime ?? serverRecord.userModificationTime - func open(_: T.Type) async throws { + func open(_: T.Type) throws { let columnNames: [String] if !force, let metadata, let allFields = metadata._lastKnownServerRecordAllFields { - columnNames = try await userDatabase.read { db in - var columnNames = T.TableColumns.writableColumns.map(\.name) - let row = try T.find(#sql("\(bind: metadata.recordPrimaryKey)")).fetchOne(db) - guard let row - else { - reportIssue( - """ - Local database record could not be found for '\(serverRecord.recordID.recordName)'. - """ - ) - return columnNames - } + var _columnNames = T.TableColumns.writableColumns.map(\.name) + let row = try T.find(#sql("\(bind: metadata.recordPrimaryKey)")).fetchOne(db) + if let row { serverRecord.update( with: allFields, row: T(queryOutput: row), - columnNames: &columnNames, + columnNames: &_columnNames, parentForeignKey: foreignKeysByTableName[T.tableName]?.count == 1 ? foreignKeysByTableName[T.tableName]?.first : nil ) - return columnNames + } else { + reportIssue( + """ + Local database record could not be found for '\(serverRecord.recordID.recordName)'. + """ + ) } + columnNames = _columnNames } else { columnNames = T.TableColumns.writableColumns.map(\.name) } - try await userDatabase.write { db in - do { - try #sql(upsert(T.self, record: serverRecord, columnNames: columnNames)).execute(db) - try UnsyncedRecordID.find(serverRecord.recordID).delete().execute(db) - try SyncMetadata - .where { $0.recordName.eq(serverRecord.recordID.recordName) } - .update { $0.setLastKnownServerRecord(serverRecord) } - .execute(db) - } catch { - guard - let error = error as? DatabaseError, - error.resultCode == .SQLITE_CONSTRAINT, - error.extendedResultCode == .SQLITE_CONSTRAINT_FOREIGNKEY - else { - throw error - } - try UnsyncedRecordID.insert(or: .ignore) { - UnsyncedRecordID(recordID: serverRecord.recordID) - } + do { + try #sql(upsert(T.self, record: serverRecord, columnNames: columnNames)).execute(db) + try UnsyncedRecordID.find(serverRecord.recordID).delete().execute(db) + try SyncMetadata + .where { $0.recordName.eq(serverRecord.recordID.recordName) } + .update { $0.setLastKnownServerRecord(serverRecord) } .execute(db) + } catch { + guard + let error = error as? DatabaseError, + error.resultCode == .SQLITE_CONSTRAINT, + error.extendedResultCode == .SQLITE_CONSTRAINT_FOREIGNKEY + else { + throw error + } + try UnsyncedRecordID.insert(or: .ignore) { + UnsyncedRecordID(recordID: serverRecord.recordID) } + .execute(db) } } - try await open(table) + try open(table) } } diff --git a/Tests/SQLiteDataTests/CloudKitTests/SharingTests.swift b/Tests/SQLiteDataTests/CloudKitTests/SharingTests.swift index e89ca6b5..ca4e414a 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/SharingTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/SharingTests.swift @@ -383,7 +383,7 @@ │ _isDeleted: false, │ │ hasLastKnownServerRecord: true, │ │ isShared: false, │ - │ userModificationDate: Date(1970-01-01T00:00:00.000Z) │ + │ userModificationTime: 0 │ │ ) │ ├─────────────────────────────────────────────────────────────────────────────────────────┤ │ SyncMetadata( │ @@ -412,7 +412,7 @@ │ _isDeleted: false, │ │ hasLastKnownServerRecord: true, │ │ isShared: false, │ - │ userModificationDate: Date(1970-01-01T00:01:00.000Z) │ + │ userModificationTime: 60 │ │ ) │ ├─────────────────────────────────────────────────────────────────────────────────────────┤ │ SyncMetadata( │ @@ -441,7 +441,7 @@ │ _isDeleted: false, │ │ hasLastKnownServerRecord: true, │ │ isShared: false, │ - │ userModificationDate: Date(1970-01-01T00:01:00.000Z) │ + │ userModificationTime: 60 │ │ ) │ └─────────────────────────────────────────────────────────────────────────────────────────┘ """ @@ -1105,7 +1105,7 @@ │ _isDeleted: false, │ │ hasLastKnownServerRecord: true, │ │ isShared: false, │ - │ userModificationDate: Date(1970-01-01T00:00:00.000Z) │ + │ userModificationTime: 0 │ │ ) │ ├──────────────────────────────────────────────────────────────────────────────────────────────┤ │ SyncMetadata( │ @@ -1134,7 +1134,7 @@ │ _isDeleted: false, │ │ hasLastKnownServerRecord: true, │ │ isShared: false, │ - │ userModificationDate: Date(1970-01-01T00:00:00.000Z) │ + │ userModificationTime: 0 │ │ ) │ ├──────────────────────────────────────────────────────────────────────────────────────────────┤ │ SyncMetadata( │ @@ -1167,7 +1167,7 @@ │ _isDeleted: false, │ │ hasLastKnownServerRecord: true, │ │ isShared: true, │ - │ userModificationDate: Date(1970-01-01T00:00:00.000Z) │ + │ userModificationTime: 0 │ │ ) │ └──────────────────────────────────────────────────────────────────────────────────────────────┘ """ diff --git a/Tests/SQLiteDataTests/CloudKitTests/SyncEngineLifecycleTests.swift b/Tests/SQLiteDataTests/CloudKitTests/SyncEngineLifecycleTests.swift index cce9b97f..76784d08 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/SyncEngineLifecycleTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/SyncEngineLifecycleTests.swift @@ -296,7 +296,7 @@ │ _isDeleted: false, │ │ hasLastKnownServerRecord: true, │ │ isShared: false, │ - │ userModificationDate: Date(1970-01-01T00:00:00.000Z) │ + │ userModificationTime: 0 │ │ ) │ ├───────────────────────────────────────────────────────────────────────────┤ │ SyncMetadata( │ @@ -312,7 +312,7 @@ │ _isDeleted: false, │ │ hasLastKnownServerRecord: false, │ │ isShared: false, │ - │ userModificationDate: Date(1970-01-01T00:01:00.000Z) │ + │ userModificationTime: 60 │ │ ) │ └───────────────────────────────────────────────────────────────────────────┘ """ diff --git a/Tests/SQLiteDataTests/Internal/BaseCloudKitTests.swift b/Tests/SQLiteDataTests/Internal/BaseCloudKitTests.swift index c6cc339c..5e34de4e 100644 --- a/Tests/SQLiteDataTests/Internal/BaseCloudKitTests.swift +++ b/Tests/SQLiteDataTests/Internal/BaseCloudKitTests.swift @@ -155,8 +155,8 @@ extension SyncEngine { convenience init( container: any CloudContainer, userDatabase: UserDatabase, - tables: [any PrimaryKeyedTable.Type], - privateTables: [any PrimaryKeyedTable.Type] = [], + tables: [any (PrimaryKeyedTable & _SendableMetatype).Type], + privateTables: [any (PrimaryKeyedTable & _SendableMetatype).Type] = [], startImmediately: Bool = true ) async throws { try self.init(