Skip to content

feat(album_artist): server-side schema for album + artist + track_artist (phase 4.d.0.1) - #34

Merged
InstaZDLL merged 2 commits into
mainfrom
feat/4-d-0-1-album-artist-schema
Jun 7, 2026
Merged

feat(album_artist): server-side schema for album + artist + track_artist (phase 4.d.0.1)#34
InstaZDLL merged 2 commits into
mainfrom
feat/4-d-0-1-album-artist-schema

Conversation

@InstaZDLL

@InstaZDLL InstaZDLL commented Jun 7, 2026

Copy link
Copy Markdown
Owner

Summary

First PR of the 4.d.0 sprint chain — adds the entity tables the web client needs for proper album / artist views with drill-down on /profiles/{p}/libraries/{l}. Schema-only; apply pipeline upsert lands in 4.d.0.2, REST endpoints in 4.d.0.4, web views in 4.d.0.5.

Tables

  • artist (id, library_id, name, picture_hash, created_at, updated_at) UNIQUE (library_id, name).
  • album (id, library_id, canonical_title, album_artist_id, year, cover_hash, is_compilation, …) UNIQUE NULLS NOT DISTINCT (library_id, canonical_title, album_artist_id) — PG15+ syntax (we target PG17) so two compilation rows with NULL album_artist_id collapse to one.
  • track_artist (track_id, artist_id, position) PK (track_id, artist_id) with reverse-direction index for the artist drill-down.
  • ALTER TABLE track ADD COLUMN album_id BIGINT REFERENCES album(id) ON DELETE SET NULL + index (album_id, disc_number, track_number).

Design notes

  • Per-library scope (matches track.library_id): same album in two libraries = two rows; dropping a library reclaims its album + artist + track_artist rows in one cascade.
  • Cascade asymmetry is deliberate: join rows (track_artist) cascade both ways; entity rows (album, track) use SET NULL on their FK targets so a stray artist scrub doesn't lose the album row, and a stray album scrub doesn't lose the audio file row.
  • UNIQUE NULLS NOT DISTINCT is the PG15+ trick that lets the apply-time upsert (4.d.0.2) skip the special-case IS NULL match for compilations.

CR pre-push findings applied

  • C1 (blocker): migration timestamp renamed 2026060800000020260608120000 to avoid collision with the already-merged 20260608000000_artwork_repair_backoff.sqlsqlx::migrate! keys on the integer prefix, two files with the same prefix break apply.
  • H1: documented in the migration header that the apply pipeline must order multi-artist rows by (position ASC, artist_id ASC) so read sites stay deterministic on tied positions.
  • M1: added album_library_updated_idx and artist_library_updated_idx for the future GET /libraries/{l}/{albums,artists} sort. Same shape as library_profile_updated_idx / track_library_added_idx.
  • M2: cited the sqlx 0.9 SMALLINT → i64 narrowing rejection in the year column comment.
  • M4: one-line note in the test header explaining the heavyweight spawn_authenticated + REST mint choice (vs direct SQL on profile + library).
  • L5: lifted 1_700_000_000_000 to a const FIXED_NOW_MS.

Skipped: L1 (per-library scope confirmed correct), L2 (PG version non-concern at sqlx layer), L3 (FK asymmetry documented), L4 (apply-pipeline contract, pinned in 4.d.0.2).

Test plan

13 schema-invariant tests in tests/album_artist.rs:

  • Album natural key — NULLS NOT DISTINCT collapse, same title under different artists, compilation + attributed coexistence.
  • Per-library artist name uniqueness — same name in two libraries OK.
  • Cascade chain — library → album / artist / track / track_artist; track → track_artist; album → track.album_id SET NULL; artist → album.album_artist_id SET NULL.
  • track_artist PK prevents duplicate pairing; position preserves multi-artist order under reverse-insert + ORDER BY ASC.
  • CHECK constraints — empty name, empty title, negative position all rejected.

cargo check --all-targets clean locally. Tests run on CI Postgres.

Summary by CodeRabbit

  • New Features

    • Gestion d'artistes et d'albums par bibliothèque.
    • Association multi-artistes par morceau avec ordre configurable.
    • Lien optionnel des morceaux vers un album et prise en charge des compilations.
  • Documentation

    • Ajout d'une documentation décrivant le schéma et les règles de contraintes cross-library.
  • Tests

    • Tests d'intégration complets vérifiant unicités, contraintes, suppressions et comportements multi-library.

