Skip to content

feat(core): tenant-scoped PostgresPlaylistRepository (Phase 1.b.5c)#188

Merged
InstaZDLL merged 2 commits into
mainfrom
feat/postgres-playlist-tenant-methods
May 30, 2026
Merged

feat(core): tenant-scoped PostgresPlaylistRepository (Phase 1.b.5c)#188
InstaZDLL merged 2 commits into
mainfrom
feat/postgres-playlist-tenant-methods

Conversation

@InstaZDLL
Copy link
Copy Markdown
Owner

@InstaZDLL InstaZDLL commented May 30, 2026

Summary

Final tier of Phase 1.b.5. A playlist belongs directly to a profile (not nested under library), so the ownership chain is the shorter playlist β†’ profile β†’ user β€” same depth as library, different parent. Same design as PostgresLibraryRepository (#185) and PostgresTrackRepository (#186): inherent methods only, no PlaylistRepository trait impl on this backend (tenancy would leak through a trait dispatch).

Changes

  • domain/playlist.rs β€” New profile_id: i64 field with #[sqlx(default)] for cross-backend compatibility. Desktop call site in commands/playlist.rs::create_playlist initializes it to 0 (single-tenant sentinel). Same pattern as Library.profile_id.
  • repository/postgres/playlist.rs β€” PostgresPlaylistRepository with 5 inherent *_for_profile(playlist_id, profile_id, user_id) methods:
    • list_for_profile β€” ORDER BY position ASC, updated_at DESC matches the desktop sidebar order
    • get_for_profile β€” Option<Playlist>, no existence leak across profile / user boundaries
    • insert_for_profile β€” writes a custom playlist (is_smart=0, smart_rules=NULL, position=0, cover_hash=NULL, cover_is_auto=0); smart-playlist support isn't on the server yet, it stays in [crate::smart_playlists] consuming the desktop SQLite path until a later phase ports it
    • update_for_profile β€” UPDATE … RETURNING * with COALESCE, race-free against concurrent delete
    • delete_for_profile β€” bool, no-leak blur same as get
  • Denormalised track_count / total_duration_ms projected as 0::bigint until playlist_track ships on the server.
  • cover_path projected as NULL::text β€” resolved app-side from cover_hash, mirrors the desktop SELECT pattern.

Test plan

  • cargo check --manifest-path src-tauri/Cargo.toml --workspace --all-targets
  • cargo check --manifest-path src-tauri/crates/core/Cargo.toml --features postgres --all-targets (the Postgres-feature path the workspace check doesn't activate β€” lesson from fix(core): inline TrackRow SELECT in postgres list/get methodsΒ #187)
  • cargo clippy --manifest-path src-tauri/crates/core/Cargo.toml --features postgres --all-targets -- -D warnings
  • cargo test --manifest-path src-tauri/Cargo.toml --workspace (no regression)

The Postgres methods themselves get exercised by integration tests in waveflow-server (next PR β€” 1.b.5c-PR-B).

Refs: RFC-001 Β§6.5, follows the pattern established by #185, #186.

Summary by CodeRabbit

  • New Features

    • Ajout d’un contrΓ΄le de portΓ©e par profil pour les playlists : lecture, crΓ©ation, mise Γ  jour et suppression respectent dΓ©sormais l’isolation par profil/utilisateur.
  • Refactor

    • Le modΓ¨le de playlist inclut dΓ©sormais l’identifiant de profil, avec comportement par dΓ©faut adaptΓ© au mode bureau (profil unique).
  • Chores

    • Exposition d’un dΓ©pΓ΄t Postgres pour gΓ©rer les playlists en mode multi‑profil.

Review Change Stack

Same pattern as PostgresLibraryRepository β€” a playlist belongs
directly to a profile (not to a library), so the ownership chain is
`playlist -> profile -> user`, same depth as library. The repo does
NOT implement the single-tenant PlaylistRepository trait β€” that
surface has no notion of tenancy.

- New profile_id: i64 field on Playlist with #[sqlx(default)] for
  cross-backend compatibility. Same pattern as Library.profile_id.
  Desktop call site in commands/playlist.rs initializes it to 0
  (single-tenant sentinel).
- New postgres/playlist.rs with 5 *_for_profile inherent methods:
  - list_for_profile: ORDER BY position ASC, updated_at DESC
    (matches the desktop sidebar order)
  - get_for_profile: Option<Playlist>, no existence leak
  - insert_for_profile: writes a custom playlist (is_smart=0,
    smart_rules=NULL, position=0, cover_hash=NULL, cover_is_auto=0
    β€” smart-playlist support isn't on the server yet)
  - update_for_profile: UPDATE ... RETURNING with COALESCE
  - delete_for_profile: bool
- Counts (track_count, total_duration_ms) projected as 0::bigint
  until playlist_track ships on the server.
- cover_path projected as NULL::text (resolved app-side from
  cover_hash, same pattern as the desktop SELECT).

Wired in repository/postgres/mod.rs alongside Library + Profile +
Track.

Validated:
- cargo check --workspace --all-targets
- cargo check --features postgres --all-targets (on the core crate
  β€” catches the sqlx Postgres-only path that workspace check misses,
  the lesson from #187)
- cargo clippy --features postgres --all-targets -- -D warnings
- cargo test --workspace (passes, no test regression)

Signed-off-by: InstaZDLL <github.105mh@8shield.net>
@InstaZDLL InstaZDLL added scope: backend Rust/Tauri backend (src-tauri/) type: feat New feature labels May 30, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 30, 2026

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: e7030a21-82ab-4ff7-81a5-c3afaee93772

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 50f3cd5 and 58c7ab0.

πŸ“’ Files selected for processing (1)
  • src-tauri/crates/core/src/repository/postgres/playlist.rs

πŸ“ Walkthrough

Walkthrough

Ajout du champ profile_id au domaine Playlist, implémentation d'un repository Postgres tenant-scopé (list/get/insert/update/delete) qui restreint l'accès par (profile_id, user_id), et adaptation des exports et de la commande create_playlist (retourne profile_id: 0 en desktop).

Changes

Support multi-tenant Postgres pour playlists

Layer / File(s) Summary
Playlist domain profile_id field
src-tauri/crates/core/src/domain/playlist.rs
Ajout du champ public profile_id: i64 avec #[cfg_attr(..., sqlx(default))] et documentation expliquant l'interprΓ©tation single-tenant (0) vs multi-tenant (rΓ©fΓ©rence Γ  profile.id).
PostgreSQL tenant-scoped repository
src-tauri/crates/core/src/repository/postgres/playlist.rs
Nouvelle implΓ©mentation PostgresPlaylistRepository (struct + docs) et mΓ©thodes async list_for_profile, get_for_profile, insert_for_profile, update_for_profile, delete_for_profile. Chaque opΓ©ration vΓ©rifie la propriΓ©tΓ© via EXISTS(profile.user_id = ?), projette cover_path = NULL et stub track_count/total_duration_ms, et renvoie Ok(None)/Ok(false) en dehors de la portΓ©e.
Module exports and command integration
src-tauri/crates/core/src/repository/postgres/mod.rs, src-tauri/crates/app/src/commands/playlist.rs
Réexporte PostgresPlaylistRepository depuis le module Postgres, et met à jour create_playlist pour inclure profile_id: 0 avec commentaire sur la frontière single-tenant desktop.

Sequence Diagram(s)

sequenceDiagram
  participant Cmd as create_playlist (command)
  participant Repo as PostgresPlaylistRepository
  participant PG as PostgreSQL
  Cmd->>Repo: insert_for_profile(draft, profile_id, user_id)
  Repo->>PG: INSERT ... SELECT ... WHERE EXISTS(profile.id = $profile_id AND profile.user_id = $user_id)
  PG-->>Repo: 0 or 1 row
  Repo-->>Cmd: Ok(Some(Playlist)) or Ok(None)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • InstaZDLL/WaveFlow#181: Ajoute l'infrastructure Postgres tenant-scoped que ce PR rΓ©utilise pour les playlists, modifiant la shape Playlist et les exports du module Postgres.

Suggested labels

type: feat, size: l, scope: backend

"Playlist prend racine dans un profil,
Postgres vΓ©rifie qui tient la clΓ©,
Le desktop garde le profil Γ  zΓ©ro,
Les accès sont bornés par l'ID,
Les listes chantent en sécurité." 🎡

πŸš₯ Pre-merge checks | βœ… 5
βœ… Passed checks (5 passed)
Check name Status Explanation
Title check βœ… Passed Le titre dΓ©crit prΓ©cisΓ©ment l'ajout d'un dΓ©pΓ΄t tenant-scoped PostgresPlaylistRepository et fait rΓ©fΓ©rence Γ  la phase du projet (Phase 1.b.5c), correspondant directement aux changements dans le changeset.
Description check βœ… Passed La description couvre tous les Γ©lΓ©ments clΓ©s : rΓ©sumΓ© du changement, dΓ©tail des modifications par fichier, plan de test complet avec les commandes exΓ©cutΓ©es et rΓ©fΓ©rences aux PRs connexes (#185, #186) et RFC-001.
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/postgres-playlist-tenant-methods

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

@InstaZDLL InstaZDLL added the size: l 200-500 lines label May 30, 2026
@InstaZDLL InstaZDLL self-assigned this May 30, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

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 `@src-tauri/crates/core/src/repository/postgres/playlist.rs`:
- Around line 139-182: The INSERT in insert_for_profile creates a playlist with
cover_hash = NULL but sets cover_is_auto = 0, which incorrectly marks a playlist
as having a manual cover; change the SQL in insert_for_profile so the SELECT
uses cover_is_auto = 1 for new playlists with no cover (i.e. set cover_is_auto
to 1 instead of 0 in the INSERT ... SELECT), making the returned Playlist
reflect that no user cover exists.
πŸͺ„ 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: 03317c94-fa7e-4771-9ba5-f6a235f75e60

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 062c550 and 50f3cd5.

πŸ“’ Files selected for processing (4)
  • src-tauri/crates/app/src/commands/playlist.rs
  • src-tauri/crates/core/src/domain/playlist.rs
  • src-tauri/crates/core/src/repository/postgres/mod.rs
  • src-tauri/crates/core/src/repository/postgres/playlist.rs

Comment thread src-tauri/crates/core/src/repository/postgres/playlist.rs
CR caught a sticky-flag bug: the previous insert_for_profile wrote
cover_is_auto=0 alongside cover_hash=NULL, which incorrectly marks
the row as "user uploaded their own cover" β€” a future server-side
auto-cover pipeline would skip it.

Desktop convention:
- migration `DEFAULT 1` (auto-managed, ready for the auto-cover
  pipeline)
- `commands/playlist.rs::create_playlist` writes `cover_is_auto: 1`
- `commands/playlist_cover.rs` flips it to `0` only when the user
  uploads a manual cover, so the auto-regen path stops touching
  the row

Aligning the Postgres insert with that convention so a freshly
created playlist is correctly auto-managed from the start. Doc
expanded to call out the rationale + the desktop parity.

(Also rewrapped the doc paragraph to avoid a clippy
doc_lazy_continuation lint that fired on a stray `+` at the start
of a line β€” markdown was parsing it as a list bullet.)

Signed-off-by: InstaZDLL <github.105mh@8shield.net>
@InstaZDLL InstaZDLL merged commit 25b9ada into main May 30, 2026
14 checks passed
@InstaZDLL InstaZDLL deleted the feat/postgres-playlist-tenant-methods branch May 30, 2026 12:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: backend Rust/Tauri backend (src-tauri/) size: l 200-500 lines type: feat New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant