You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
After reporting #274, I’ve encountered another incorrect CloudKit sync behavior. When a device sends a batch containing (a) an update to a parent record and (b) an inserted child record referencing that parent, and the parent has a newer version on the server, the child record is silently dropped and never synced again.
I wasn’t able to reproduce this in any built-in sample apps, so I slightly tweaked the Reminders example. The modification lives on this branch and adds a toolbar button for triggering the database operations. See also this diff.
Here is a step-by-step description of the scenario:
Devices A and B both start with an empty reminders list titled “List” with the ID 80ef064a-3dd8-4406-9a7b-548bedd18c65.
Both devices go offline.
Device B updates the title of the list to “List changed on B”.
UPDATE"remindersLists"SET"title"='List changed on B'WHERE ("remindersLists"."id") IN (('80ef064a-3dd8-4406-9a7b-548bedd18c65'))
Device A updates the title to “List changed on A” and inserts a new reminder titled “Reminder added on A” with ID 48107186-4283-41a7-ad75-23bbf018438b.
At this point, a race begins between the two devices. The issue occurs only if device B sends its changes first, and device A attempts to send its changes before fetching B’s updates from the server. Due to the nondeterminism in CKSyncEngine, I haven’t found a reliable way to force this exact ordering, so it may take a few attempts. The following steps assume this sequence.
Device A then attempts to send its changes in a single batch. Because a newer parent record now exists on the server, the update to the reminders list is rejected with a serverRecordChanged error. However, the save of the newly inserted child reminder is also rejected, this time with a batchRequestFailed error.
I’m not entirely sure about this behavior, but it appears that CloudKit refuses to save a child record when its parent record is considered outdated on the server.
Device A upserts the reminders list from the fetched server record, but because the local title holds a newer timestamp, nothing changes. (Since not all fields were excluded from updates, this schedules another save.)
INSERT INTO"remindersLists" ("id", "color", "position", "title")
VALUES ('80ef064a-3dd8-4406-9a7b-548bedd18c65', 1167716351, 2, 'List changed on B') ON CONFLICT("id") DO UPDATESET"position"="excluded"."position"
Device A fetches changes from the server and attempts to apply them. Again, as the local reminders list’s title is newer, the one coming from the server isn’t applied.
Both devices end up with a synced reminders list, but the inserted reminder never makes it to the server. Since it was rejected in the original batch and never retried on its own, it remains unsynced until it’s modified again.
The chosen example in the Reminders sample app may look a bit constructed, but in my project, creating a child record while modifying its parent (with a high likelihood of hitting a conflict) is a common and intentional pattern. This is just a minimal repro to demonstrate the problem. The key issue is that the child insert is permanently dropped unless it’s modified again, which leads to data divergence similar to #274.
Checklist
I have determined whether this bug is also reproducible in a vanilla SwiftUI project.
I have determined whether this bug is also reproducible in a vanilla GRDB project.
If possible, I've reproduced the issue using the main branch of this package.
Description
After reporting #274, I’ve encountered another incorrect CloudKit sync behavior. When a device sends a batch containing (a) an update to a parent record and (b) an inserted child record referencing that parent, and the parent has a newer version on the server, the child record is silently dropped and never synced again.
I wasn’t able to reproduce this in any built-in sample apps, so I slightly tweaked the Reminders example. The modification lives on this branch and adds a toolbar button for triggering the database operations. See also this diff.
Here is a step-by-step description of the scenario:
Devices A and B both start with an empty reminders list titled “List” with the ID
80ef064a-3dd8-4406-9a7b-548bedd18c65.Both devices go offline.
Device B updates the title of the list to “List changed on B”.
48107186-4283-41a7-ad75-23bbf018438b.Warning
At this point, a race begins between the two devices. The issue occurs only if device B sends its changes first, and device A attempts to send its changes before fetching B’s updates from the server. Due to the nondeterminism in
CKSyncEngine, I haven’t found a reliable way to force this exact ordering, so it may take a few attempts. The following steps assume this sequence.serverRecordChangederror. However, the save of the newly inserted child reminder is also rejected, this time with abatchRequestFailederror.I’m not entirely sure about this behavior, but it appears that CloudKit refuses to save a child record when its parent record is considered outdated on the server.
The chosen example in the Reminders sample app may look a bit constructed, but in my project, creating a child record while modifying its parent (with a high likelihood of hitting a conflict) is a common and intentional pattern. This is just a minimal repro to demonstrate the problem. The key issue is that the child insert is permanently dropped unless it’s modified again, which leads to data divergence similar to #274.
Checklist
mainbranch of this package.SQLiteData version information
mainSharing version information
2.7.4
GRDB version information
7.8.0
Destination operating system
iOS 26
Xcode version information
26.1 (17B54)
Swift Compiler version information