//! Database-error helpers: unique-violation (23505) detection/mapping and the //! auto-suffixing unique-slug insert loop. /// Maximum auto-suffix attempts when allocating a unique slug before giving up. pub const SLUG_SUFFIX_CAP: u32 = 100; /// Whether `e` is a Postgres unique-violation (SQLSTATE 23505), the slug /// collision backstop every [`insert_with_unique_slug`] caller relies on. pub fn is_unique_violation(e: &crate::error::AppError) -> bool { matches!( e, crate::error::AppError::Database(sqlx::Error::Database(db_err)) if db_err.code().as_deref() == Some("23505") ) } /// Map a Postgres unique-violation (23505) into a clean `AppError::Conflict(msg)`; /// pass any other error through unchanged. A create/insert on a deterministic or /// user-supplied unique key that legitimately re-runs (a retry, a duplicate the /// user can fix) should surface a 409 with a helpful message, not a raw 500 /// (ultra-fuzz Run 12 Storage: unhandled 23505 on lower-traffic create paths). /// Use only where the INSERT has a single relevant unique constraint. pub fn map_unique_violation(e: crate::error::AppError, msg: &str) -> crate::error::AppError { if is_unique_violation(&e) { crate::error::AppError::Conflict(msg.to_string()) } else { e } } /// Insert a row whose slug must be unique under a per-parent `UNIQUE` index, /// auto-suffixing the slug (`base`, `base-2`, `base-3`, ...) on collision. /// /// The `UNIQUE` index is the race-safe source of truth: `insert` is retried on a /// 23505 unique violation, up to [`SLUG_SUFFIX_CAP`] attempts, so a concurrent /// insert racing between any pre-check and this one can never surface a raw 500. /// `insert` receives the candidate slug and performs the actual row insert. /// Collapses the formerly copy-pasted suffix loops across blog posts, items, and /// item/project sections (ultra-fuzz Run #1 UX, D1), including the internal /// content route that previously pre-checked but did not retry the insert. pub async fn insert_with_unique_slug( base: &str, mut insert: F, ) -> crate::error::Result where F: FnMut(String) -> Fut, Fut: std::future::Future>, { let mut slug = base.to_string(); let mut suffix = 1u32; loop { match insert(slug.clone()).await { Ok(value) => return Ok(value), Err(e) if is_unique_violation(&e) && suffix < SLUG_SUFFIX_CAP => { suffix += 1; slug = format!("{base}-{suffix}"); } Err(e) => return Err(e), } } }