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