Skip to main content

max / makenotwork

2.6 KB · 62 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 /// (ultra-fuzz Run 12 Storage: unhandled 23505 on lower-traffic create paths).
22 /// Use only where the INSERT has a single relevant unique constraint.
23 pub fn map_unique_violation(e: crate::error::AppError, msg: &str) -> crate::error::AppError {
24 if is_unique_violation(&e) {
25 crate::error::AppError::Conflict(msg.to_string())
26 } else {
27 e
28 }
29 }
30
31 /// Insert a row whose slug must be unique under a per-parent `UNIQUE` index,
32 /// auto-suffixing the slug (`base`, `base-2`, `base-3`, ...) on collision.
33 ///
34 /// The `UNIQUE` index is the race-safe source of truth: `insert` is retried on a
35 /// 23505 unique violation, up to [`SLUG_SUFFIX_CAP`] attempts, so a concurrent
36 /// insert racing between any pre-check and this one can never surface a raw 500.
37 /// `insert` receives the candidate slug and performs the actual row insert.
38 /// Collapses the formerly copy-pasted suffix loops across blog posts, items, and
39 /// item/project sections (ultra-fuzz Run #1 UX, D1), including the internal
40 /// content route that previously pre-checked but did not retry the insert.
41 pub async fn insert_with_unique_slug<T, F, Fut>(
42 base: &str,
43 mut insert: F,
44 ) -> crate::error::Result<T>
45 where
46 F: FnMut(String) -> Fut,
47 Fut: std::future::Future<Output = crate::error::Result<T>>,
48 {
49 let mut slug = base.to_string();
50 let mut suffix = 1u32;
51 loop {
52 match insert(slug.clone()).await {
53 Ok(value) => return Ok(value),
54 Err(e) if is_unique_violation(&e) && suffix < SLUG_SUFFIX_CAP => {
55 suffix += 1;
56 slug = format!("{base}-{suffix}");
57 }
58 Err(e) => return Err(e),
59 }
60 }
61 }
62