Skip to content

Incremental sync skips five tables whose entities are missing from _hlcTargets #1144

Description

@ericgriffin

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:

  1. Own clock: register in _hlcTargets, as equipmentSets does.
  2. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions