use chrono::{DateTime, Utc}; use sqlx::PgPool; use uuid::Uuid; // Row shape for `event`: every field is populated by sqlx from `SELECT *` / `RETURNING *`. Several // (`slug`, `cover_image_path`, `export_epoch`, `created_at`) are not read through this struct today // — callers that need them query the column directly — but they are part of the row and stay here so // the struct keeps mirroring the table. #[allow(dead_code)] #[derive(Debug, sqlx::FromRow)] pub struct Event { pub id: Uuid, pub slug: String, pub name: String, pub cover_image_path: Option, pub is_active: bool, pub uploads_locked_at: Option>, pub export_released_at: Option>, /// Monotonic generation counter for the keepsake. Bumped in the SAME UPDATE as any change to /// `export_released_at` (release and reopen are its only writers). An export is downloadable /// iff a `done` `export_job` row carries this exact epoch — readiness is derived from that, /// never stored, so it cannot drift and no worker can resurrect it. See migration 014. pub export_epoch: i64, pub created_at: DateTime, } impl Event { pub async fn find_by_slug(pool: &PgPool, slug: &str) -> Result, sqlx::Error> { sqlx::query_as::<_, Self>("SELECT * FROM event WHERE slug = $1") .bind(slug) .fetch_optional(pool) .await } pub async fn create(pool: &PgPool, slug: &str, name: &str) -> Result { sqlx::query_as::<_, Self>("INSERT INTO event (slug, name) VALUES ($1, $2) RETURNING *") .bind(slug) .bind(name) .fetch_one(pool) .await } pub async fn find_or_create( pool: &PgPool, slug: &str, name: &str, ) -> Result { if let Some(event) = Self::find_by_slug(pool, slug).await? { return Ok(event); } Self::create(pool, slug, name).await } }