//! Per-page persistence. Mirrors the rows that `pages` holds. use sqlx::{PgExecutor, PgPool}; use uuid::Uuid; use crate::domain::Page; use crate::error::AppResult; pub async fn create<'e, E: PgExecutor<'e>>( executor: E, chapter_id: Uuid, page_number: i32, storage_key: &str, content_type: &str, size_bytes: i64, ) -> AppResult { let row = sqlx::query_as::<_, Page>( r#" INSERT INTO pages (chapter_id, page_number, storage_key, content_type, size_bytes) VALUES ($1, $2, $3, $4, $5) RETURNING id, chapter_id, page_number, storage_key, content_type "#, ) .bind(chapter_id) .bind(page_number) .bind(storage_key) .bind(content_type) .bind(size_bytes) .fetch_one(executor) .await?; Ok(row) } /// Fetch a single page by id. `None` when it doesn't exist (e.g. deleted /// between an analysis job's enqueue and its dispatch). pub async fn find_by_id(pool: &PgPool, id: Uuid) -> AppResult> { let row = sqlx::query_as::<_, Page>( "SELECT id, chapter_id, page_number, storage_key, content_type \ FROM pages WHERE id = $1", ) .bind(id) .fetch_optional(pool) .await?; Ok(row) } /// Resolve a page's breadcrumb `(manga_id, chapter_id, page_number)` for /// live event payloads. `None` if the page doesn't exist. pub async fn locate( pool: &PgPool, page_id: Uuid, ) -> AppResult> { let row: Option<(Uuid, Uuid, i32)> = sqlx::query_as( "SELECT c.manga_id, p.chapter_id, p.page_number \ FROM pages p JOIN chapters c ON c.id = p.chapter_id WHERE p.id = $1", ) .bind(page_id) .fetch_optional(pool) .await?; Ok(row) } /// Like [`locate`] but also resolves the manga title and chapter number, so /// live analysis events can carry human labels for the "now analyzing" /// banner without the dashboard having to look them up. Returns /// `(manga_id, manga_title, chapter_id, chapter_number, page_number)`. pub async fn locate_labeled( pool: &PgPool, page_id: Uuid, ) -> AppResult> { let row: Option<(Uuid, String, Uuid, i32, i32)> = sqlx::query_as( "SELECT c.manga_id, m.title, p.chapter_id, c.number, p.page_number \ FROM pages p \ JOIN chapters c ON c.id = p.chapter_id \ JOIN mangas m ON m.id = c.manga_id \ WHERE p.id = $1", ) .bind(page_id) .fetch_optional(pool) .await?; Ok(row) } pub async fn list_for_chapter(pool: &PgPool, chapter_id: Uuid) -> AppResult> { let rows = sqlx::query_as::<_, Page>( r#" SELECT id, chapter_id, page_number, storage_key, content_type FROM pages WHERE chapter_id = $1 ORDER BY page_number ASC "#, ) .bind(chapter_id) .fetch_all(pool) .await?; Ok(rows) }