Skip to content

Fix CloudKit data loss on delete-then-reinsert#421

Open
jsutula wants to merge 2 commits intopointfreeco:mainfrom
jsutula:fix-data-loss-on-delete-then-reinsert
Open

Fix CloudKit data loss on delete-then-reinsert#421
jsutula wants to merge 2 commits intopointfreeco:mainfrom
jsutula:fix-data-loss-on-delete-then-reinsert

Conversation

@jsutula
Copy link

@jsutula jsutula commented Mar 13, 2026

Fixes #418

Root cause

Reinsertion of a record after a delete does not produce a new pending .saveRecord change entry due to missing "undelete" state detection in afterInsert trigger. No new .saveRecord means that only the .deleteRecord is propagated.

Solution

Two parts:

1. Prerequisite change necessary for writing accurate tests: Update MockSyncEngineState to deduplicate changes across types like the real CKSyncEngine.State

From CKSyncEngine.State.add(pendingRecordZoneChanges:) documentation:

Note
The order in which you apply record zone changes is important.
For example:

  • If you add .saveRecord(recordA) then .deleteRecord(recordA), the sync engine
    discards the save and sends only the delete change.
  • If you add .deleteRecord(recordA) then .saveRecord(recordA), the sync engine
    discards the delete and sends only the save change.

MockSyncEngineState.add(pendingRecordZoneChanges:) deduplicates fully identical changes (type+ID) by virtue of the underlying OrderedSet, but does not deduplicate when the type (save vs delete) is different, as is described above with CKSyncEngine.State. MockSyncEngineState.add(pendingDatabaseChanges:) has a similar issue. The fix: update both to deduplicate based on ID alone which will allow for the cross-type deduplication described above, and add tests.

2. Primary fix: Detect "undelete" on reinsertion in order to queue new .saveRecord change

Update the afterInsert trigger to conditionally reset _isDeleted to false if it's been set to true (as it will be in a delete-then-reinsert scenario), and update userModificationTime to the current time (as would occur in a normal insert or update). Hook up a new afterUndeleteTrigger that listens for this specific change and queues a .saveRecord change via syncEngine.$didUpdate (just as would occur on a normal insert or update).

Testing

In addition to new unit tests, performed a manual test with 9ea5bdd applied to verify that the data loss issue no longer occurs. Result (compare with before):

Screen.Recording.2026-03-13.at.12.32.44.AM.mov

@jsutula jsutula force-pushed the fix-data-loss-on-delete-then-reinsert branch from 332871e to 1f5150a Compare March 13, 2026 02:17
@jsutula jsutula changed the title Fix CloudKit data loss on delete-then-reinsert (#418) Fix CloudKit data loss on delete-then-reinsert Mar 13, 2026
@jsutula jsutula force-pushed the fix-data-loss-on-delete-then-reinsert branch from 1f5150a to bdd291d Compare March 13, 2026 08:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CloudKit data loss after deleting then re-inserting record with the same UUID

1 participant