…ist (phase 4.d.0.1)

First PR of the 4.d.0 sprint chain — adds the entity tables the
web client needs for proper album / artist views with drill-down
on `/profiles/{p}/libraries/{l}`. Schema-only; apply pipeline
upsert lands in 4.d.0.2, REST endpoints in 4.d.0.4, web views
in 4.d.0.5.

Tables:
- `artist (id, library_id, name, picture_hash, …)` UNIQUE
  (library_id, name).
- `album (id, library_id, canonical_title, album_artist_id,
  year, cover_hash, is_compilation, …)` UNIQUE NULLS NOT
  DISTINCT (library_id, canonical_title, album_artist_id) —
  PG15+ syntax (target PG17) so two compilation rows with NULL
  album_artist_id collapse to one.
- `track_artist (track_id, artist_id, position)` PK (track_id,
  artist_id) with reverse-direction index (artist_id, track_id)
  for the artist drill-down.
- `track.album_id BIGINT REFERENCES album(id) ON DELETE SET
  NULL` + index (album_id, disc_number, track_number).

Per-library scope (matches `track.library_id`): the same album
in two libraries = two rows; dropping a library reclaims its
album + artist + track_artist rows in one cascade. Cascade
asymmetry is deliberate — join rows (`track_artist`) cascade
both ways; entity rows (`album`, `track`) use SET NULL on their
FK targets so a stray artist scrub doesn't lose the album row,
and a stray album scrub doesn't lose the audio file row.

13 schema-invariant tests cover:
- Album natural key (NULLS NOT DISTINCT collapse, same title
  under different artists, compilation + attributed coexistence).
- Per-library artist name uniqueness (same name in two libs OK).
- Cascade chain (library → album / artist / track / track_artist,
  track → track_artist, album → track.album_id SET NULL, artist
  → album.album_artist_id SET NULL).
- track_artist PK + position ordering.
- CHECK constraints (empty name, empty title, negative position).

CR pre-push findings applied:
- C1 (blocker): migration timestamp renamed `20260608000000` →
  `20260608120000` to avoid collision with the already-merged
  `20260608000000_artwork_repair_backoff.sql` — sqlx::migrate!
  keys on the integer prefix, two files with the same prefix
  break apply.
- H1: documented in the migration header that the apply pipeline
  must order multi-artist rows by `(position ASC, artist_id ASC)`
  so read sites stay deterministic on tied positions.
- M1: added `album_library_updated_idx` and
  `artist_library_updated_idx` for the future
  `GET /libraries/{l}/{albums,artists}` sort. Same shape as
  `library_profile_updated_idx` / `track_library_added_idx` — every
  entity table in this repo ships its list-query index alongside
  the table.
- M2: cited the sqlx 0.9 SMALLINT → i64 narrowing rejection in
  the year-column comment (same reason `track.rating` is BIGINT).
- M4: one-line note in the test header explaining the heavyweight
  spawn_authenticated + REST mint choice (vs direct SQL on
  `profile` + `library`).
- L5: lifted `1_700_000_000_000` to a `const FIXED_NOW_MS`.

Skipped:
- L1 (per-library vs per-profile scope): per-library is the right
  call — confirmed by CR.
- L2 (NULLS NOT DISTINCT PG version): non-concern at the sqlx
  layer; PG14- fails loudly at apply.
- L3 (FK asymmetry): correct as-is; documented.
- L4 (position=0 primary-artist invariant): apply-pipeline
  contract, not schema. Pinned in 4.d.0.2 instead.

`cargo check --all-targets` clean.

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

coderabbitai Bot commented Jun 7, 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: 2a0bcca2-c209-4387-b50d-8c983054c90a

📥 Commits

Reviewing files that changed from the base of the PR and between e449ecc and 6902056.

📒 Files selected for processing (3)
  • CLAUDE.md
  • migrations/20260608120000_album_artist.sql
  • tests/album_artist.rs

📝 Walkthrough

Walkthrough

Cette PR introduit le schéma SQL Phase 4.d.0.1 pour artist, album, track_artist et track.album_id, avec FK composites cross-library, règles CASCADE/SET NULL, index de requêtes, documentation associée, et tests d’intégration SQL couvrant les invariants de contraintes.

Changes

Schéma de métadonnées musicales

