1use thiserror::Error;
4
5#[derive(Error, Debug)]
7pub enum EntityHydrationError {
8 #[error("EntityHydrationError - UninitializedFieldError: {0}")]
9 UninitializedFieldError(#[from] derive_builder::UninitializedFieldError),
10 #[error("EntityHydrationError - Deserialization: {0}")]
11 EventDeserialization(#[from] serde_json::Error),
12 #[error("EntityHydrationError - SnapshotDecode at sequence {sequence}: {source}")]
16 SnapshotDecode {
17 sequence: i32,
18 #[source]
19 source: serde_json::Error,
20 },
21 #[error(
23 "EntityHydrationError - SnapshotGap: snapshot at sequence {snapshot_sequence}, next event at {next_event_sequence}"
24 )]
25 SnapshotGap {
26 snapshot_sequence: i32,
27 next_event_sequence: i32,
28 },
29 #[error("EntityHydrationError - NoEvents")]
31 NoEvents,
32}
33
34#[derive(Error, Debug)]
35#[error("CursorDestructureError: couldn't turn {0} into {1}")]
36pub struct CursorDestructureError(&'static str, &'static str);
37
38impl From<(&'static str, &'static str)> for CursorDestructureError {
39 fn from((name, variant): (&'static str, &'static str)) -> Self {
40 Self(name, variant)
41 }
42}
43
44#[doc(hidden)]
45pub fn parse_constraint_detail_value(detail: Option<&str>) -> Option<String> {
57 let detail = detail?;
58 let start = detail.find("=(")? + 2;
59 let end = detail.rfind(") already")?;
60 if start <= end {
61 Some(detail[start..end].to_string())
62 } else {
63 None
64 }
65}
66
67#[doc(hidden)]
68pub fn extract_constraint_value(db_err: &dyn sqlx::error::DatabaseError) -> Option<String> {
76 let pg_err = db_err.try_downcast_ref::<sqlx::postgres::PgDatabaseError>()?;
77 parse_constraint_detail_value(pg_err.detail())
78}
79
80#[doc(hidden)]
81pub fn extract_events_pkey_id_value(db_err: &dyn sqlx::error::DatabaseError) -> Option<String> {
92 events_pkey_id_from_value(extract_constraint_value(db_err)?)
93}
94
95fn events_pkey_id_from_value(value: String) -> Option<String> {
96 match value.rsplit_once(", ") {
97 Some((id, _sequence)) => Some(id.to_string()),
98 None => Some(value),
99 }
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106pub enum ConstraintKind {
107 Unique,
108 ForeignKey,
109 Check,
110}
111
112#[doc(hidden)]
113pub fn is_classified_constraint_violation(db_err: &dyn sqlx::error::DatabaseError) -> bool {
118 db_err.is_unique_violation() || db_err.is_foreign_key_violation() || db_err.is_check_violation()
119}
120
121#[doc(hidden)]
122pub struct NotFoundValue<'a, T: ?Sized>(pub &'a T);
125
126impl<T: std::fmt::Display + ?Sized> NotFoundValue<'_, T> {
127 pub fn to_not_found_value(&self) -> String {
128 self.0.to_string()
129 }
130}
131
132#[doc(hidden)]
133pub trait ToNotFoundValueFallback {
134 fn to_not_found_value(&self) -> String;
135}
136
137impl<T: std::fmt::Debug + ?Sized> ToNotFoundValueFallback for NotFoundValue<'_, T> {
138 fn to_not_found_value(&self) -> String {
139 format!("{:?}", self.0)
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use proptest::prelude::*;
147
148 #[test]
149 fn parse_simple_uuid_value() {
150 let detail = Some("Key (id)=(550e8400-e29b-41d4-a716-446655440000) already exists.");
151 assert_eq!(
152 parse_constraint_detail_value(detail),
153 Some("550e8400-e29b-41d4-a716-446655440000".to_string())
154 );
155 }
156
157 #[test]
158 fn parse_string_value() {
159 let detail = Some("Key (email)=(user@example.com) already exists.");
160 assert_eq!(
161 parse_constraint_detail_value(detail),
162 Some("user@example.com".to_string())
163 );
164 }
165
166 #[test]
167 fn parse_composite_key_value() {
168 let detail = Some("Key (tenant_id, email)=(abc, user@example.com) already exists.");
169 assert_eq!(
170 parse_constraint_detail_value(detail),
171 Some("abc, user@example.com".to_string())
172 );
173 }
174
175 #[test]
176 fn parse_none_detail() {
177 assert_eq!(parse_constraint_detail_value(None), None);
178 }
179
180 #[test]
181 fn parse_unexpected_format() {
182 let detail = Some("something unexpected");
183 assert_eq!(parse_constraint_detail_value(detail), None);
184 }
185
186 #[test]
187 fn parse_value_containing_parentheses() {
188 let detail = Some("Key (name)=(foo (bar)) already exists.");
189 assert_eq!(
190 parse_constraint_detail_value(detail),
191 Some("foo (bar)".to_string())
192 );
193 }
194
195 #[test]
196 fn events_pkey_id_strips_the_sequence() {
197 let value = parse_constraint_detail_value(Some(
198 "Key (id, sequence)=(550e8400-e29b-41d4-a716-446655440000, 1) already exists.",
199 ))
200 .unwrap();
201 assert_eq!(
202 events_pkey_id_from_value(value),
203 Some("550e8400-e29b-41d4-a716-446655440000".to_string())
204 );
205 }
206
207 #[test]
208 fn events_pkey_id_keeps_commas_inside_the_id() {
209 assert_eq!(
210 events_pkey_id_from_value("a, b, 3".to_string()),
211 Some("a, b".to_string())
212 );
213 }
214
215 #[test]
216 fn events_pkey_id_without_separator_returns_value_as_is() {
217 assert_eq!(
218 events_pkey_id_from_value("plain".to_string()),
219 Some("plain".to_string())
220 );
221 }
222
223 #[test]
224 fn parse_empty_value() {
225 let detail = Some("Key (col)=() already exists.");
226 assert_eq!(parse_constraint_detail_value(detail), Some("".to_string()));
227 }
228
229 #[test]
230 fn not_found_value_uses_display_when_available() {
231 #[allow(unused_imports)]
232 use crate::ToNotFoundValueFallback;
233
234 let val = "hello";
236 assert_eq!(NotFoundValue(val).to_not_found_value(), "hello");
237
238 let num = 42;
240 assert_eq!(NotFoundValue(&num).to_not_found_value(), "42");
241 }
242
243 #[test]
244 fn not_found_value_falls_back_to_debug() {
245 use crate::ToNotFoundValueFallback;
246
247 #[derive(Debug)]
249 #[allow(dead_code)]
250 struct OnlyDebug(i32);
251
252 let val = OnlyDebug(7);
253 assert_eq!(NotFoundValue(&val).to_not_found_value(), "OnlyDebug(7)");
254 }
255
256 proptest! {
257 #[test]
261 fn constraint_detail_never_panics_and_is_honest(detail in ".*") {
262 match parse_constraint_detail_value(Some(&detail)) {
263 None => {}
264 Some(v) => {
265 prop_assert!(detail.contains(&v));
267 let start = detail.find("=(").expect("start marker present") + 2;
269 let end = detail
270 .rfind(") already")
271 .expect("end marker present");
272 prop_assert!(start <= end);
273 prop_assert_eq!(&detail[start..end], v.as_str());
274 }
275 }
276 }
277
278 #[test]
279 fn constraint_detail_none_input_returns_none(_ in Just(())) {
280 prop_assert_eq!(parse_constraint_detail_value(None), None);
281 }
282 }
283}