feat(track-sync): apply pipeline for the track entity (phase 4.d.0.2) - #35
Conversation
Closes the Phase 1.g.0 deferred TODO that left `track` out of the apply pipeline (see `20260604000000_apply_pipeline.sql:60-67`). The desktop can now sync tracks + their album / artist linkage through `/api/v1/sync/ops`; the apply path materialises the 4.d.0.1 entity tables in one atomic transaction per op. == Wire shape == - `entity: "track"`, `entity_id: <file_path>` — the per-library natural identity (`UNIQUE (library_id, file_path)` from `20260530000003_track.sql:64`). - `payload.library_canonical_id` carries the tenant scope. - `payload.file_hash` rides as a payload field (BLAKE3 hex). It joins to `liked_track` / `track_rating` via the existing per-user `(user_id, file_hash)` PKs but is NOT the row identity for the `track` entity itself — the desktop's tag-editor rewrites embedded metadata frames so file_hash changes while file_path doesn't. - INSERT payload also packs the full track metadata + the album / artist plumbing: `album_title?`, `album_artist_name?` (None → compilation), `is_compilation?`, `artists?: [String, ...]` (the desktop's `;`-split list — position derives from array index). == Handler chain == 1. Resolve library_id from `library_canonical_id` (Skipped if not yet materialised — the op stays in the durable log). 2. Upsert every contributor artist on `(library_id, name)`. 3. Resolve album_artist_id by name (dedups against contributors). 4. Upsert album on the 4.d.0.1 natural key `(library_id, canonical_title, album_artist_id)`. NULLS NOT DISTINCT so the compilation case collapses to one row. 5. Upsert track on `(library_id, file_path)`. Every scalar column overwrites on conflict — a tag-edit re-emit lands as an in-place UPDATE (file_hash + title + audio specs). 6. DELETE-then-INSERT the multi-artist links via a single UNNEST-driven INSERT (avoids N+1 round-trips). == DELETE / SET == - DELETE keyed on `(library_id, file_path)`. The composite FK from `track_artist` cascades the link rows automatically; `track.album_id` SET NULL via the 4.d.0.1 schema. - SET ops are Unknown. The desktop's tag-editor save re-emits a full INSERT (the upsert handles the merge); SET would be unreachable. Surfaced as Unknown rather than Skipped so a future protocol extension is visible in telemetry. == Empty-string guards == `album_title` / `album_artist_name` of `""` are rejected at the apply boundary as InvalidPayload (400-family) rather than falling through to the `length(...) > 0` CHECK constraints (which would 500). Symmetric with the existing `artists[]` empty-string rejection. == CR pre-push findings (round 1, all applied) == - H1: tag-edit re-emit would 500 because `ON CONFLICT (library_id, file_hash)` falls through to INSERT (file_hash changes on tag edit) and trips the pre-existing `(library_id, file_path)` UNIQUE. Fix: swap the upsert key to `(library_id, file_path)` (the existing natural key) and move `file_hash` into the payload. Wire shape change: `entity_id` is now the file_path, not the file_hash. Tests cover the exact scenario (`track_re_emit_after_tag_edit_updates_in_place`). The deferred `20260609120000_track_sync.sql` migration is dropped — no schema change is needed since the existing `(library_id, file_path)` UNIQUE is the upsert key. - H2: empty-string `album_title` / `album_artist_name` would surface as 500. Added the `reject_empty` helper + new test `track_insert_rejects_empty_album_title`. - M1: added `track_insert_cross_tenant_library_canonical_skips` — Bob pushing a track op naming Alice's library_canonical_id must Skip (per-profile scope on `lookup_library_id`). - M2: added `track_insert_set_op_is_unknown` to pin the Unknown contract for SET ops. - M3: `replace_track_artists` now uses a single UNNEST-driven INSERT instead of N+1 round-trips. - L3: doc-comment about the tag-edit hashing behaviour rewritten to match reality. Skipped: M4 (skip-when-unchanged) + M5 (updated_at chattiness) are perf optimisations worth measuring before adding. L1 (O(n) album_artist dedup) is acceptable at realistic artist counts. L2 (auto-provisioned profile on a Skipped op) matches the existing playlist / library behaviour. L4 / L5 are documentation polish. `cargo check --all-targets` + `cargo fmt --all --check` clean locally. 12 new tests in `tests/apply.rs`. Signed-off-by: InstaZDLL <github.105mh@8shield.net>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughSynchronisation des entités track via apply operations: identification par (library_id, file_path), file_hash comme payload de déduplication inter-appareils, validation de métadonnées album/artistes, upserts séquentiels de contributeurs/album/track, remplacement des liens multi-artistes, isolation multi-tenant par library_canonical_id, et comportement Skipped pour librairies non encore matérialisées. ChangesTrack Synchronization
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 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 `@src/apply.rs`:
- Around line 1130-1161: The function artists_from_payload currently returns
duplicates which later cause PK conflicts in
upsert_artist/replace_track_artists; change artists_from_payload to deduplicate
artists while preserving their first-seen order (e.g., iterate arr, convert
entries to strings as currently done, but track seen names with a HashSet and
only push the first occurrence into out) and return that deduplicated
Vec<String>; alternatively, if you prefer strict validation, detect duplicates
and return ApplyError::InvalidPayload noting duplicate artist entries instead of
deduplicating—implement the chosen behavior inside artists_from_payload so
upsert_artist and replace_track_artists no longer receive duplicate artist_ids.
In `@src/db.rs`:
- Around line 818-833: L'UPSERT ON CONFLICT dans la requête SQL ne met pas à
jour la colonne added_at sur conflit, donc les re-emit n'actualisent pas le tri
"Recently added"; modify the ON CONFLICT ... DO UPDATE clause in the SQL string
found in src/db.rs (the UPSERT that currently sets title, file_hash, ...,
album_id) to also include added_at = EXCLUDED.added_at so that added_at is
overwritten on conflict (ensure the same SQL string/variable that contains the
RETURNING id is updated).
In `@tests/apply.rs`:
- Around line 1175-1216: The test
track_insert_with_unknown_library_skips_but_logs currently only asserts the
track row was not materialised; also assert the operation was persisted in the
durable log (sync_op) so replay can later succeed: capture the op UUID before
calling push, then after push query the sync_op table for a row with that op id
(or matching target = 'track' and file_path) and assert it exists (and
optionally has a status/mark indicating Skipped). Update the test to store the
generated Uuid, run a SELECT against sync_op to fetch/count that entry, and
assert the result to ensure the op was logged.
🪄 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: 09f212d0-7525-4ad8-b723-97b154928606
📒 Files selected for processing (4)
CLAUDE.mdsrc/apply.rssrc/db.rstests/apply.rs
…_scalar fix CI failure: `track_insert_creates_track_album_and_artists` panic'd on a `(i64,)` decode mismatch. The query used `query_scalar::<_, (i64,)>` which tells sqlx to decode a single column AS a tuple — the column is INT8, not a record, so the decode trips. `query_scalar` returns a scalar by definition; the tuple is the wrong type-witness. CR findings (round 1): - **Artist dedup**: `artists_from_payload` returned duplicates verbatim. A desktop that ships `["A", "A"]` (e.g., a tag with a duplicated artist after the `";"` split) would have `upsert_artist` return the same id twice → `replace_track_artists` tripping the `(track_id, artist_id)` PK on the second INSERT. Fix: HashSet-based first-seen-order dedup inside the parser. New regression test `track_insert_dedups_duplicate_artists` pins the contract — duplicates collapse to first-seen, others are dropped silently. - **sync_op durability assertion**: the Skipped-path test asserted only "no track row materialised". Added a SELECT COUNT on `sync_op WHERE user_id + operation_id + entity_id` so a future refactor that drops the op on the floor breaks the test before it breaks the Skipped-then-replay contract. Skipped: - **`added_at = EXCLUDED.added_at` on UPSERT conflict** (CR finding B): the column's documented semantic is "Epoch milliseconds for the original library import. Drives the Recently added sort" (`20260530000003_track.sql:47-50`). A tag-edit re-emit doesn't change WHEN the file first landed, so the existing value MUST be preserved on conflict. Overwriting would bump the row to "just added" on every re-emit, which is the opposite of the column's purpose. `cargo check --all-targets` + `cargo fmt --all --check` clean. Signed-off-by: InstaZDLL <github.105mh@8shield.net>
|
@coderabbitai CI fix + round 1 findings pushed (commit 8418029). CI failure fixed: Findings applied:
Finding skipped:
`cargo check --all-targets` + `cargo fmt --all --check` clean locally. |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Check artists_from_payload dedup implementation
rg -n "artists_from_payload" --type rust -A 20
# Check upsert_track ON CONFLICT clause for added_at
rg -n "added_at" src/db.rs -A 2 -B 2
# Check the new regression test
rg -n "track_insert_dedups_duplicate_artists" tests/apply.rs -A 40
# Check sync_op durability assertion in Skipped-path test
rg -n "sync_op" tests/apply.rs -A 5 -B 2Length of output: 9445 🏁 Script executed: #!/bin/bash
# Check full ON CONFLICT clause for upsert_track
rg -n "ON CONFLICT" src/db.rs -A 30 -B 2
# Also check the position field handling in artists_from_payload after dedup
rg -n "artists_from_payload" src/apply.rs -A 40 | tail -30Length of output: 19672
CI fix ✅ Finding 1 — Artist dedup ✅ Implémentation solide : Finding 2 — sync_op durability ✅ L'assertion est précise — Finding 3 — Une observation mineure : 🐇 ✅ 🧠 Learnings used |
Summary
Second PR of the 4.d.0 sprint chain — closes the Phase 1.g.0 deferred TODO that left
trackout of the apply pipeline (see20260604000000_apply_pipeline.sql:60-67). The desktop can now sync tracks + their album / artist linkage through `/api/v1/sync/ops`; the apply path materialises the 4.d.0.1 entity tables in one atomic transaction per op.Wire shape
Handler chain
DELETE is keyed on `(library_id, file_path)` and cascades into `track_artist`. SET ops are intentionally Unknown — the desktop re-emits a full INSERT on tag edit and the upsert merges.
CR pre-push findings applied
Skipped: M4 / M5 (perf optimisations to measure first), L1 (acceptable at realistic artist counts), L2 (matches existing playlist / library behaviour), L4 / L5 (documentation polish).
Test plan
12 new tests in `tests/apply.rs`:
`cargo check --all-targets` + `cargo fmt --all --check` clean locally. Tests run on CI Postgres.
Summary by CodeRabbit
New Features
Bug Fixes / Comportements
Documentation
Tests