Layer / File(s) Summary
Schéma artist et album avec contraintes naturelles
CLAUDE.md, migrations/20260608120000_album_artist.sql
Documentation de phase + création de artist et album avec unicités naturelles (UNIQUE, UNIQUE NULLS NOT DISTINCT), validations non-vides, et FK composites avec garde library_id et ON DELETE SET NULL sur album_artist_id.
Table track_artist, index et intégration track.album_id
migrations/20260608120000_album_artist.sql
Ajout de UNIQUE (id, library_id) sur track, création de track_artist (PK, position >= 0, FKs composites ON DELETE CASCADE), ajout de track.album_id + FK composite ON DELETE SET NULL, puis index de lecture (track_artist_artist_idx, album_library_updated_idx, artist_library_updated_idx, track_album_idx).
Tests d’invariants SQL (unicité, suppression, cross-library)
tests/album_artist.rs
Helpers REST/SQL pour fixtures, puis tests couvrant unicité album avec NULLS NOT DISTINCT, unicité artist par bibliothèque, cascades/SET NULL, anti-doublon et ordre track_artist, CHECKs (name, canonical_title, position) et rejets cross-library sur toutes les FK composites.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • InstaZDLL/waveflow-server#9: Ce changement prolonge le modèle track introduit dans ce PR en y rattachant album et la relation N-N track_artist avec contraintes multi-bibliothèques.

Poem

🎵 Trois tables entrent en scène, bien cadrées,
Les clés veillent, les nulls sont domptés,
Cascades et SET NULL gardent l’équilibre,
Les index filent, les tests confirment,
Et la base chante juste, sans dérive.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Le titre décrit précisément le contenu principal : ajout du schéma serveur pour album, artist et track_artist dans la phase 4.d.0.1.
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/4-d-0-1-album-artist-schema

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

@InstaZDLL InstaZDLL self-assigned this Jun 7, 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: 2

