1pub type RepoFault = errlanes::Fault<errlanes::lanes!(Transient, Fatal)>;
8
9pub type RepoWriteError<C> = errlanes::Fail<C, errlanes::lanes!(Transient, Fatal)>;
12
13#[derive(Debug, errlanes::Classify)]
15pub enum EntityHydrationError {
16 #[error("EntityHydrationError - MissingField: {entity}.{field} was never set by any event")]
18 #[classify(fatal(CorruptState))]
19 MissingField {
20 entity: &'static str,
21 field: &'static str,
22 },
23 #[classify(fatal(CorruptState), from)]
28 EventDeserialization(serde_json::Error),
29 #[error("EntityHydrationError - SnapshotDecode at sequence {sequence}: {source}")]
35 #[classify(fatal(CorruptState))]
36 SnapshotDecode {
37 sequence: i32,
38 #[source]
39 source: serde_json::Error,
40 },
41 #[error(
43 "EntityHydrationError - SnapshotGap: snapshot at sequence {snapshot_sequence}, next event at {next_event_sequence}"
44 )]
45 #[classify(fatal(CorruptState))]
46 SnapshotGap {
47 snapshot_sequence: i32,
48 next_event_sequence: i32,
49 },
50 #[classify(fatal(CorruptState))]
52 NoEvents,
53 #[error(
56 "EntityHydrationError - IdMismatch: the entity id is not the id its events are stored under"
57 )]
58 #[classify(fatal(CorruptState))]
59 IdMismatch,
60}
61
62#[derive(Debug, errlanes::Classify)]
63#[error("CursorDestructureError: couldn't turn {0} into {1}")]
64#[classify(fatal(Config))]
65pub struct CursorDestructureError(&'static str, &'static str);
66
67impl From<(&'static str, &'static str)> for CursorDestructureError {
68 fn from((name, variant): (&'static str, &'static str)) -> Self {
69 Self(name, variant)
70 }
71}
72
73#[doc(hidden)]
74pub fn parse_constraint_detail_value(detail: Option<&str>) -> Option<String> {
95 let detail = detail?;
96 let start = detail.find("=(")? + 2;
97 let end = detail.rfind(')')?;
98 if start <= end {
99 Some(detail[start..end].to_string())
100 } else {
101 None
102 }
103}
104
105pub fn extract_constraint_value(db_err: &dyn sqlx::error::DatabaseError) -> Option<String> {
115 let pg_err = db_err.try_downcast_ref::<sqlx::postgres::PgDatabaseError>()?;
116 parse_constraint_detail_value(pg_err.detail())
117}
118
119pub fn extract_events_pkey_id_value(db_err: &dyn sqlx::error::DatabaseError) -> Option<String> {
131 events_pkey_id_from_value(extract_constraint_value(db_err)?)
132}
133
134fn events_pkey_id_from_value(value: String) -> Option<String> {
135 match value.rsplit_once(", ") {
136 Some((id, _sequence)) => Some(id.to_string()),
137 None => Some(value),
138 }
139}
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq)]
148pub enum ConstraintKind {
149 Unique,
150 ForeignKey,
151 Check,
152}
153
154#[derive(Debug, errlanes::Classify)]
167#[classify(fatal(Invariant), error = manual)]
168pub struct NotFound {
169 pub entity: &'static str,
170 pub column: Option<&'static str>,
171 pub value: String,
172}
173
174impl NotFound {
175 pub fn new(
176 entity: &'static str,
177 column: Option<&'static str>,
178 value: impl Into<String>,
179 ) -> Self {
180 Self {
181 entity,
182 column,
183 value: value.into(),
184 }
185 }
186}
187
188impl std::fmt::Display for NotFound {
189 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190 match self.column {
191 Some(column) => write!(f, "{} not found by {column}", self.entity),
192 None => write!(f, "{} not found", self.entity),
193 }
194 }
195}
196
197impl std::error::Error for NotFound {}
198
199#[doc(hidden)]
200pub fn fatal_is_not_found(fatal: &errlanes::Fatal) -> bool {
201 std::error::Error::source(fatal).is_some_and(|s| s.is::<NotFound>())
202}
203
204#[doc(hidden)]
205pub fn is_classified_constraint_violation(db_err: &dyn sqlx::error::DatabaseError) -> bool {
210 db_err.is_unique_violation() || db_err.is_foreign_key_violation() || db_err.is_check_violation()
211}
212
213#[doc(hidden)]
214pub struct NotFoundValue<'a, T: ?Sized>(pub &'a T);
217
218impl<T: std::fmt::Display + ?Sized> NotFoundValue<'_, T> {
219 pub fn to_not_found_value(&self) -> String {
220 self.0.to_string()
221 }
222}
223
224#[doc(hidden)]
225pub trait ToNotFoundValueFallback {
226 fn to_not_found_value(&self) -> String;
227}
228
229impl<T: std::fmt::Debug + ?Sized> ToNotFoundValueFallback for NotFoundValue<'_, T> {
230 fn to_not_found_value(&self) -> String {
231 format!("{:?}", self.0)
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238 use proptest::prelude::*;
239
240 #[test]
241 fn parse_simple_uuid_value() {
242 let detail = Some("Key (id)=(550e8400-e29b-41d4-a716-446655440000) already exists.");
243 assert_eq!(
244 parse_constraint_detail_value(detail),
245 Some("550e8400-e29b-41d4-a716-446655440000".to_string())
246 );
247 }
248
249 #[test]
250 fn parse_string_value() {
251 let detail = Some("Key (email)=(user@example.com) already exists.");
252 assert_eq!(
253 parse_constraint_detail_value(detail),
254 Some("user@example.com".to_string())
255 );
256 }
257
258 #[test]
259 fn parse_composite_key_value() {
260 let detail = Some("Key (tenant_id, email)=(abc, user@example.com) already exists.");
261 assert_eq!(
262 parse_constraint_detail_value(detail),
263 Some("abc, user@example.com".to_string())
264 );
265 }
266
267 #[test]
268 fn parse_none_detail() {
269 assert_eq!(parse_constraint_detail_value(None), None);
270 }
271
272 #[test]
273 fn parse_unexpected_format() {
274 let detail = Some("something unexpected");
275 assert_eq!(parse_constraint_detail_value(detail), None);
276 }
277
278 #[test]
279 fn parse_value_containing_parentheses() {
280 let detail = Some("Key (name)=(foo (bar)) already exists.");
281 assert_eq!(
282 parse_constraint_detail_value(detail),
283 Some("foo (bar)".to_string())
284 );
285 }
286
287 #[test]
288 fn events_pkey_id_strips_the_sequence() {
289 let value = parse_constraint_detail_value(Some(
290 "Key (id, sequence)=(550e8400-e29b-41d4-a716-446655440000, 1) already exists.",
291 ))
292 .unwrap();
293 assert_eq!(
294 events_pkey_id_from_value(value),
295 Some("550e8400-e29b-41d4-a716-446655440000".to_string())
296 );
297 }
298
299 #[test]
300 fn events_pkey_id_keeps_commas_inside_the_id() {
301 assert_eq!(
302 events_pkey_id_from_value("a, b, 3".to_string()),
303 Some("a, b".to_string())
304 );
305 }
306
307 #[test]
308 fn events_pkey_id_without_separator_returns_value_as_is() {
309 assert_eq!(
310 events_pkey_id_from_value("plain".to_string()),
311 Some("plain".to_string())
312 );
313 }
314
315 #[test]
316 fn parse_empty_value() {
317 let detail = Some("Key (col)=() already exists.");
318 assert_eq!(parse_constraint_detail_value(detail), Some("".to_string()));
319 }
320
321 #[test]
327 fn parse_translated_detail_value() {
328 let detail =
329 Some("Schlüssel »(id)=(550e8400-e29b-41d4-a716-446655440000)« existiert bereits.");
330 assert_eq!(
331 parse_constraint_detail_value(detail),
332 Some("550e8400-e29b-41d4-a716-446655440000".to_string())
333 );
334 }
335
336 #[test]
337 fn not_found_value_uses_display_when_available() {
338 #[allow(unused_imports)]
339 use crate::ToNotFoundValueFallback;
340
341 let val = "hello";
343 assert_eq!(NotFoundValue(val).to_not_found_value(), "hello");
344
345 let num = 42;
347 assert_eq!(NotFoundValue(&num).to_not_found_value(), "42");
348 }
349
350 #[test]
351 fn not_found_value_falls_back_to_debug() {
352 use crate::ToNotFoundValueFallback;
353
354 #[derive(Debug)]
356 #[allow(dead_code)]
357 struct OnlyDebug(i32);
358
359 let val = OnlyDebug(7);
360 assert_eq!(NotFoundValue(&val).to_not_found_value(), "OnlyDebug(7)");
361 }
362
363 proptest! {
364 #[test]
368 fn constraint_detail_never_panics_and_is_honest(detail in ".*") {
369 match parse_constraint_detail_value(Some(&detail)) {
370 None => {}
371 Some(v) => {
372 prop_assert!(detail.contains(&v));
374 let start = detail.find("=(").expect("start marker present") + 2;
376 let end = detail.rfind(')').expect("end marker present");
377 prop_assert!(start <= end);
378 prop_assert_eq!(&detail[start..end], v.as_str());
379 }
380 }
381 }
382
383 #[test]
384 fn constraint_detail_none_input_returns_none(_ in Just(())) {
385 prop_assert_eq!(parse_constraint_detail_value(None), None);
386 }
387 }
388}