Skip to content

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

@jsutula

Description

@jsutula

Description

I stumbled across this issue after refactoring an ItemAsset-like table to no longer have its own UUID and instead use the foreign key into its parent table (Item.ID), as suggested in the documentation's uniqueness constraints section.

Pre-refactor (distinct ItemAsset.ID UUID field), the process to update an ItemAsset record consisted of two-steps:

  1. Delete all ItemAsset records having a specific itemID foreign key (there could be multiple records and the goal was to have just one)
  2. Insert a fresh ItemAsset record.

Post-refactor (combined PK/FK ItemAsset.itemID), the initial step to delete should no longer be necessary (only one ItemAsset record per Item record, enforced by new PK), however, I missed this, and the delete step remained in place. So the steps remained roughly the same:

  1. Delete ItemAsset record having a specific itemID PK+FK
  2. Upsert ItemAsset record.

With the new delete-then-reinsert setup, when an ItemAsset record exists and is updated, only the deletion of the record is synchronized to CloudKit, not the insert, resulting in unexpected data loss. The ItemAsset record remains visible on the device which performed the update, but will be deleted on all other devices after the next CloudKit sync.

I've reproduced this behavior with a simple addition to the CloudKitDemo example project (introducing a CounterAsset table and "Update asset" button):

  1. Draft PR with 100% repro setup: Repro CloudKit data loss on delete-then-reinsert #417
  2. Repro video

Note: another precondition of this behavior is that delete and reinsert events need to occur within a couple seconds of each other (to be part of the same sync batch). If they are spaced out further than that and end up in separate sync batches, the issue does not reproduce.

In my case, this behavior was easy to work around (just remove the unnecessary delete), but I think this still warrants either a fix or at least an update to documentation and Point-Free Way skills warning against this pattern.

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.
  • This issue hasn't been addressed in an existing GitHub issue or discussion.

Expected behavior

Ideally, the local delete-then-reinsert events in this scenario should be collapsed to a single CloudKit update event (not delete). But if that's infeasible, or this really does just represent an anti-pattern, then there should be a developer-facing error/issue thrown when SyncEngine detects this situation.

Actual behavior

Copy/pasted from Description:

With the new delete-then-reinsert setup, when an ItemAsset record exists and is updated, only the deletion of the record is synchronized to CloudKit, not the insert, resulting in unexpected data loss. The ItemAsset record remains visible on the device which performed the update, but will be deleted on all other devices after the next CloudKit sync.

I've reproduced this behavior with a simple addition to the CloudKitDemo example project (introducing a CounterAsset table and "Update asset" button):

  1. Draft PR with 100% repro setup: Repro CloudKit data loss on delete-then-reinsert #417
  2. Repro video

Note: another precondition of this behavior is that delete and reinsert events need to occur within a couple seconds of each other (to be part of the same sync batch). If they are spaced out further than that and end up in separate sync batches, the issue does not reproduce.

Reproducing project

Draft PR: #417

SQLiteData version information

65502ac

Sharing version information

2.7.4

GRDB version information

7.9.0

Destination operating system

iOS 26

Xcode version information

Xcode 26.3

Swift Compiler version information

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions