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 #[classify(fatal(CorruptState), from)]
17 UninitializedFieldError(derive_builder::UninitializedFieldError),
18 #[classify(fatal(CorruptState), from)]
23 EventDeserialization(serde_json::Error),
24 #[error("EntityHydrationError - SnapshotDecode at sequence {sequence}: {source}")]
30 #[classify(fatal(CorruptState))]
31 SnapshotDecode {
32 sequence: i32,
33 #[source]
34 source: serde_json::Error,
35 },
36 #[error(
38 "EntityHydrationError - SnapshotGap: snapshot at sequence {snapshot_sequence}, next event at {next_event_sequence}"
39 )]
40 #[classify(fatal(CorruptState))]
41 SnapshotGap {
42 snapshot_sequence: i32,
43 next_event_sequence: i32,
44 },
45 #[classify(fatal(CorruptState))]
47 NoEvents,
48 #[error(
51 "EntityHydrationError - IdMismatch: the entity id is not the id its events are stored under"
52 )]
53 #[classify(fatal(CorruptState))]
54 IdMismatch,
55}
56
57#[derive(Debug, errlanes::Classify)]
58#[error("CursorDestructureError: couldn't turn {0} into {1}")]
59#[classify(fatal(Config))]
60pub struct CursorDestructureError(&'static str, &'static str);
61
62impl From<(&'static str, &'static str)> for CursorDestructureError {
63 fn from((name, variant): (&'static str, &'static str)) -> Self {
64 Self(name, variant)
65 }
66}
67
68#[doc(hidden)]
69pub fn parse_constraint_detail_value(detail: Option<&str>) -> Option<String> {
90 let detail = detail?;
91 let start = detail.find("=(")? + 2;
92 let end = detail.rfind(')')?;
93 if start <= end {
94 Some(detail[start..end].to_string())
95 } else {
96 None
97 }
98}
99
100pub fn extract_constraint_value(db_err: &dyn sqlx::error::DatabaseError) -> Option<String> {
110 let pg_err = db_err.try_downcast_ref::<sqlx::postgres::PgDatabaseError>()?;
111 parse_constraint_detail_value(pg_err.detail())
112}
113
114pub fn extract_events_pkey_id_value(db_err: &dyn sqlx::error::DatabaseError) -> Option<String> {
126 events_pkey_id_from_value(extract_constraint_value(db_err)?)
127}
128
129fn events_pkey_id_from_value(value: String) -> Option<String> {
130 match value.rsplit_once(", ") {
131 Some((id, _sequence)) => Some(id.to_string()),
132 None => Some(value),
133 }
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq)]
143pub enum ConstraintKind {
144 Unique,
145 ForeignKey,
146 Check,
147}
148
149#[derive(Debug, errlanes::Classify)]
162#[classify(fatal(Invariant), error = manual)]
163pub struct NotFound {
164 pub entity: &'static str,
165 pub column: Option<&'static str>,
166 pub value: String,
167}
168
169impl NotFound {
170 pub fn new(
171 entity: &'static str,
172 column: Option<&'static str>,
173 value: impl Into<String>,
174 ) -> Self {
175 Self {
176 entity,
177 column,
178 value: value.into(),
179 }
180 }
181}
182
183impl std::fmt::Display for NotFound {
184 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
185 match self.column {
186 Some(column) => write!(f, "{} not found by {column}", self.entity),
187 None => write!(f, "{} not found", self.entity),
188 }
189 }
190}
191
192impl std::error::Error for NotFound {}
193
194#[doc(hidden)]
195pub fn fatal_is_not_found(fatal: &errlanes::Fatal) -> bool {
196 std::error::Error::source(fatal).is_some_and(|s| s.is::<NotFound>())
197}
198
199#[doc(hidden)]
200pub fn is_classified_constraint_violation(db_err: &dyn sqlx::error::DatabaseError) -> bool {
205 db_err.is_unique_violation() || db_err.is_foreign_key_violation() || db_err.is_check_violation()
206}
207
208#[doc(hidden)]
209pub struct NotFoundValue<'a, T: ?Sized>(pub &'a T);
212
213impl<T: std::fmt::Display + ?Sized> NotFoundValue<'_, T> {
214 pub fn to_not_found_value(&self) -> String {
215 self.0.to_string()
216 }
217}
218
219#[doc(hidden)]
220pub trait ToNotFoundValueFallback {
221 fn to_not_found_value(&self) -> String;
222}
223
224impl<T: std::fmt::Debug + ?Sized> ToNotFoundValueFallback for NotFoundValue<'_, T> {
225 fn to_not_found_value(&self) -> String {
226 format!("{:?}", self.0)
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233 use proptest::prelude::*;
234
235 #[test]
236 fn parse_simple_uuid_value() {
237 let detail = Some("Key (id)=(550e8400-e29b-41d4-a716-446655440000) already exists.");
238 assert_eq!(
239 parse_constraint_detail_value(detail),
240 Some("550e8400-e29b-41d4-a716-446655440000".to_string())
241 );
242 }
243
244 #[test]
245 fn parse_string_value() {
246 let detail = Some("Key (email)=(user@example.com) already exists.");
247 assert_eq!(
248 parse_constraint_detail_value(detail),
249 Some("user@example.com".to_string())
250 );
251 }
252
253 #[test]
254 fn parse_composite_key_value() {
255 let detail = Some("Key (tenant_id, email)=(abc, user@example.com) already exists.");
256 assert_eq!(
257 parse_constraint_detail_value(detail),
258 Some("abc, user@example.com".to_string())
259 );
260 }
261
262 #[test]
263 fn parse_none_detail() {
264 assert_eq!(parse_constraint_detail_value(None), None);
265 }
266
267 #[test]
268 fn parse_unexpected_format() {
269 let detail = Some("something unexpected");
270 assert_eq!(parse_constraint_detail_value(detail), None);
271 }
272
273 #[test]
274 fn parse_value_containing_parentheses() {
275 let detail = Some("Key (name)=(foo (bar)) already exists.");
276 assert_eq!(
277 parse_constraint_detail_value(detail),
278 Some("foo (bar)".to_string())
279 );
280 }
281
282 #[test]
283 fn events_pkey_id_strips_the_sequence() {
284 let value = parse_constraint_detail_value(Some(
285 "Key (id, sequence)=(550e8400-e29b-41d4-a716-446655440000, 1) already exists.",
286 ))
287 .unwrap();
288 assert_eq!(
289 events_pkey_id_from_value(value),
290 Some("550e8400-e29b-41d4-a716-446655440000".to_string())
291 );
292 }
293
294 #[test]
295 fn events_pkey_id_keeps_commas_inside_the_id() {
296 assert_eq!(
297 events_pkey_id_from_value("a, b, 3".to_string()),
298 Some("a, b".to_string())
299 );
300 }
301
302 #[test]
303 fn events_pkey_id_without_separator_returns_value_as_is() {
304 assert_eq!(
305 events_pkey_id_from_value("plain".to_string()),
306 Some("plain".to_string())
307 );
308 }
309
310 #[test]
311 fn parse_empty_value() {
312 let detail = Some("Key (col)=() already exists.");
313 assert_eq!(parse_constraint_detail_value(detail), Some("".to_string()));
314 }
315
316 #[test]
322 fn parse_translated_detail_value() {
323 let detail =
324 Some("Schlüssel »(id)=(550e8400-e29b-41d4-a716-446655440000)« existiert bereits.");
325 assert_eq!(
326 parse_constraint_detail_value(detail),
327 Some("550e8400-e29b-41d4-a716-446655440000".to_string())
328 );
329 }
330
331 #[test]
332 fn not_found_value_uses_display_when_available() {
333 #[allow(unused_imports)]
334 use crate::ToNotFoundValueFallback;
335
336 let val = "hello";
338 assert_eq!(NotFoundValue(val).to_not_found_value(), "hello");
339
340 let num = 42;
342 assert_eq!(NotFoundValue(&num).to_not_found_value(), "42");
343 }
344
345 #[test]
346 fn not_found_value_falls_back_to_debug() {
347 use crate::ToNotFoundValueFallback;
348
349 #[derive(Debug)]
351 #[allow(dead_code)]
352 struct OnlyDebug(i32);
353
354 let val = OnlyDebug(7);
355 assert_eq!(NotFoundValue(&val).to_not_found_value(), "OnlyDebug(7)");
356 }
357
358 proptest! {
359 #[test]
363 fn constraint_detail_never_panics_and_is_honest(detail in ".*") {
364 match parse_constraint_detail_value(Some(&detail)) {
365 None => {}
366 Some(v) => {
367 prop_assert!(detail.contains(&v));
369 let start = detail.find("=(").expect("start marker present") + 2;
371 let end = detail.rfind(')').expect("end marker present");
372 prop_assert!(start <= end);
373 prop_assert_eq!(&detail[start..end], v.as_str());
374 }
375 }
376 }
377
378 #[test]
379 fn constraint_detail_none_input_returns_none(_ in Just(())) {
380 prop_assert_eq!(parse_constraint_detail_value(None), None);
381 }
382 }
383}