🤖 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/20260608120000_album_artist.sql`:
- Around line 73-75: Les FK actuelles autorisent des liaisons
inter-bibliothèques : ajoutez dans la migration des contraintes qui forcent
l’égalité de library_id pour les relations concernées (par ex. remplacer ou
compléter album.album_artist_id REFERENCES artist(id) par une contrainte
composite liant (album.album_artist_id, album.library_id) à (artist.id,
artist.library_id) ou implémenter un trigger/constraint CHECK équivalent ;
faites de même pour track_artist (track_id, artist_id) → track(id,
track.library_id)/artist(id, artist.library_id) et pour track.album_id →
album(id, album.library_id)). Ensuite ajoutez des tests d’intégration négatifs
qui tentent d’insérer des relations cross-library (album→artist, track→album,
track_artist cross-library) et qui doivent échouer.

In `@tests/album_artist.rs`:
- Around line 270-423: Add negative "cross-library" tests in
tests/album_artist.rs that assert schema prevents linking entities across
libraries: create two libraries (via mint_library), then attempt (1) to set
album.album_artist_id to an artist from the other library, (2) to set
track.album_id to an album from the other library, and (3) to insert a
track_artist row pairing a track and artist from different libraries; for each
case use the existing helpers (insert_artist, insert_album, insert_track) and
perform the offending SQL (UPDATE or INSERT) expecting a database error
(constraint violation) rather than success, using async test functions named
e.g. album_artist_cross_library_constraint,
track_cross_library_album_constraint, and track_artist_cross_library_constraint
to lock the invariant against regressions.
🪄 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: 8e3e635c-1341-4561-a76f-5a1f8e5c78e8

📥 Commits

Reviewing files that changed from the base of the PR and between 7cbca9b and e449ecc.

📒 Files selected for processing (3)
  • CLAUDE.md
  • migrations/20260608120000_album_artist.sql
  • tests/album_artist.rs

Comment thread migrations/20260608120000_album_artist.sql Outdated
Comment thread tests/album_artist.rs
…argo fmt

CR finding (real): the original migration's single-column FKs let
`album.album_artist_id`, `track.album_id`, and `track_artist`
pairs link entities across libraries — the per-library cascade
chain assumes intra-library locality, so a stray cross-library
link would leak rows on a library delete.

Fix: composite FKs that carry `library_id` in BOTH columns, with
the parent table's `UNIQUE (id, library_id)` as the FK target. A
try to set `album.album_artist_id` to an artist in a different
library can't satisfy the composite FK; same for `track.album_id`
and `track_artist (track_id, artist_id)`.

Schema changes:
- `track` (existing) gets `UNIQUE (id, library_id)` so dependent
  tables can use it as a composite FK target.
- `artist` and `album` each get `UNIQUE (id, library_id)` for the
  same reason.
- `album.album_artist_id` → composite FK with `ON DELETE SET NULL
  (album_artist_id)` (PG15+ column-level form) so the album's
  library_id stays intact when its artist is scrubbed.
- `track.album_id` → composite FK with `ON DELETE SET NULL
  (album_id)`.
- `track_artist` gains a `library_id BIGINT NOT NULL` column with
  composite FKs to BOTH `track(id, library_id)` and
  `artist(id, library_id)` — the shared `library_id` column means
  the only way to insert a row is for track AND artist to live in
  the referenced library. The apply pipeline (4.d.0.2) derives
  this value from `track.library_id` at upsert.

Test surface:
- 3 new cross-library tests:
  - `album_artist_cross_library_constraint` — album in lib_a
    linking to artist in lib_b is rejected.
  - `track_cross_library_album_constraint` — track in lib_b
    linking to album in lib_a is rejected.
  - `track_artist_cross_library_constraint` — pairing a track in
    lib_a with an artist in lib_b fails BOTH possible
    `library_id` values (whichever we pass, one composite FK
    rejects the row).
- New `insert_track_artist` / `try_insert_track_artist` helpers
  (track_artist now carries library_id, so all 5 existing
  callsites had to update — helper saves the repetition).
- New `try_insert_track` variant for the cross-library track
  test (the existing `insert_track` panics on error).

CI lint:
- `cargo fmt --all` applied — addresses the rustfmt CI failure
  on the original PR.

CLAUDE.md updated to document the cross-library guard pattern
for future entity additions.

`cargo check --all-targets` + `cargo fmt --all --check` both
clean locally.

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

Copy link
Copy Markdown
Owner Author

@coderabbitai CI fixes + cross-library guards pushed (commit 6902056).

Findings appliqués :

  1. CI cargo fmt --all --check fail ✅ Fixed. Ran cargo fmt --all on the whole crate. cargo fmt --all --check is now clean locally.

  2. Cross-library FK invariant (inline finding on migration + tests) ✅ Fixed. The original single-column FKs let album.album_artist_id, track.album_id, and track_artist pairs link entities across libraries — the per-library cascade chain assumes intra-library locality, so a stray cross-link would leak rows on a library delete.

    Solution: composite FKs that carry library_id in BOTH columns, with the parent table's UNIQUE (id, library_id) as the target. PG15+ column-level ON DELETE SET NULL (col) drops only the FK link, leaving library_id intact on the dependent row.

    • track (existing), artist, album all get UNIQUE (id, library_id).
    • album.album_artist_id → composite FK with ON DELETE SET NULL (album_artist_id).
    • track.album_id → composite FK with ON DELETE SET NULL (album_id).
    • track_artist gains a denormalised library_id BIGINT NOT NULL column with composite FKs to both track(id, library_id) and artist(id, library_id) — the shared library_id column means the only way to insert a row is for track AND artist to live in the referenced library.

    3 new tests added per your spec:

    • album_artist_cross_library_constraint — album in lib_a linking to artist in lib_b → rejected.
    • track_cross_library_album_constraint — track in lib_b linking to album in lib_a → rejected.
    • track_artist_cross_library_constraint — pairing a track in lib_a with an artist in lib_b fails BOTH possible library_id values.

    New helpers insert_track_artist / try_insert_track_artist / try_insert_track introduced — track_artist now carries library_id, so all 5 existing callsites use the helper (saves the repetition).

CLAUDE.md updated to document the cross-library guard pattern for future entity additions.

cargo check --all-targets + cargo fmt --all --check both clean.

@coderabbitai

coderabbitai Bot commented Jun 7, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Fetch the diff of commit 6902056
gh api repos/InstaZDLL/waveflow-server/commits/6902056 --jq '.files[] | "\(.filename)\n---\n\(.patch)\n==="'

Length of output: 31756



@InstaZDLL Tout est vérifié — le diff est propre.

Migration ✅

  • UNIQUE (id, library_id) ajouté sur track, artist, album comme cibles des FK composites — correct.
  • FOREIGN KEY (album_artist_id, library_id) REFERENCES artist (id, library_id) ON DELETE SET NULL (album_artist_id) — syntaxe PG15+ column-level SET NULL correcte ; album.library_id reste intact.
  • Même pattern sur track.album_idalbum (id, library_id).
  • track_artist.library_id dénormalisé + double FK composite vers track et artist — la contrainte est élégante : aucune valeur de library_id ne peut satisfaire les deux FKs pour une paire inter-bibliothèque.
  • NULL album_artist_id contourne la FK composite (comportement PostgreSQL standard), la contrainte UNIQUE NULLS NOT DISTINCT des compilations reste intact.

Tests ✅

  • album_artist_cross_library_constraint / track_cross_library_album_constraint / track_artist_cross_library_constraint couvrent les trois vecteurs de cross-link.
  • Le test track_artist teste bien les deux valeurs possibles de library_id (lib_a et lib_b), ce qui est exhaustif.
  • Refactoring des 5 callsites vers insert_track_artist — pas de dérive logique.

Rien à redire, l'implémentation est solide. Je mets à jour la mémoire avec le pattern.

🐇 ✨

[approve]


✏️ Learnings added
Learnt from: InstaZDLL
URL: https://github.com/InstaZDLL/waveflow-server/pull/34

Timestamp: 2026-06-07T22:50:38.169Z
Learning: Dans waveflow-server (migrations/20260608120000_album_artist.sql + tests/album_artist.rs), la contrainte cross-library est enforced au niveau schéma via des FK composites : chaque lien entité-à-entité porte `library_id` dans LES DEUX colonnes de la FK, avec `UNIQUE (id, library_id)` sur la table parente comme cible. Détails :
- `track`, `artist`, `album` ont tous `UNIQUE (id, library_id)`.
- `album.album_artist_id` → `FOREIGN KEY (album_artist_id, library_id) REFERENCES artist (id, library_id) ON DELETE SET NULL (album_artist_id)` (PG15+ column-level SET NULL).
- `track.album_id` → `FOREIGN KEY (album_id, library_id) REFERENCES album (id, library_id) ON DELETE SET NULL (album_id)`.
- `track_artist` porte une colonne dénormalisée `library_id BIGINT NOT NULL` + deux FK composites vers `track(id, library_id)` et `artist(id, library_id)` ON DELETE CASCADE — la même colonne `library_id` partagée rend impossible l'insertion d'une paire inter-bibliothèque.
- Cible PG17 (même gate que `UNIQUE NULLS NOT DISTINCT`). Le apply pipeline (4.d.0.2) dérive `track_artist.library_id` de `track.library_id` à l'upsert.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: InstaZDLL
Repo: InstaZDLL/waveflow-server PR: 0
File: :0-0
Timestamp: 2026-06-06T19:46:50.757Z
Learning: Dans `src/artwork_jobs.rs` et la migration `20260608000000_artwork_repair_backoff.sql` (waveflow-server), la colonne `metadata_artwork.last_repair_failure_at` est `BIGINT` epoch-millis (pas `TIMESTAMPTZ`), conforme au contrat CLAUDE.md "epoch-millis BIGINT for timestamps" + compatibilité SQLite mirror. `list_partial_parents` prend `backoff_cutoff_ms: i64`. `mark_repair_failure` prend `now_ms: i64` (caller-minted, clock DB découpée). `run_once` mint `chrono::Utc::now().timestamp_millis()` une fois par cycle et utilise `saturating_sub` pour le cutoff. `REPAIR_BACKOFF = 3600s` → 3_600_000 ms.

Learnt from: CR
Repo: InstaZDLL/waveflow-server PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-06-07T21:44:04.584Z
Learning: Applies to src/db.rs : Playlist track materialisation: Apply pipeline writes of `playlist + field: "tracks"` ops into a dedicated `playlist_track` table. Store `(playlist_id, track_id)` PK with position index and BIGINT epoch-millis `added_at`. Include snapshot columns (`snapshot_title`, `snapshot_artist`, `snapshot_duration_ms`) for cross-device display. Pre-1.j.b desktops emit ops without `snapshots` — rows land with NULL snapshot fields and stay invisible in public preview (`fetch_for_share` filters `snapshot_title IS NOT NULL`).

@InstaZDLL
InstaZDLL merged commit 02a371f into main Jun 7, 2026
8 checks passed
@InstaZDLL
InstaZDLL deleted the feat/4-d-0-1-album-artist-schema branch June 7, 2026 22:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant