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> {
72 let detail = detail?;
73 let start = detail.find("=(")? + 2;
74 let end = detail.rfind(") already")?;
75 if start <= end {
76 Some(detail[start..end].to_string())
77 } else {
78 None
79 }
80}
81
82#[doc(hidden)]
83pub fn extract_constraint_value(db_err: &dyn sqlx::error::DatabaseError) -> Option<String> {
91 let pg_err = db_err.try_downcast_ref::<sqlx::postgres::PgDatabaseError>()?;
92 parse_constraint_detail_value(pg_err.detail())
93}
94
95#[doc(hidden)]
96pub fn extract_events_pkey_id_value(db_err: &dyn sqlx::error::DatabaseError) -> Option<String> {
107 events_pkey_id_from_value(extract_constraint_value(db_err)?)
108}
109
110fn events_pkey_id_from_value(value: String) -> Option<String> {
111 match value.rsplit_once(", ") {
112 Some((id, _sequence)) => Some(id.to_string()),
113 None => Some(value),
114 }
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub enum ConstraintKind {
125 Unique,
126 ForeignKey,
127 Check,
128}
129
130#[derive(Debug, errlanes::Classify)]
143#[classify(fatal(Invariant), error = manual)]
144pub struct NotFound {
145 pub entity: &'static str,
146 pub column: Option<&'static str>,
147 pub value: String,
148}
149
150impl NotFound {
151 pub fn new(
152 entity: &'static str,
153 column: Option<&'static str>,
154 value: impl Into<String>,
155 ) -> Self {
156 Self {
157 entity,
158 column,
159 value: value.into(),
160 }
161 }
162}
163
164impl std::fmt::Display for NotFound {
165 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166 match self.column {
167 Some(column) => write!(f, "{} not found by {column}", self.entity),
168 None => write!(f, "{} not found", self.entity),
169 }
170 }
171}
172
173impl std::error::Error for NotFound {}
174
175#[doc(hidden)]
176pub fn fatal_is_not_found(fatal: &errlanes::Fatal) -> bool {
177 std::error::Error::source(fatal).is_some_and(|s| s.is::<NotFound>())
178}
179
180#[doc(hidden)]
181pub fn is_classified_constraint_violation(db_err: &dyn sqlx::error::DatabaseError) -> bool {
186 db_err.is_unique_violation() || db_err.is_foreign_key_violation() || db_err.is_check_violation()
187}
188
189#[doc(hidden)]
190pub struct NotFoundValue<'a, T: ?Sized>(pub &'a T);
193
194impl<T: std::fmt::Display + ?Sized> NotFoundValue<'_, T> {
195 pub fn to_not_found_value(&self) -> String {
196 self.0.to_string()
197 }
198}
199
200#[doc(hidden)]
201pub trait ToNotFoundValueFallback {
202 fn to_not_found_value(&self) -> String;
203}
204
205impl<T: std::fmt::Debug + ?Sized> ToNotFoundValueFallback for NotFoundValue<'_, T> {
206 fn to_not_found_value(&self) -> String {
207 format!("{:?}", self.0)
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214 use proptest::prelude::*;
215
216 #[test]
217 fn parse_simple_uuid_value() {
218 let detail = Some("Key (id)=(550e8400-e29b-41d4-a716-446655440000) already exists.");
219 assert_eq!(
220 parse_constraint_detail_value(detail),
221 Some("550e8400-e29b-41d4-a716-446655440000".to_string())
222 );
223 }
224
225 #[test]
226 fn parse_string_value() {
227 let detail = Some("Key (email)=(user@example.com) already exists.");
228 assert_eq!(
229 parse_constraint_detail_value(detail),
230 Some("user@example.com".to_string())
231 );
232 }
233
234 #[test]
235 fn parse_composite_key_value() {
236 let detail = Some("Key (tenant_id, email)=(abc, user@example.com) already exists.");
237 assert_eq!(
238 parse_constraint_detail_value(detail),
239 Some("abc, user@example.com".to_string())
240 );
241 }
242
243 #[test]
244 fn parse_none_detail() {
245 assert_eq!(parse_constraint_detail_value(None), None);
246 }
247
248 #[test]
249 fn parse_unexpected_format() {
250 let detail = Some("something unexpected");
251 assert_eq!(parse_constraint_detail_value(detail), None);
252 }
253
254 #[test]
255 fn parse_value_containing_parentheses() {
256 let detail = Some("Key (name)=(foo (bar)) already exists.");
257 assert_eq!(
258 parse_constraint_detail_value(detail),
259 Some("foo (bar)".to_string())
260 );
261 }
262
263 #[test]
264 fn events_pkey_id_strips_the_sequence() {
265 let value = parse_constraint_detail_value(Some(
266 "Key (id, sequence)=(550e8400-e29b-41d4-a716-446655440000, 1) already exists.",
267 ))
268 .unwrap();
269 assert_eq!(
270 events_pkey_id_from_value(value),
271 Some("550e8400-e29b-41d4-a716-446655440000".to_string())
272 );
273 }
274
275 #[test]
276 fn events_pkey_id_keeps_commas_inside_the_id() {
277 assert_eq!(
278 events_pkey_id_from_value("a, b, 3".to_string()),
279 Some("a, b".to_string())
280 );
281 }
282
283 #[test]
284 fn events_pkey_id_without_separator_returns_value_as_is() {
285 assert_eq!(
286 events_pkey_id_from_value("plain".to_string()),
287 Some("plain".to_string())
288 );
289 }
290
291 #[test]
292 fn parse_empty_value() {
293 let detail = Some("Key (col)=() already exists.");
294 assert_eq!(parse_constraint_detail_value(detail), Some("".to_string()));
295 }
296
297 #[test]
298 fn not_found_value_uses_display_when_available() {
299 #[allow(unused_imports)]
300 use crate::ToNotFoundValueFallback;
301
302 let val = "hello";
304 assert_eq!(NotFoundValue(val).to_not_found_value(), "hello");
305
306 let num = 42;
308 assert_eq!(NotFoundValue(&num).to_not_found_value(), "42");
309 }
310
311 #[test]
312 fn not_found_value_falls_back_to_debug() {
313 use crate::ToNotFoundValueFallback;
314
315 #[derive(Debug)]
317 #[allow(dead_code)]
318 struct OnlyDebug(i32);
319
320 let val = OnlyDebug(7);
321 assert_eq!(NotFoundValue(&val).to_not_found_value(), "OnlyDebug(7)");
322 }
323
324 proptest! {
325 #[test]
329 fn constraint_detail_never_panics_and_is_honest(detail in ".*") {
330 match parse_constraint_detail_value(Some(&detail)) {
331 None => {}
332 Some(v) => {
333 prop_assert!(detail.contains(&v));
335 let start = detail.find("=(").expect("start marker present") + 2;
337 let end = detail
338 .rfind(") already")
339 .expect("end marker present");
340 prop_assert!(start <= end);
341 prop_assert_eq!(&detail[start..end], v.as_str());
342 }
343 }
344 }
345
346 #[test]
347 fn constraint_detail_none_input_returns_none(_ in Just(())) {
348 prop_assert_eq!(parse_constraint_detail_value(None), None);
349 }
350 }
351}