max / makenotwork
| 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 | |
| 10 | matches! |
| 11 | e, |
| 12 | crateDatabase |
| 13 | if db_err.code.as_deref == Some |
| 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 | |
| 24 | if is_unique_violation |
| 25 | crateConflict |
| 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 |
| 42 | base: &str, |
| 43 | mut insert: F, |
| 44 | |
| 45 | |
| 46 | F: FnMut(String) -> Fut, |
| 47 | Fut: , |
| 48 | |
| 49 | let mut slug = base.to_string; |
| 50 | let mut suffix = 1u32; |
| 51 | loop |
| 52 | match insert.await |
| 53 | Ok => return Ok, |
| 54 | Err if is_unique_violation && suffix < SLUG_SUFFIX_CAP => |
| 55 | suffix += 1; |
| 56 | slug = format!; |
| 57 | |
| 58 | Err => return Err, |
| 59 | |
| 60 | |
| 61 | |
| 62 |