//! Chapter persistence. use sqlx::{PgExecutor, PgPool}; use uuid::Uuid; use crate::domain::Chapter; use crate::error::{AppError, AppResult}; pub async fn list_for_manga( pool: &PgPool, manga_id: Uuid, limit: i64, offset: i64, ) -> AppResult> { let rows = sqlx::query_as::<_, Chapter>( r#" SELECT id, manga_id, number, title, page_count, created_at FROM chapters WHERE manga_id = $1 ORDER BY number ASC LIMIT $2 OFFSET $3 "#, ) .bind(manga_id) .bind(limit) .bind(offset) .fetch_all(pool) .await?; Ok(rows) } pub async fn find_by_manga_and_number( pool: &PgPool, manga_id: Uuid, number: i32, ) -> AppResult> { let row = sqlx::query_as::<_, Chapter>( r#" SELECT id, manga_id, number, title, page_count, created_at FROM chapters WHERE manga_id = $1 AND number = $2 "#, ) .bind(manga_id) .bind(number) .fetch_optional(pool) .await?; Ok(row) } /// Accepts any `PgExecutor` so the upload handler can run this inside a /// transaction with the per-page inserts. Returns `AppError::Conflict` /// on the (manga_id, number) unique violation so handlers can surface a /// clean 409. pub async fn create<'e, E: PgExecutor<'e>>( executor: E, manga_id: Uuid, number: i32, title: Option<&str>, ) -> AppResult { let result = sqlx::query_as::<_, Chapter>( r#" INSERT INTO chapters (manga_id, number, title) VALUES ($1, $2, $3) RETURNING id, manga_id, number, title, page_count, created_at "#, ) .bind(manga_id) .bind(number) .bind(title) .fetch_one(executor) .await; match result { Ok(c) => Ok(c), Err(e) if is_unique_violation(&e) => Err(AppError::Conflict(format!( "chapter {number} already exists for this manga" ))), Err(e) => Err(AppError::Database(e)), } } pub async fn set_page_count<'e, E: PgExecutor<'e>>( executor: E, id: Uuid, page_count: i32, ) -> AppResult<()> { sqlx::query("UPDATE chapters SET page_count = $1 WHERE id = $2") .bind(page_count) .bind(id) .execute(executor) .await?; Ok(()) } fn is_unique_violation(err: &sqlx::Error) -> bool { if let sqlx::Error::Database(db_err) = err { db_err.code().as_deref() == Some("23505") } else { false } }