Skip to main content

max / makenotwork

2.4 KB · 60 lines History Blame Raw
1 //! Database-error helpers: unique-violation (23505) detection/mapping and the
2 //! auto-suffixing unique-slug insert loop.
3
4 /// Maximum auto-suffix attempts when allocating a unique slug before giving up.
5 pub const SLUG_SUFFIX_CAP: u32 = 100;
6
7 /// Whether `e` is a Postgres unique-violation (SQLSTATE 23505), the slug
8 /// collision backstop every [`insert_with_unique_slug`] caller relies on.
9 pub fn is_unique_violation(e: &crate::error::AppError) -> bool {
10 matches!(
11 e,
12 crate::error::AppError::Database(sqlx::Error::Database(db_err))
13 if db_err.code().as_deref() == Some("23505")
14 )
15 }
16
17 /// Map a Postgres unique-violation (23505) into a clean `AppError::Conflict(msg)`;
18 /// pass any other error through unchanged. A create/insert on a deterministic or
19 /// user-supplied unique key that legitimately re-runs (a retry, a duplicate the
20 /// user can fix) should surface a 409 with a helpful message, not a raw 500.
21 /// Use only where the INSERT has a single relevant unique constraint.
22 pub fn map_unique_violation(e: crate::error::AppError, msg: &str) -> crate::error::AppError {
23 if is_unique_violation(&e) {
24 crate::error::AppError::Conflict(msg.to_string())
25 } else {
26 e
27 }
28 }
29
30 /// Insert a row whose slug must be unique under a per-parent `UNIQUE` index,
31 /// auto-suffixing the slug (`base`, `base-2`, `base-3`, ...) on collision.
32 ///
33 /// The `UNIQUE` index is the race-safe source of truth: `insert` is retried on a
34 /// 23505 unique violation, up to [`SLUG_SUFFIX_CAP`] attempts, so a concurrent
35 /// insert racing between any pre-check and this one can never surface a raw 500.
36 /// `insert` receives the candidate slug and performs the actual row insert.
37 /// Use it for every slug insert (blog posts, items, item/project sections, the
38 /// internal content route) rather than pre-checking and hoping.
39 pub async fn insert_with_unique_slug<T, F, Fut>(
40 base: &str,
41 mut insert: F,
42 ) -> crate::error::Result<T>
43 where
44 F: FnMut(String) -> Fut,
45 Fut: std::future::Future<Output = crate::error::Result<T>>,
46 {
47 let mut slug = base.to_string();
48 let mut suffix = 1u32;
49 loop {
50 match insert(slug.clone()).await {
51 Ok(value) => return Ok(value),
52 Err(e) if is_unique_violation(&e) && suffix < SLUG_SUFFIX_CAP => {
53 suffix += 1;
54 slug = format!("{base}-{suffix}");
55 }
56 Err(e) => return Err(e),
57 }
58 }
59 }
60