use std::collections::BTreeMap; use sqlx::{PgConnection, PgPool}; use uuid::Uuid; use crate::domain::genre::{Genre, GenreRef}; use crate::error::AppResult; pub async fn list_all(pool: &PgPool) -> AppResult> { let rows = sqlx::query_as::<_, Genre>( "SELECT id, name FROM genres ORDER BY lower(name)", ) .fetch_all(pool) .await?; Ok(rows) } pub async fn list_for_manga(pool: &PgPool, manga_id: Uuid) -> AppResult> { let rows = sqlx::query_as::<_, GenreRef>( r#" SELECT g.id, g.name FROM manga_genres mg JOIN genres g ON g.id = mg.genre_id WHERE mg.manga_id = $1 ORDER BY lower(g.name) "#, ) .bind(manga_id) .fetch_all(pool) .await?; Ok(rows) } pub async fn load_for_mangas( pool: &PgPool, manga_ids: &[Uuid], ) -> AppResult>> { if manga_ids.is_empty() { return Ok(BTreeMap::new()); } let rows: Vec<(Uuid, Uuid, String)> = sqlx::query_as( r#" SELECT mg.manga_id, g.id, g.name FROM manga_genres mg JOIN genres g ON g.id = mg.genre_id WHERE mg.manga_id = ANY($1) ORDER BY mg.manga_id, lower(g.name) "#, ) .bind(manga_ids) .fetch_all(pool) .await?; let mut grouped: BTreeMap> = BTreeMap::new(); for (manga_id, id, name) in rows { grouped.entry(manga_id).or_default().push(GenreRef { id, name }); } Ok(grouped) } /// Replace the manga's genres. Unknown ids are silently ignored: the /// FK constraint would reject them, so we filter upstream rather than /// surface a 500 here. (The API layer validates the set against /// `list_all` first.) /// /// Note: `crawler::repo::sync_genres` does a similar replace, but by /// *name* and with auto-create of unseen genres — the crawler can't /// validate against the curated vocabulary on its own. Both paths are /// intentional; don't merge them without preserving that semantic. pub async fn set_for_manga( conn: &mut PgConnection, manga_id: Uuid, genre_ids: &[Uuid], ) -> AppResult<()> { sqlx::query("DELETE FROM manga_genres WHERE manga_id = $1") .bind(manga_id) .execute(&mut *conn) .await?; if genre_ids.is_empty() { return Ok(()); } sqlx::query( r#" INSERT INTO manga_genres (manga_id, genre_id) SELECT $1, unnest($2::uuid[]) ON CONFLICT DO NOTHING "#, ) .bind(manga_id) .bind(genre_ids) .execute(&mut *conn) .await?; Ok(()) }