1use dovecote::{DeliveryState, RowId};
4use thiserror::Error;
5
6#[derive(Clone, Copy, Debug, Eq, PartialEq)]
9#[non_exhaustive]
10pub enum TransientKind {
11 SerializationFailure,
13 DeadlockDetected,
15 StatementOrLockTimeout,
17}
18
19impl std::fmt::Display for TransientKind {
20 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21 let label = match self {
22 Self::SerializationFailure => "serialization failure",
23 Self::DeadlockDetected => "deadlock detected",
24 Self::StatementOrLockTimeout => "statement or lock timeout",
25 };
26 formatter.write_str(label)
27 }
28}
29
30impl TransientKind {
31 pub(crate) fn from_sqlx(source: &sqlx::Error) -> Option<Self> {
32 Self::from_sqlstate(source.as_database_error()?.code()?.as_ref())
33 }
34
35 pub(crate) fn from_sqlstate(code: &str) -> Option<Self> {
36 match code {
37 "40001" => Some(Self::SerializationFailure),
38 "40P01" => Some(Self::DeadlockDetected),
39 "57014" | "55P03" => Some(Self::StatementOrLockTimeout),
40 _ => None,
41 }
42 }
43}
44
45#[cfg(test)]
46mod tests {
47 use super::TransientKind;
48
49 #[test]
50 fn postgres_transient_sqlstates_have_typed_categories() {
51 assert_eq!(
52 TransientKind::from_sqlstate("40001"),
53 Some(TransientKind::SerializationFailure)
54 );
55 assert_eq!(
56 TransientKind::from_sqlstate("40P01"),
57 Some(TransientKind::DeadlockDetected)
58 );
59 for code in ["57014", "55P03"] {
60 assert_eq!(
61 TransientKind::from_sqlstate(code),
62 Some(TransientKind::StatementOrLockTimeout)
63 );
64 }
65 assert_eq!(TransientKind::from_sqlstate("23505"), None);
66 }
67}
68
69#[derive(Debug, Error)]
71#[non_exhaustive]
72pub enum EnqueueError {
73 #[error("idempotency conflict for existing row {existing_row_id:?}")]
75 IdempotencyConflict {
76 existing_row_id: RowId,
78 },
79 #[error("migration mismatch: {detail}")]
80 MigrationMismatch {
82 detail: String,
84 },
85 #[error("serialization: {detail}")]
87 Serialization {
88 detail: String,
90 },
91 #[error("{operation}: {source}")]
92 Sql {
94 operation: &'static str,
96 #[source]
98 source: sqlx::Error,
99 },
100 #[error("{operation}: {kind}: {source}")]
101 Transient {
103 operation: &'static str,
105 kind: TransientKind,
107 #[source]
109 source: sqlx::Error,
110 },
111}
112
113impl EnqueueError {
114 pub(crate) fn sql(operation: &'static str, source: sqlx::Error) -> Self {
115 match TransientKind::from_sqlx(&source) {
116 Some(kind) => Self::Transient {
117 operation,
118 kind,
119 source,
120 },
121 None => Self::Sql { operation, source },
122 }
123 }
124
125 pub(crate) fn serialization(detail: impl Into<String>) -> Self {
126 Self::Serialization {
127 detail: detail.into(),
128 }
129 }
130}
131
132#[derive(Debug, Error)]
134#[non_exhaustive]
135pub enum ImportError {
136 #[error("immutable event identity conflict for existing row {existing_row_id:?}")]
138 IdentityConflict {
139 existing_row_id: RowId,
141 },
142 #[error("imported delivery state conflict for existing row {existing_row_id:?}")]
143 ImportConflict {
145 existing_row_id: RowId,
147 },
148 #[error("invalid imported delivery state: {source}")]
149 InvalidState {
151 #[source]
153 source: dovecote::ValidationError,
154 },
155 #[error("migration mismatch: {detail}")]
156 MigrationMismatch {
158 detail: String,
160 },
161 #[error("serialization: {detail}")]
162 Serialization {
164 detail: String,
166 },
167 #[error("{operation}: {source}")]
168 Sql {
170 operation: &'static str,
172 #[source]
174 source: sqlx::Error,
175 },
176 #[error("{operation}: {kind}: {source}")]
177 Transient {
179 operation: &'static str,
181 kind: TransientKind,
183 #[source]
185 source: sqlx::Error,
186 },
187}
188
189#[derive(Debug, Error)]
191#[non_exhaustive]
192pub enum FinalizeError {
193 #[error("event row not found")]
195 NotFound,
196 #[error("delivery row {row_id:?} is not a canonical imported pending delivery")]
197 StateConflict {
199 row_id: RowId,
201 },
202 #[error("invalid authoritative delivery timestamp: {source}")]
203 InvalidTimestamp {
205 #[source]
207 source: dovecote::ValidationError,
208 },
209 #[error("migration mismatch: {detail}")]
210 MigrationMismatch {
212 detail: String,
214 },
215 #[error("serialization: {detail}")]
216 Serialization {
218 detail: String,
220 },
221 #[error("{operation}: {source}")]
222 Sql {
224 operation: &'static str,
226 #[source]
228 source: sqlx::Error,
229 },
230 #[error("{operation}: {kind}: {source}")]
231 Transient {
233 operation: &'static str,
235 kind: TransientKind,
237 #[source]
239 source: sqlx::Error,
240 },
241}
242
243impl FinalizeError {
244 pub(crate) fn sql(operation: &'static str, source: sqlx::Error) -> Self {
245 match TransientKind::from_sqlx(&source) {
246 Some(kind) => Self::Transient {
247 operation,
248 kind,
249 source,
250 },
251 None => Self::Sql { operation, source },
252 }
253 }
254}
255
256impl ImportError {
257 pub(crate) fn sql(operation: &'static str, source: sqlx::Error) -> Self {
258 match TransientKind::from_sqlx(&source) {
259 Some(kind) => Self::Transient {
260 operation,
261 kind,
262 source,
263 },
264 None => Self::Sql { operation, source },
265 }
266 }
267
268 pub(crate) fn serialization(detail: impl Into<String>) -> Self {
269 Self::Serialization {
270 detail: detail.into(),
271 }
272 }
273}
274
275#[derive(Debug, Error)]
277#[non_exhaustive]
278pub enum ClaimError {
279 #[error("attempt counter overflow for row {row_id:?}")]
281 CounterOverflow {
282 row_id: RowId,
284 },
285 #[error("operating-system entropy unavailable: {source}")]
286 EntropyUnavailable {
288 #[source]
290 source: getrandom::Error,
291 },
292 #[error("serialization: {detail}")]
293 Serialization {
295 detail: String,
297 },
298 #[error("migration mismatch: {detail}")]
299 MigrationMismatch {
301 detail: String,
303 },
304 #[error("{operation}: {source}")]
305 Sql {
307 operation: &'static str,
309 #[source]
311 source: sqlx::Error,
312 },
313 #[error("{operation}: {kind}: {source}")]
314 Transient {
316 operation: &'static str,
318 kind: TransientKind,
320 #[source]
322 source: sqlx::Error,
323 },
324}
325
326impl ClaimError {
327 pub(crate) fn sql(operation: &'static str, source: sqlx::Error) -> Self {
328 match TransientKind::from_sqlx(&source) {
329 Some(kind) => Self::Transient {
330 operation,
331 kind,
332 source,
333 },
334 None => Self::Sql { operation, source },
335 }
336 }
337
338 pub(crate) fn serialization(detail: impl Into<String>) -> Self {
339 Self::Serialization {
340 detail: detail.into(),
341 }
342 }
343}
344
345#[derive(Debug, Error)]
347#[non_exhaustive]
348pub enum MutationError {
349 #[error("event row not found")]
351 NotFound,
352 #[error("illegal delivery transition from {state:?}")]
353 IllegalTransition {
355 state: DeliveryState,
357 },
358 #[error("claim was lost")]
359 LostClaim,
361 #[error("migration mismatch: {detail}")]
362 MigrationMismatch {
364 detail: String,
366 },
367 #[error("serialization: {detail}")]
368 Serialization {
370 detail: String,
372 },
373 #[error("{operation}: {source}")]
374 Sql {
376 operation: &'static str,
378 #[source]
380 source: sqlx::Error,
381 },
382 #[error("{operation}: {kind}: {source}")]
383 Transient {
385 operation: &'static str,
387 kind: TransientKind,
389 #[source]
391 source: sqlx::Error,
392 },
393}
394
395#[derive(Debug, Error)]
397#[non_exhaustive]
398pub enum PageError {
399 #[error("serialization: {detail}")]
401 Serialization {
402 detail: String,
404 },
405 #[error("{operation}: {source}")]
406 Sql {
408 operation: &'static str,
410 #[source]
412 source: sqlx::Error,
413 },
414 #[error("{operation}: {kind}: {source}")]
415 Transient {
417 operation: &'static str,
419 kind: TransientKind,
421 #[source]
423 source: sqlx::Error,
424 },
425}
426
427impl PageError {
428 pub(crate) fn sql(operation: &'static str, source: sqlx::Error) -> Self {
429 match TransientKind::from_sqlx(&source) {
430 Some(kind) => Self::Transient {
431 operation,
432 kind,
433 source,
434 },
435 None => Self::Sql { operation, source },
436 }
437 }
438
439 pub(crate) fn serialization(detail: impl Into<String>) -> Self {
440 Self::Serialization {
441 detail: detail.into(),
442 }
443 }
444}
445
446impl MutationError {
447 pub(crate) fn sql(operation: &'static str, source: sqlx::Error) -> Self {
448 match TransientKind::from_sqlx(&source) {
449 Some(kind) => Self::Transient {
450 operation,
451 kind,
452 source,
453 },
454 None => Self::Sql { operation, source },
455 }
456 }
457
458 pub(crate) fn serialization(detail: impl Into<String>) -> Self {
459 Self::Serialization {
460 detail: detail.into(),
461 }
462 }
463}
464
465#[derive(Debug, Error)]
467#[non_exhaustive]
468pub enum SchemaError {
469 #[error("migration mismatch: {detail}")]
471 MigrationMismatch {
472 detail: String,
474 },
475 #[error("{operation}: {source}")]
476 Sql {
478 operation: &'static str,
480 #[source]
482 source: sqlx::Error,
483 },
484 #[error("{operation}: {kind}: {source}")]
485 Transient {
487 operation: &'static str,
489 kind: TransientKind,
491 #[source]
493 source: sqlx::Error,
494 },
495}
496
497impl SchemaError {
498 pub(crate) fn sql(operation: &'static str, source: sqlx::Error) -> Self {
499 match TransientKind::from_sqlx(&source) {
500 Some(kind) => Self::Transient {
501 operation,
502 kind,
503 source,
504 },
505 None => Self::Sql { operation, source },
506 }
507 }
508}