CloudKit data loss after deleting then re-inserting record with the same UUID
Evaluación
- Dificultad
- 4/5
- Tiempo estimado
- 3-5 días
- Aptitud para principiantes
- 45/100
- Tipo de issue
- Error
- Claridad
- Bastante claro
- Estado de actividad
- Tranquilo
- Stack tecnológico
- sqlite, swift
Línea de trabajo
Start with the CloudKitDemo changes in draft PR #417 and run the reproduced delete-then-reinsert flow described in the issue. Read the synchronization path around SyncEngine behavior and verify the result across devices. Done means the scenario no longer causes remote data loss, or the project documents the pattern and reports it clearly if it is unsupported.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
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:
- Delete all ItemAsset records having a specific itemID foreign key (there could be multiple records and the goal was to have just one)
- 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:
- Delete ItemAsset record having a specific itemID PK+FK
- 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):
- Draft PR with 100% repro setup: https://github.com/pointfreeco/sqlite-data/pull/417
- Repro video
- Observe alternating (data loss) -> (asset present) -> (data loss)... pattern. Presumably every other "Update asset" succeeds because there's no CloudKit record to delete anymore, so the insert makes its way through.
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
mainbranch 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):
- Draft PR with 100% repro setup: https://github.com/pointfreeco/sqlite-data/pull/417
- Repro video
- Observe alternating (data loss) -> (asset present) -> (data loss)... pattern. Presumably every other "Update asset" succeeds because there's no CloudKit record to delete anymore, so the insert makes its way through.
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: https://github.com/pointfreeco/sqlite-data/pull/417
SQLiteData version information
65502acdb033ec8025a0bcc443abf2f4ca0598f9
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
- Lenguaje dominante
- Swift
- Estrellas
- 1.9k
- Forks
- 154
- Merge medio
- 2 d 18 h
- PR fusionados (30 d)
- 1
Preparar el entorno
Este proyecto no incluye contenedor de desarrollo, Dockerfile ni guía de contribución, así que la configuración corre por tu cuenta: empieza por su README y consulta nuestra guía para la primera contribución para los pasos generales.
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de pointfreeco/sqlite-data
-
bug
Dificultad 1/5 Menos de una hora Aptitud para principiantes 84/100
pointfreeco/sqlite-data#558 · 1 comentario ·
-
bug
Dificultad 4/5 3-5 días Aptitud para principiantes 50/100
pointfreeco/sqlite-data#557 · 2 comentarios ·
-
Dificultad 5/5 Más de una semana Aptitud para principiantes 38/100
pointfreeco/sqlite-data#551 ·
-
Child records deleted locally when their parent's save failed with `quotaExceeded`Posiblemente ocupada Un pull request vinculado a esta issue está abierto o ya se fusionó. Abiertobug
Dificultad 4/5 3-5 días Aptitud para principiantes 52/100
pointfreeco/sqlite-data#547 · 1 comentario · 1 reacción ·
-
bug
Dificultad 4/5 3-5 días Aptitud para principiantes 35/100
pointfreeco/sqlite-data#356 ·
Todos los issues de pointfreeco/sqlite-data
Issues similares
-
TokenIterator.prepare drops LMOutput.State in the .logits branch, breaking generation for every VLMAbierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 82/100
ml-explore/mlx-swift-lm#681 ·
Los mantenedores suelen responder en 2 días
-
p: pigeon team-ecosystem
Dificultad 2/5 1-3 horas Aptitud para principiantes 76/100
Los mantenedores suelen responder en 1 día
-
Empty label or headline exports the editor hint ("LABEL" / "Headline goes here") into the PNGAbierto
Dificultad 1/5 1-3 horas Aptitud para principiantes 88/100
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
omacom/try-omarchy#319 ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 85/100
Los mantenedores suelen responder en 1 día