Skip to content

feat(sync): rfc-003 phase a.1 — additive hlc fields on sync_op - #50

Merged
InstaZDLL merged 2 commits into
mainfrom
feat/rfc-003-a-1-sync-op-hlc
Jun 13, 2026
Merged

feat(sync): rfc-003 phase a.1 — additive hlc fields on sync_op#50
InstaZDLL merged 2 commits into
mainfrom
feat/rfc-003-a-1-sync-op-hlc

Conversation

@InstaZDLL

@InstaZDLL InstaZDLL commented Jun 13, 2026

Copy link
Copy Markdown
Owner

What

First slice of RFC-003 Phase A — extend `sync_op` with the HLC pair (`hlc_wall`, `hlc_logical`) the §2 total order is defined on, so the wire shape can carry the new ordering primitive while every other moving part stays untouched. No semantic change yet — this PR is wire-shape additive only.

Migration

migrations/20260612000000_sync_op_hlc.sql:

ALTER TABLE sync_op
    ADD COLUMN hlc_wall    BIGINT,
    ADD COLUMN hlc_logical INTEGER;

UPDATE sync_op
   SET hlc_wall    = 0,
       hlc_logical = lamport_ts
 WHERE hlc_wall    IS NULL;

ALTER TABLE sync_op
    ALTER COLUMN hlc_wall    SET NOT NULL,
    ALTER COLUMN hlc_logical SET NOT NULL;

ALTER TABLE sync_op
    ADD CONSTRAINT sync_op_user_device_hlc_uniq
        UNIQUE (user_id, device_id, hlc_wall, hlc_logical);

Backfill rule matches Phase A in the RFC: treat the legacy counter as the logical component, set wall = 0. That keeps the new unique invariant satisfied without needing a separate `WHERE` clause, and means any v2 op (with `hlc_wall > 0`) strictly outranks every legacy-shape row under the §2 total order — the intended LWW behaviour once A.2 lands.

The legacy `UNIQUE (user_id, device_id, lamport_ts)` stays in place. A v1 desktop pushing a stale-lamport replay still 23505s the same way it does today; v2 pushes 23505 against the new HLC constraint.

Code change

A single touchpoint in `src/db.rs` — `insert_op_returning` now projects `(hlc_wall = 0, hlc_logical = lamport_ts)` directly in the SQL VALUES clause so the new NOT NULL columns are satisfied without a caller-visible signature change. When A.2 lands and clients start emitting their own HLC, the signature gains an optional `hlc` parameter and the projection flips to use the client-provided values when present.

What this does NOT do

  • No entity-table changes. `profile`, `library`, `track`, `playlist`, `liked_track`, `track_rating` keep their current shape. A.1.2 adds `hlc_wall`, `hlc_logical`, `origin_device_id UUID` to each.
  • No apply-pipeline semantics change. Still LWW-blanket, still pick latest `(user_id, device_id, lamport_ts)` on conflict. The new HLC fields ride along untouched in the row, available for A.2 to consume.
  • No wire shape v2 yet. Clients keep sending v1 ops with only `lamport_ts`; the server derives the HLC pair on write. A.2 adds the dual-shape ingestion (accept both v1 and v2 from clients, prefer v2 when present).

The point of slicing Phase A into A.1 → A.2 → A.3 → A.4 is exactly to keep each PR narrow enough that a rollback never has to unwind both schema and code at once.

Test plan

  • `cargo check --all-targets --all-features` passes.
  • `cargo clippy --all-targets --all-features -- -D warnings` clean.
  • `cargo test --all-features` 32 / 32 passes (sync + apply + share + artwork test suites all green against the new schema).
  • Migration dry-runs cleanly in a transaction against a live post-1.5.0 schema (one legacy `sync_op` row from a previous QA session backfilled successfully — `UPDATE 1`).

Refs

