1pub type RepoReadError = errlanes::Fault<errlanes::lanes!(Transient, Fatal)>;
6
7pub type RepoWriteError<C> = errlanes::Fail<C, errlanes::lanes!(Transient, Fatal)>;
10
11#[derive(Debug, errlanes::Classify)]
13pub enum EntityHydrationError {
14 #[classify(fatal(CorruptState), from)]
15 UninitializedFieldError(derive_builder::UninitializedFieldError),
16 #[classify(fatal(CorruptState), from)]
21 EventDeserialization(serde_json::Error),
22 #[error("EntityHydrationError - SnapshotDecode at sequence {sequence}: {source}")]
28 #[classify(fatal(CorruptState))]
29 SnapshotDecode {
30 sequence: i32,
31 #[source]
32 source: serde_json::Error,
33 },
34 #[error(
36 "EntityHydrationError - SnapshotGap: snapshot at sequence {snapshot_sequence}, next event at {next_event_sequence}"
37 )]
38 #[classify(fatal(CorruptState))]
39 SnapshotGap {
40 snapshot_sequence: i32,
41 next_event_sequence: i32,
42 },
43 #[classify(fatal(CorruptState))]
45 NoEvents,
46}
47
48#[derive(Debug, errlanes::Classify)]
49#[error("CursorDestructureError: couldn't turn {0} into {1}")]
50#[classify(fatal(Config))]
51pub struct CursorDestructureError(&'static str, &'static str);
52
53impl From<(&'static str, &'static str)> for CursorDestructureError {
54 fn from((name, variant): (&'static str, &'static str)) -> Self {
55 Self(name, variant)
56 }
57}
58
59#[doc(hidden)]
60pub fn parse_constraint_detail_value(detail: Option<&str>) -> Option<String> {
81 let detail = detail?;
82 let start = detail.find("=(")? + 2;
83 let end = detail.rfind(')')?;
84 if start <= end {
85 Some(detail[start..end].to_string())
86 } else {
87 None
88 }
89}
90
91pub fn extract_constraint_value(db_err: &dyn sqlx::error::DatabaseError) -> Option<String> {
101 let pg_err = db_err.try_downcast_ref::<sqlx::postgres::PgDatabaseError>()?;
102 parse_constraint_detail_value(pg_err.detail())
103}
104
105pub fn extract_events_pkey_id_value(db_err: &dyn sqlx::error::DatabaseError) -> Option<String> {
117 events_pkey_id_from_value(extract_constraint_value(db_err)?)
118}
119
120fn events_pkey_id_from_value(value: String) -> Option<String> {
121 match value.rsplit_once(", ") {
122 Some((id, _sequence)) => Some(id.to_string()),
123 None => Some(value),
124 }
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134pub enum ConstraintKind {
135 Unique,
136 ForeignKey,
137 Check,
138}
139
140#[derive(Debug, errlanes::Classify)]
153#[classify(fatal(Invariant), error = manual)]
154pub struct NotFound {
155 pub entity: &'static str,
156 pub column: Option<&'static str>,
157 pub value: String,
158}
159
160impl NotFound {
161 pub fn new(
162 entity: &'static str,
163 column: Option<&'static str>,
164 value: impl Into<String>,
165 ) -> Self {
166 Self {
167 entity,
168 column,
169 value: value.into(),
170 }
171 }
172}
173
174impl std::fmt::Display for NotFound {
175 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176 match self.column {
177 Some(column) => write!(f, "{} not found by {column}", self.entity),
178 None => write!(f, "{} not found", self.entity),
179 }
180 }
181}
182
183impl std::error::Error for NotFound {}
184
185#[doc(hidden)]
186pub fn fatal_is_not_found(fatal: &errlanes::Fatal) -> bool {
187 std::error::Error::source(fatal).is_some_and(|s| s.is::<NotFound>())
188}
189
190#[doc(hidden)]
191pub fn is_classified_constraint_violation(db_err: &dyn sqlx::error::DatabaseError) -> bool {
196 db_err.is_unique_violation() || db_err.is_foreign_key_violation() || db_err.is_check_violation()
197}
198
199#[doc(hidden)]
200pub struct NotFoundValue<'a, T: ?Sized>(pub &'a T);
203
204impl<T: std::fmt::Display + ?Sized> NotFoundValue<'_, T> {
205 pub fn to_not_found_value(&self) -> String {
206 self.0.to_string()
207 }
208}
209
210#[doc(hidden)]
211pub trait ToNotFoundValueFallback {
212 fn to_not_found_value(&self) -> String;
213}
214
215impl<T: std::fmt::Debug + ?Sized> ToNotFoundValueFallback for NotFoundValue<'_, T> {
216 fn to_not_found_value(&self) -> String {
217 format!("{:?}", self.0)
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224 use proptest::prelude::*;
225
226 #[test]
227 fn parse_simple_uuid_value() {
228 let detail = Some("Key (id)=(550e8400-e29b-41d4-a716-446655440000) already exists.");
229 assert_eq!(
230 parse_constraint_detail_value(detail),
231 Some("550e8400-e29b-41d4-a716-446655440000".to_string())
232 );
233 }
234
235 #[test]
236 fn parse_string_value() {
237 let detail = Some("Key (email)=(user@example.com) already exists.");
238 assert_eq!(
239 parse_constraint_detail_value(detail),
240 Some("user@example.com".to_string())
241 );
242 }
243
244 #[test]
245 fn parse_composite_key_value() {
246 let detail = Some("Key (tenant_id, email)=(abc, user@example.com) already exists.");
247 assert_eq!(
248 parse_constraint_detail_value(detail),
249 Some("abc, user@example.com".to_string())
250 );
251 }
252
253 #[test]
254 fn parse_none_detail() {
255 assert_eq!(parse_constraint_detail_value(None), None);
256 }
257
258 #[test]
259 fn parse_unexpected_format() {
260 let detail = Some("something unexpected");
261 assert_eq!(parse_constraint_detail_value(detail), None);
262 }
263
264 #[test]
265 fn parse_value_containing_parentheses() {
266 let detail = Some("Key (name)=(foo (bar)) already exists.");
267 assert_eq!(
268 parse_constraint_detail_value(detail),
269 Some("foo (bar)".to_string())
270 );
271 }
272
273 #[test]
274 fn events_pkey_id_strips_the_sequence() {
275 let value = parse_constraint_detail_value(Some(
276 "Key (id, sequence)=(550e8400-e29b-41d4-a716-446655440000, 1) already exists.",
277 ))
278 .unwrap();
279 assert_eq!(
280 events_pkey_id_from_value(value),
281 Some("550e8400-e29b-41d4-a716-446655440000".to_string())
282 );
283 }
284
285 #[test]
286 fn events_pkey_id_keeps_commas_inside_the_id() {
287 assert_eq!(
288 events_pkey_id_from_value("a, b, 3".to_string()),
289 Some("a, b".to_string())
290 );
291 }
292
293 #[test]
294 fn events_pkey_id_without_separator_returns_value_as_is() {
295 assert_eq!(
296 events_pkey_id_from_value("plain".to_string()),
297 Some("plain".to_string())
298 );
299 }
300
301 #[test]
302 fn parse_empty_value() {
303 let detail = Some("Key (col)=() already exists.");
304 assert_eq!(parse_constraint_detail_value(detail), Some("".to_string()));
305 }
306
307 #[test]
313 fn parse_translated_detail_value() {
314 let detail =
315 Some("Schlüssel »(id)=(550e8400-e29b-41d4-a716-446655440000)« existiert bereits.");
316 assert_eq!(
317 parse_constraint_detail_value(detail),
318 Some("550e8400-e29b-41d4-a716-446655440000".to_string())
319 );
320 }
321
322 #[test]
323 fn not_found_value_uses_display_when_available() {
324 #[allow(unused_imports)]
325 use crate::ToNotFoundValueFallback;
326
327 let val = "hello";
329 assert_eq!(NotFoundValue(val).to_not_found_value(), "hello");
330
331 let num = 42;
333 assert_eq!(NotFoundValue(&num).to_not_found_value(), "42");
334 }
335
336 #[test]
337 fn not_found_value_falls_back_to_debug() {
338 use crate::ToNotFoundValueFallback;
339
340 #[derive(Debug)]
342 #[allow(dead_code)]
343 struct OnlyDebug(i32);
344
345 let val = OnlyDebug(7);
346 assert_eq!(NotFoundValue(&val).to_not_found_value(), "OnlyDebug(7)");
347 }
348
349 proptest! {
350 #[test]
354 fn constraint_detail_never_panics_and_is_honest(detail in ".*") {
355 match parse_constraint_detail_value(Some(&detail)) {
356 None => {}
357 Some(v) => {
358 prop_assert!(detail.contains(&v));
360 let start = detail.find("=(").expect("start marker present") + 2;
362 let end = detail.rfind(')').expect("end marker present");
363 prop_assert!(start <= end);
364 prop_assert_eq!(&detail[start..end], v.as_str());
365 }
366 }
367 }
368
369 #[test]
370 fn constraint_detail_none_input_returns_none(_ in Just(())) {
371 prop_assert_eq!(parse_constraint_detail_value(None), None);
372 }
373 }
374}