Apariencia
0054. Mobile: sync de catálogos (sectores, piscinas, tipos de equipo) usa hard replace, no upsert
Estado
Aceptada
Contexto
PoolsRepository.syncAll() trae sectores, piscinas y tipos de equipo del backend y los guarda en Drift. Un upsert puro (insertAllOnConflictUpdate sin borrar antes) deja datos fantasma: si el backend deja de devolver un registro (borrado, o el usuario perdió acceso a un sector), la fila vieja sigue en el dispositivo indefinidamente. replaceSectors y replacePools (app_database.dart) ya resolvían esto con un delete + insertAllOnConflictUpdate en una transacción. _syncEquipmentTypes no seguía el mismo patrón: usaba upsertEquipmentTypes, que nunca borraba tipos obsoletos.
No hay claves foráneas en el schema Drift (grep references → 0 resultados), así que un hard replace de EquipmentTypes no rompe constraints. PoolEquipments denormaliza equipmentTypeName, así que tampoco depende de un join con EquipmentTypes para mostrarse.
Decisión
Todo catálogo que se sincroniza completo desde el backend (sectores, piscinas, tipos de equipo) usa el patrón hard replace: delete(tabla).go() + insertAllOnConflictUpdate en una única transacción. Se agregó AppDatabase.replaceEquipmentTypes con este patrón y se reemplazó el único uso de upsertEquipmentTypes en PoolsRepository._syncEquipmentTypes; upsertEquipmentTypes se eliminó (sin otros consumidores).
PoolEquipments (equipo asignado por piscina) queda fuera de esta decisión: no es un catálogo global, se sincroniza por piscina (syncEquipmentForPool) con deleteEquipmentForPool + upsertPoolEquipments, que ya tiene el mismo efecto (borra todo lo de esa piscina antes de reinsertar).
Consecuencias
- Regla general para futuros catálogos sincronizados completos: preferir
replaceX(hard replace transaccional) sobreupsertX, salvo que exista una razón explícita para conservar filas que el backend dejó de devolver. - Cubierto con tests de regresión en
test/features/pools/pools_repository_test.dartytest/shared/database/app_database_test.dart(Drift in-memory): una segunda sync más chica borra los registros ausentes, para sectores, piscinas y tipos de equipo. - Esta tarea (
sync-offline-hardening) solo cubrió la sync de lectura. La cola de escrituras offline con last-write-wins se implementó después ensync-offline-escrituras— ver ADR-0055. - Ese ADR-0055 extiende este mismo patrón: los
replace*ForPool/ForCyclede los 6 módulos de campo (parámetros de agua, alimentación, muestreos, poblaciones, eventos, raleos) ahora excluyen deldeletelas filas con una escritura pendiente en el outbox (pendingOp IS NOT NULL) — un hard replace sin esa excepción borraría un registro que el técnico acaba de crear offline antes de que llegue a sincronizar.