Summary by CodeRabbit

  • Chores
    • Mise à jour du schéma de base de données pour supporter la nouvelle forme HLC v2 et renforcer les contraintes d’intégrité.
  • Corrections
    • Ajout de vérifications et d’un backfill sécurisé pour éviter toute perte ou troncature silencieuse des horodatages lors de la migration.

First step of the RFC-003 Phase A migration: extend sync_op with
the HLC pair (`hlc_wall`, `hlc_logical`) that §2's total order is
defined on. Pre-existing rows backfill from `lamport_ts` per the
RFC's Phase A rule — treat the legacy counter as the logical
component, set wall = 0. The legacy
`UNIQUE (user_id, device_id, lamport_ts)` stays in place; a new
`UNIQUE (user_id, device_id, hlc_wall, hlc_logical)` enforces the
same invariant on v2 wire-shape pushes.

The `db::insert_op_returning` callsite is updated to write the
derived HLC pair on every insert so the new NOT NULL columns are
satisfied without a wire-shape change yet — the caller still passes
only `lamport_ts`, and the SQL VALUES expression projects
`(hlc_wall = 0, hlc_logical = lamport_ts)` to match the migration
backfill. When A.2 lands and clients start emitting their own HLC,
the signature gains an optional `hlc` parameter and the projection
flips to use the client-provided values when present.

The §2 total order's `origin_device_id` tiebreaker is the existing
`sync_op.device_id` column (TEXT — unchanged at this layer). The
stricter `origin_device_id UUID` typing lands in A.1.2 on the
entity tables (`profile`, `library`, `track`, `playlist`,
`liked_track`, `track_rating`) where the apply pipeline
materialises the post-conflict-resolution state.

`cargo check --all-targets --all-features` passes.
`cargo test --all-features` 32 / 32 passes (sync + apply + share +
artwork test suites all green against the new schema; the existing
INSERT path automatically lands the derived HLC pair).
`cargo clippy --all-targets --all-features -- -D warnings` clean.

Migration dry-runs cleanly in a transaction against a live
post-1.5.0 schema (one legacy `sync_op` row from a previous QA
session backfilled successfully).

