Five synced tables declare an hlc column but their entity types are absent
from SyncRepository._hlcTargets, so their rows never appear in an
incremental changeset. Local edits reach peers only on a full base
republish, which is why the gap reads as working sync in manual testing.
Mechanism
markRecordPending stamps the HLC through _stampHlc
(lib/core/data/repositories/sync_repository.dart:513):
Future<void> _stampHlc(String entityType, String recordId) async {
final target = _hlcTargets[entityType];
if (target == null) return; // silent no-op for unregistered entities
...
}
The affected repositories all call markRecordPending faithfully, so the
write path looks correct. But the entity type is not in the map, the stamp
is skipped, and hlc stays NULL. The incremental export then filters
hlc.isBiggerThanValue(hlcSince), and SQL NULL > x is false, so the row
is excluded from every changeset:
// lib/core/services/sync/sync_data_serializer.dart:4835
final query = _db.select(_db.serviceKinds)
..where((t) => t.isBuiltIn.equals(false));
if (hlcSince != null) {
query.where((t) => t.hlc.isBiggerThanValue(hlcSince));
}
Affected tables
| Entity type |
Table |
Notes |
serviceKinds |
service_kinds |
diver-scoped custom maintenance task catalog |
serviceSchedules |
service_schedules |
per equipment item service clock |
cylinderConfigs |
cylinder_configs |
found 2026-08-05 auditing PR #868 |
cylinderConfigItems |
cylinder_config_items |
same |
equipmentSetGeofences |
equipment_set_geofences |
same shape, pre-existing |
Design choice per table
Two legitimate designs; each table needs a deliberate pick:
- Own clock: register in
_hlcTargets, as equipmentSets does.
- Clockless child riding the parent: export by selecting parents with
hlc > since and gathering their children, as _exportEquipmentSetItems
and courseRequirementDives do. The child must then NOT filter on its
own hlc.
Note that riding the parent only works when a child write re-stamps the
parent. Editing a ServiceSchedule does not touch the parent equipment
row today, so option 2 there would need extra plumbing.
Backfill
Registering the tables only helps rows written from that point on. Rows
already on disk carry hlc IS NULL and stay invisible until they happen to
be edited. A one-time backfill is needed, mirroring
backfillMediaEnrichmentHlc (sync_repository.dart:486), which exists for
exactly this situation after media_enrichment gained its hlc column in
v130.
Testing trap
A serializer test that seeds via upsertRecord cannot catch this. The
remote apply path carries the peer's HLC in the payload, so the filter
appears to work. The test must write through the repository and then assert
the column is populated:
await repository.createKind(kind);
final hlc = await db.customSelect(
'SELECT hlc FROM service_kinds WHERE id = ?', variables: [Variable(kind.id)],
).getSingle();
expect(hlc.read<String?>('hlc'), isNotNull);
Impact
Multi-device users lose custom maintenance tasks, per item service clocks,
cylinder configurations, and equipment set geofences from incremental
syncs. Split out from #829, whose default-price feature stores data on
service_kinds and service_schedules and inherits this gap.
Five synced tables declare an
hlccolumn but their entity types are absentfrom
SyncRepository._hlcTargets, so their rows never appear in anincremental changeset. Local edits reach peers only on a full base
republish, which is why the gap reads as working sync in manual testing.
Mechanism
markRecordPendingstamps the HLC through_stampHlc(
lib/core/data/repositories/sync_repository.dart:513):The affected repositories all call
markRecordPendingfaithfully, so thewrite path looks correct. But the entity type is not in the map, the stamp
is skipped, and
hlcstays NULL. The incremental export then filtershlc.isBiggerThanValue(hlcSince), and SQLNULL > xis false, so the rowis excluded from every changeset:
Affected tables
serviceKindsservice_kindsserviceSchedulesservice_schedulescylinderConfigscylinder_configscylinderConfigItemscylinder_config_itemsequipmentSetGeofencesequipment_set_geofencesDesign choice per table
Two legitimate designs; each table needs a deliberate pick:
_hlcTargets, asequipmentSetsdoes.hlc > sinceand gathering their children, as_exportEquipmentSetItemsand
courseRequirementDivesdo. The child must then NOT filter on itsown
hlc.Note that riding the parent only works when a child write re-stamps the
parent. Editing a
ServiceScheduledoes not touch the parentequipmentrow today, so option 2 there would need extra plumbing.
Backfill
Registering the tables only helps rows written from that point on. Rows
already on disk carry
hlc IS NULLand stay invisible until they happen tobe edited. A one-time backfill is needed, mirroring
backfillMediaEnrichmentHlc(sync_repository.dart:486), which exists forexactly this situation after
media_enrichmentgained itshlccolumn inv130.
Testing trap
A serializer test that seeds via
upsertRecordcannot catch this. Theremote apply path carries the peer's HLC in the payload, so the filter
appears to work. The test must write through the repository and then assert
the column is populated:
Impact
Multi-device users lose custom maintenance tasks, per item service clocks,
cylinder configurations, and equipment set geofences from incremental
syncs. Split out from #829, whose default-price feature stores data on
service_kindsandservice_schedulesand inherits this gap.