Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 42 additions & 12 deletions Sources/SQLiteData/CloudKit/CloudKit+StructuredQueries.swift
Original file line number Diff line number Diff line change
Expand Up @@ -156,10 +156,13 @@
package func setValue(
_ newValue: some CKRecordValueProtocol & Equatable,
forKey key: CKRecord.FieldKey,
at userModificationTime: Int64
at userModificationTime: Int64,
requireStrictlyNewer: Bool = false
) -> Bool {
guard
encryptedValues[at: key] <= userModificationTime,
requireStrictlyNewer
? encryptedValues[at: key] < userModificationTime
: encryptedValues[at: key] <= userModificationTime,
encryptedValues[key] != newValue
else { return false }
encryptedValues[key] = newValue
Expand All @@ -172,15 +175,18 @@
package func setAsset(
_ newValue: CKAsset,
forKey key: CKRecord.FieldKey,
at userModificationTime: Int64
at userModificationTime: Int64,
requireStrictlyNewer: Bool = false
) -> Bool {
@Dependency(\.dataManager) var dataManager
guard
let fileURL = newValue.fileURL,
let hash = dataManager.sha256(of: fileURL)
else { return false }
guard
encryptedValues[at: key] <= userModificationTime,
requireStrictlyNewer
? encryptedValues[at: key] < userModificationTime
: encryptedValues[at: key] <= userModificationTime,
encryptedValues[hash: key] != hash
else { return false }

Expand Down Expand Up @@ -223,9 +229,13 @@
@discardableResult
package func removeValue(
forKey key: CKRecord.FieldKey,
at userModificationTime: Int64
at userModificationTime: Int64,
requireStrictlyNewer: Bool = false
) -> Bool {
guard encryptedValues[at: key] <= userModificationTime
guard
requireStrictlyNewer
? encryptedValues[at: key] < userModificationTime
: encryptedValues[at: key] <= userModificationTime
else {
return false
}
Expand Down Expand Up @@ -283,26 +293,42 @@
}
}

@discardableResult
func update<T: PrimaryKeyedTable>(
with other: CKRecord,
row: T,
columnNames: inout [String],
parentForeignKey: ForeignKey?
) {
) -> Bool {
typealias EquatableCKRecordValueProtocol = CKRecordValueProtocol & Equatable

self.userModificationTime = other.userModificationTime
var didPreserveLocalValues = false
for column in T.TableColumns.writableColumns {
func open<Root, Value>(_ column: some WritableTableColumnExpression<Root, Value>) {
let key = column.name
let keyPath = column.keyPath as! KeyPath<T, Value.QueryOutput>
let didSet: Bool
if let value = other[key] as? CKAsset {
didSet = setAsset(value, forKey: key, at: other.encryptedValues[at: key])
didSet = setAsset(
value,
forKey: key,
at: other.encryptedValues[at: key],
requireStrictlyNewer: true
)
} else if let value = other.encryptedValues[key] as? any EquatableCKRecordValueProtocol {
didSet = setValue(value, forKey: key, at: other.encryptedValues[at: key])
didSet = setValue(
value,
forKey: key,
at: other.encryptedValues[at: key],
requireStrictlyNewer: true
)
} else if other.encryptedValues[key] == nil {
didSet = removeValue(forKey: key, at: other.encryptedValues[at: key])
didSet = removeValue(
forKey: key,
at: other.encryptedValues[at: key],
requireStrictlyNewer: true
)
} else {
didSet = false
}
Expand Down Expand Up @@ -334,13 +360,17 @@
}
if didSet || isRowValueModified {
columnNames.removeAll(where: { $0 == key })
if didSet, let parentForeignKey, key == parentForeignKey.from {
self.parent = other.parent
if didSet {
didPreserveLocalValues = true
if let parentForeignKey, key == parentForeignKey.from {
self.parent = other.parent
}
}
}
}
open(column)
}
return didPreserveLocalValues
}

package var userModificationTime: Int64 {
Expand Down
51 changes: 40 additions & 11 deletions Sources/SQLiteData/CloudKit/SyncEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1591,24 +1591,33 @@
case share(CKShare)
case reference(CKShare.Reference)
}
let shares: [ShareOrReference] =
let (shares, preservedRecordIDs): ([ShareOrReference], [CKRecord.ID]) =
await withErrorReporting(.sqliteDataCloudKitFailure) {
try await userDatabase.write { db in
var shares: [ShareOrReference] = []
var preservedRecordIDs: [CKRecord.ID] = []
for record in modifications {
if let share = record as? CKShare {
shares.append(.share(share))
} else {
upsertFromServerRecord(record, db: db)
if upsertFromServerRecord(record, db: db) {
preservedRecordIDs.append(record.recordID)
}
if let shareReference = record.share {
shares.append(.reference(shareReference))
}
}
}
return shares
return (shares, preservedRecordIDs)
}
}
?? []
?? ([], [])

if !preservedRecordIDs.isEmpty {
syncEngine.state.add(
pendingRecordZoneChanges: preservedRecordIDs.map { .saveRecord($0) }
)
}

await withTaskGroup(of: Void.self) { group in
for share in shares {
Expand Down Expand Up @@ -1897,28 +1906,31 @@
}
}

@discardableResult
private func upsertFromServerRecord(
_ serverRecord: CKRecord,
force: Bool = false
) async {
) async -> Bool {
await withErrorReporting(.sqliteDataCloudKitFailure) {
try await userDatabase.write { db in
upsertFromServerRecord(serverRecord, force: force, db: db)
}
}
?? false
}

@discardableResult
private func upsertFromServerRecord(
_ serverRecord: CKRecord,
force: Bool = false,
db: Database
) {
) -> Bool {
withErrorReporting(.sqliteDataCloudKitFailure) {
guard
let recordPrimaryKey = serverRecord.recordID.recordPrimaryKey,
serverRecord.encryptedValues[CKRecord.userModificationTimeKey] != nil
else {
return
return false
}

try SyncMetadata.insert {
Expand Down Expand Up @@ -1950,18 +1962,19 @@
let metadata = try SyncMetadata.find(serverRecord.recordID).fetchOne(db),
let table = tablesByName[serverRecord.recordType]
else {
return
return false
}

serverRecord.userModificationTime = metadata.userModificationTime

func open<T>(_ table: some SynchronizableTable<T>) throws {
func open<T>(_ table: some SynchronizableTable<T>) throws -> Bool {
var columnNames: [String] = T.TableColumns.writableColumns.map(\.name)
var didPreserveLocalValues = false
if !force,
let allFields = metadata._lastKnownServerRecordAllFields,
let row = try T.unscoped.find(#sql("\(bind: metadata.recordPrimaryKey)")).fetchOne(db)
{
serverRecord.update(
didPreserveLocalValues = serverRecord.update(
with: allFields,
row: T(queryOutput: row),
columnNames: &columnNames,
Expand Down Expand Up @@ -1994,9 +2007,25 @@
}
.execute(db)
}
if didPreserveLocalValues, let lastKnownServerRecord = metadata.lastKnownServerRecord {
if let lastKnownDate = lastKnownServerRecord.modificationDate,
let serverDate = serverRecord.modificationDate
{
if serverDate < lastKnownDate {
didPreserveLocalValues = false
}
} else if let lastKnownTag = lastKnownServerRecord._recordChangeTag,
let serverTag = serverRecord._recordChangeTag,
serverTag < lastKnownTag
{
didPreserveLocalValues = false
}
}
return didPreserveLocalValues
}
try open(table)
return try open(table)
}
?? false
}

private func refreshLastKnownServerRecord(_ record: CKRecord) async {
Expand Down
Loading