Refs RFC-003 (PR #235 on InstaZDLL/WaveFlow).

Signed-off-by: InstaZDLL <github.105mh@8shield.net>
@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c19b6c4e-7cf1-4a59-8e47-e8cca0f7282a

📥 Commits

Reviewing files that changed from the base of the PR and between a89ab60 and 67587d9.

📒 Files selected for processing (2)
  • migrations/20260612000000_sync_op_hlc.sql
  • src/db.rs

📝 Walkthrough

Walkthrough

Cette PR ajoute les colonnes HLC (hlc_wall, hlc_logical) au schéma sync_op via une migration SQL (prévalidation, backfill, NOT NULL, contrainte d’unicité) et met à jour sync::insert_op_returning pour dériver et insérer ces champs depuis lamport_ts.

Changes

Extension du schéma HLC pour sync_op

Layer / File(s) Summary
Migration du schéma sync_op avec HLC
migrations/20260612000000_sync_op_hlc.sql
Ajout des colonnes hlc_wall (BIGINT) et hlc_logical (INTEGER), prévalidation PL/pgSQL pour détecter lamport_ts hors plage i32, backfill des enregistrements (hlc_wall=0, hlc_logical=lamport_ts::INTEGER), SET NOT NULL, et ajout de la contrainte d'unicité sync_op_user_device_hlc_uniq sur (user_id, device_id, hlc_wall, hlc_logical).
Insertion avec dérivation HLC dans Rust
src/db.rs
sync::insert_op_returning valide que lamport_ts tient dans un i32, dérive hlc_logical = lamport_ts as i32, ajoute hlc_wall/hlc_logical (avec hlc_wall=0) aux bindings et à l'INSERT ON CONFLICT ... DO NOTHING RETURNING.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • InstaZDLL/waveflow-server#19: PR antérieur touchant la table sync_op et la logique d'insertion/idempotence basée sur lamport_ts, lié conceptuellement aux changements de schéma et d'insertion ici.

Poem

🕐 HLC pousse ses bornes en silence,
Lamport cède sa place à une danse,
murs à zéro, logiques alignés,
la migration remplit l'historique planifié.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed Le titre décrit précisément le changement principal : ajout des champs HLC à sync_op pour la phase A.1 du RFC-003.
Description check ✅ Passed La description couvre tous les éléments clés : le contexte RFC, la migration SQL, les changements de code, les tests et les limitations de scope. Elle est structurée et détaillée.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/rfc-003-a-1-sync-op-hlc

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added scope: server Server core (Rust) scope: db SQLite schema, migrations, queries type: feat New feature size: s 10-50 lines and removed type: feat New feature labels Jun 13, 2026
@InstaZDLL InstaZDLL self-assigned this Jun 13, 2026
@github-actions github-actions Bot added type: feat New feature and removed type: feat New feature labels Jun 13, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@migrations/20260612000000_sync_op_hlc.sql`:
- Line 35: La colonne hlc_logical est INTEGER mais insert_op_returning
(src/db.rs) réutilise lamport_ts:i64 sans valider la plage 32-bit, ce qui peut
provoquer "integer out of range": dans src/db.rs (lines 123-123) ajoute un
contrôle explicite dans la fonction insert_op_returning pour vérifier que
lamport_ts est dans i32::MIN..=i32::MAX et retourner une erreur claire si hors
plage (ou clamp/convertir explicitement selon la politique), puis continue
d'utiliser la valeur validée/castée pour $4; pour
migrations/20260612000000_sync_op_hlc.sql (lines 35-35) ne change rien si vous
choisissez la validation Rust, sinon si vous voulez supporter des timestamps
>32-bit modifiez la colonne hlc_logical INTEGER → BIGINT et mettez à jour le
backfill SQL en conséquence.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: cf32bbb0-1474-4bb9-9ce6-f65acc9ef30e

📥 Commits

Reviewing files that changed from the base of the PR and between 112974a and a89ab60.

📒 Files selected for processing (2)
  • migrations/20260612000000_sync_op_hlc.sql
  • src/db.rs

Comment thread migrations/20260612000000_sync_op_hlc.sql
Review finding on PR #50: `hlc_logical` is INTEGER (i32) per RFC-003
§2 but the migration backfill and the Rust INSERT both bound a raw
i64 `lamport_ts` against it. Postgres would have raised SQLSTATE
22003 ("integer out of range") on any value > 2^31-1 — practically
impossible in any current install (the legacy v1 Lamport counter is
in the low thousands at most), but the silent-truncation risk on
the SQL-side cast and the bare Postgres error on the Rust-side
bind were both worth closing.

Two changes, one PR:

1. Migration preflight + explicit cast. A DO block aborts the
   migration loudly if any sync_op.lamport_ts is outside
   [0, 2^31-1] before the UPDATE runs; the UPDATE itself now does
   an explicit `lamport_ts::INTEGER` cast so the contract between
   the wide source column and the narrow target column is visible
   in the SQL, not inferred.

2. Rust-side validation in `db::insert_op_returning`. A range
   check returns `sqlx::Error::Protocol` with a clear actionable
   message before binding, instead of letting Postgres surface
   bare 22003. The bound value is then narrowed to i32 explicitly
   (`lamport_ts as i32`) so the bind site type-matches the
   column.

`cargo check --all-targets --all-features` clean.
`cargo test --all-features` 32 / 32 green against a fresh DB
running the new migration shape.

Refs PR #50 review.

Signed-off-by: InstaZDLL <github.105mh@8shield.net>
@github-actions github-actions Bot added type: feat New feature size: m 50-200 lines and removed type: feat New feature size: s 10-50 lines labels Jun 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: db SQLite schema, migrations, queries scope: server Server core (Rust) size: m 50-200 lines type: feat New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant