Skip to content

[Sync] 3 - Add sync_seq to six tables #281

Description

@KacperKozak

Parent

What to build

Give the six remaining mutable tables a Sync Cursor column, so an upload scan can ask each one "what changed since I last looked".

app_settings      board_settings      board_warnings
privacy_zones     tune_profiles       favorites

boards, alerts and telemetry_minute_buckets already have sync_seq and updated_at from earlier on this branch. #287 lands Favorites first with stable identity and ordinary timestamps but deliberately no sync dependency. This slice adds/backfills its sync_seq, registers favorites with the shared sequence, and switches its create/rename paths to the same ratcheted write mechanism. This extends the same mechanism — the shared sequence table and the bump-then-read primitive already in the DAO — to the rest, and nothing about the design is new. vescape-server ADR-0007 records why the cursor is a counter and not a clock.

The append-only tables need nothing: telemetry frames, markers, diagnostic events, metric exclusion ranges and tune history entries all declare INTEGER PRIMARY KEY AUTOINCREMENT on both platforms, which SQLite guarantees monotonic and never reused, so their existing key is their cursor. Do not add sync_seq to them.

Two corrections to existing behaviour belong here, since they are the same mechanism:

Minute buckets adopt the ratchet. The bucket merge currently clamps its Change Timestamp with max(existing, incoming) rather than max(previous + 1, now), on the stated premise that the server upserts that table unconditionally. It does not — the server guards it with WHERE stored.updated_at < EXCLUDED.updated_at, the same rule as every other mutable table. On a backwards clock step the row is scanned, sent, and silently dropped server-side. Use the same ratchet as boards and alerts, and delete the comment claiming the exception.

Decide which app settings are per-Account and which are per-phone. This is the HITL part and it must be settled before the table's sync shape is fixed. app_settings currently syncs wholesale, but Rider Name and Rider Color live in it by design so that Group Ride keeps working signed-out. Restoring those onto a second phone overwrites that phone's Rider identity. Other keys may be equally phone-shaped — connection preferences, debug toggles, anything naming this device rather than this Rider. The outcome is either a per-key sync flag or an explicit not-synced list, plus a decision recorded on #277 about which keys fall on which side.

Likely files

  • modules/vescape-core/android/src/main/java/expo/modules/vescapecore/telemetry/TelemetryEntities.kt - the six entities and SYNC_SEQ_TABLES
  • modules/vescape-core/android/src/main/java/expo/modules/vescapecore/telemetry/TelemetryDao.kt - nextSyncSeq, ratchetUpdatedAt, the bucket merge, and the seven upsert paths
  • modules/vescape-core/android/src/main/java/expo/modules/vescapecore/telemetry/TelemetryDatabase.kt - Room version and the migration
  • modules/vescape-core/ios/telemetry/TelemetryDatabase.swift and TelemetryDao.swift - the GRDB peers
  • modules/vescape-core/android/src/test/java/expo/modules/vescapecore/telemetry/SyncCursorMigrationTest.kt - the migration test to extend
  • src/modules/settings/store/ - the JS side of app settings, for the per-key decision
  • modules/vescape-core/src/index.ts - setting read/write surface across the bridge

Implementation hints

nextSyncSeq(name) seeds, bumps, then reads, inside the caller's transaction — bump-then-read specifically so two writes racing in the same database cannot be handed the same number. Reuse it; do not derive a sequence as MAX(sync_seq) + 1 per table, which hands out a duplicate as soon as the highest row is deleted.

ratchetUpdatedAt(previous, now) is max(previous + 1, now) and already carries a comment explaining that a plain max satisfies the scan while still losing the edit server-side. That comment is the argument for changing the bucket merge.

Each of the six tables needs its sequence name added to SYNC_SEQ_TABLES, which the migration iterates. Fresh installs build the schema from the entities and never run the migration, which is why nextSyncSeq seeds its row first — keep that.

setAlertRuleEnabled was the regression that motivated the original work: a targeted UPDATE that bypassed the row rewrite and so never moved the change columns. Check the six tables for the same shape — any targeted UPDATE that skips the upsert path.

Acceptance criteria

  • The six tables carry sync_seq and a Change Timestamp, stamped from the native clock on every write
  • No append-only table gains a sync_seq
  • Every write path for those tables moves both columns, including any targeted UPDATE that bypasses an upsert
  • Minute bucket merges ratchet their Change Timestamp like boards and alerts, and the comment asserting the server upserts unconditionally is gone
  • Migration backfills existing rows rather than leaving them at a default, on both platforms
  • Existing Favorite rows receive distinct non-zero cursor values; future create/rename writes advance sync_seq and ratcheted updated_at
  • Migration is a no-op on re-run, with tests extending the existing migration tests
  • JS input types cannot fabricate either column, matching how BoardInput and AlertRuleInput already work
  • A decision is recorded on [PRD][Sync] Ride history backup — app side #277 naming which app_settings keys sync and which stay on the phone, and the code enforces it

Blocked by

The app settings decision on #277 must also be settled before the app_settings half is implemented.

Related

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:dbTouches database / persistent storagearea:nativeTouches native side (modules/vesc-ble, Swift/Kotlin)area:syncBackup sync — native uploader, Sync Cursors, Sync Actions, Device Tokencomplexity:mediumNeeds care, moderate integration surface. Use sonnet.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions