Repository navigation
Keep children of never-uploaded parents and retry quotaExceeded saves - #548
jalalawqati wants to merge 1 commit into
Conversation
…eeco#548) Backported to 1.6.0 from pointfreeco#548, which fixes pointfreeco#547. When a parent record's save is dropped (quotaExceeded, and any other error handleSentRecordZoneChanges swallows with `continue`), a child sent later is rejected with referenceViolation. The handler read that as "the parent was deleted remotely" and applied the FK's ON DELETE CASCADE, destroying the child row locally. This skips the destructive branch when the parent has no lastKnownServerRecord — i.e. it was never on the server, so nothing was deleted — and re-enqueues parent and child instead. Also moves quotaExceeded into the retry branch. Source hunks only; the PR's mock and test changes are omitted. Remove this fork once pointfreeco#548 merges and ships upstream; upgrade straight to that release rather than rebasing this patch.
Signing in clears CKSyncEngine's pending changes, so `.signIn` re-enqueues the records CloudKit does not know about: `enqueueUnknownRecordsForCloudKit` touches every row without a last known server record. A record that was sent but never saved has one, though, because a record is stored as its own last known server record when it is built for sending. With pointfreeco#548 a save rejected with `quotaExceeded` is retried, but if the user signs out of iCloud, keeps their local data and signs back in, the retry is dropped with the other pending changes, and the record is never uploaded unless it is edited again. Also enqueue live rows whose last known server record has not been saved to the server. Their archives are decoded from a metadatabase cursor before the write, and the updates are batched to stay under SQLite's bound parameter limit. Covered by `saveWhileQuotaExceededThenSignOutAndIn`, which fails without this change. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A child save rejected with referenceViolation was handled as a remote delete of its parent: cascade deleted the child locally, set null / set default unlinked it. When the parent had simply not reached the server yet (for example waiting out quotaExceeded), the child was lost. Now, if the parent exists locally and the server has never saved it (no change tag on its stored server record, which is written before the send), parent and child are re-queued after quotaRetryDelay. Adapted from upstream pointfreeco#548 without its immediate quota retry. Also reset the quota retry delay to its 30 s default in quotaExceededSavesAreRequeuedAfterADelayAndThenUpload (was 300 s). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@mbrandonw @stephencelis I would appreciate if you could have a look at this whenever you have a chance. We're currently getting many reports from users who are running into this and losing their data. Thanks in advance |
|
Hi @jalalawqati, we merged #335 recently which should fix this problem. Can you give it a test when you have a chance? |
|
@mbrandonw thanks for taking the time to look into this. I tried I haven't had time to look into it in detail, but I've attached the logs below. This was from a run on an iOS 27.2 Simulator signed into an iCloud account that has no free storage left. I confirmed again that the issue isn't reproducible however on a build pinned against the commit in this PR (#548) |
|
Hey @jalalawqati, thanks for trying it out! It does seem we need a little more work in our quota detection, and we're working on it now! Will have more to share soon. |
Description
Fixes #547
When a user's iCloud storage is full, a parent table save fails with
quotaExceededand is dropped. When one of the parent's child tables is sent later, it this time fails withreferenceViolation(rather thanquotaExceeded), andSyncEngineapplies the parent'sON DELETEaction, causing the child table to be deleted.We couldn't find a clean fix to get around this in our app unfortunately. Happy to know your suggestions.
We tested the changes in this branch with our app on a device with full iCloud storage and the issue was no longer reproducible