Skip to main content

dovecote_sqlx_sqlite/
error.rs

1//! Typed errors at the `SQLite` adapter boundary.
2
3use dovecote::{DeliveryState, RowId};
4use thiserror::Error;
5
6/// Errors that can be retried by the adapter's bounded busy policy.
7#[derive(Clone, Copy, Debug, Eq, PartialEq)]
8#[non_exhaustive]
9pub enum TransientKind {
10    /// `SQLite` could not acquire its single-writer lock before the configured
11    /// busy timeout. The complete operation has been rolled back.
12    BusyExhausted,
13}
14
15impl std::fmt::Display for TransientKind {
16    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17        formatter.write_str("SQLite busy timeout exhausted")
18    }
19}
20
21pub(crate) fn is_busy(source: &sqlx::Error) -> bool {
22    source
23        .as_database_error()
24        .and_then(sqlx::error::DatabaseError::code)
25        .and_then(|code| code.parse::<i32>().ok())
26        .is_some_and(|code| code == 5 || code == 6 || code & 0xff == 5 || code & 0xff == 6)
27}
28
29/// Errors returned while enqueueing an event.
30#[derive(Debug, Error)]
31#[non_exhaustive]
32pub enum EnqueueError {
33    /// The transaction was not opened with `SQLite`'s writer lock.
34    #[error("enqueue requires a SQLite write transaction (BEGIN IMMEDIATE or a prior write)")]
35    WriteTransactionRequired,
36    /// The adapter's bounded busy policy is not representable by `SQLite`.
37    #[error("invalid SQLite busy configuration: {detail}")]
38    Configuration {
39        /// Diagnostic describing the invalid configuration.
40        detail: String,
41    },
42    /// Existing identity has different immutable event content.
43    #[error("idempotency conflict for existing row {existing_row_id:?}")]
44    IdempotencyConflict {
45        /// Existing Dovecote row that conflicted with the event.
46        existing_row_id: RowId,
47    },
48    /// Installed durable schema is incompatible with this adapter.
49    #[error("migration mismatch: {detail}")]
50    MigrationMismatch {
51        /// Diagnostic describing the incompatible schema.
52        detail: String,
53    },
54    /// Stored or returned data could not be represented safely.
55    #[error("serialization: {detail}")]
56    Serialization {
57        /// Diagnostic describing the invalid stored data.
58        detail: String,
59    },
60    /// A caller transaction remained blocked after the configured busy wait.
61    #[error("{operation}: busy lock exhausted by the caller transaction: {source}")]
62    BusyExhausted {
63        /// Operation being performed when the lock wait was exhausted.
64        operation: &'static str,
65        #[source]
66        /// Original underlying `SQLite` error.
67        source: sqlx::Error,
68    },
69    /// A non-busy `SQLx` operation failed.
70    #[error("{operation}: {source}")]
71    Sql {
72        /// Operation being performed when SQL failed.
73        operation: &'static str,
74        #[source]
75        /// Original underlying `SQLite` error.
76        source: sqlx::Error,
77    },
78}
79
80impl EnqueueError {
81    pub(crate) fn sql(operation: &'static str, source: sqlx::Error) -> Self {
82        if is_busy(&source) {
83            Self::BusyExhausted { operation, source }
84        } else {
85            Self::Sql { operation, source }
86        }
87    }
88
89    pub(crate) fn serialization(detail: impl Into<String>) -> Self {
90        Self::Serialization {
91            detail: detail.into(),
92        }
93    }
94}
95
96/// Errors returned by the migration-only importer.
97#[derive(Debug, Error)]
98#[non_exhaustive]
99pub enum ImportError {
100    /// The transaction was not opened with `SQLite`'s writer lock.
101    #[error("import requires a SQLite write transaction (BEGIN IMMEDIATE or a prior write)")]
102    WriteTransactionRequired,
103    /// Existing identity has different immutable event content.
104    #[error("immutable event identity conflict for existing row {existing_row_id:?}")]
105    IdentityConflict {
106        /// Existing Dovecote row that conflicted with the imported event.
107        existing_row_id: RowId,
108    },
109    /// Existing delivery state differs from the requested imported state.
110    #[error("imported delivery state conflict for existing row {existing_row_id:?}")]
111    ImportConflict {
112        /// Existing Dovecote row whose delivery state conflicted.
113        existing_row_id: RowId,
114    },
115    /// The adapter configuration is not valid for `SQLite`.
116    #[error("invalid SQLite configuration: {detail}")]
117    Configuration {
118        /// Diagnostic describing the invalid configuration.
119        detail: String,
120    },
121    /// Imported state failed Dovecote validation.
122    #[error("invalid imported delivery state: {source}")]
123    InvalidState {
124        #[source]
125        /// Original validation error.
126        source: dovecote::ValidationError,
127    },
128    /// Installed durable schema is incompatible with this adapter.
129    #[error("migration mismatch: {detail}")]
130    MigrationMismatch {
131        /// Diagnostic describing the incompatible schema.
132        detail: String,
133    },
134    /// Stored or returned data could not be represented safely.
135    #[error("serialization: {detail}")]
136    Serialization {
137        /// Diagnostic describing the invalid stored data.
138        detail: String,
139    },
140    /// A caller transaction remained blocked after the configured busy wait.
141    #[error("{operation}: busy lock exhausted by the caller transaction: {source}")]
142    BusyExhausted {
143        /// Operation being performed when the lock wait was exhausted.
144        operation: &'static str,
145        #[source]
146        /// Original underlying `SQLite` error.
147        source: sqlx::Error,
148    },
149    /// A non-busy `SQLx` operation failed.
150    #[error("{operation}: {source}")]
151    Sql {
152        /// Operation being performed when SQL failed.
153        operation: &'static str,
154        #[source]
155        /// Original underlying `SQLite` error.
156        source: sqlx::Error,
157    },
158}
159
160/// Errors returned by the migration-only delivery finalizer.
161#[derive(Debug, Error)]
162#[non_exhaustive]
163pub enum FinalizeError {
164    /// The transaction was not opened with `SQLite`'s writer lock.
165    #[error("finalization requires a SQLite write transaction (BEGIN IMMEDIATE or a prior write)")]
166    WriteTransactionRequired,
167    /// No event row exists for the requested delivery.
168    #[error("event row not found")]
169    NotFound,
170    /// The row is not a canonical imported pending delivery.
171    #[error("delivery row {row_id:?} is not a canonical imported pending delivery")]
172    StateConflict {
173        /// Dovecote row that was not in the expected state.
174        row_id: RowId,
175    },
176    /// Authoritative delivery time failed Dovecote validation.
177    #[error("invalid authoritative delivery timestamp: {source}")]
178    InvalidTimestamp {
179        #[source]
180        /// Original validation error.
181        source: dovecote::ValidationError,
182    },
183    /// Installed durable schema is incompatible with this adapter.
184    #[error("migration mismatch: {detail}")]
185    MigrationMismatch {
186        /// Diagnostic describing the incompatible schema.
187        detail: String,
188    },
189    /// Stored or returned data could not be represented safely.
190    #[error("serialization: {detail}")]
191    Serialization {
192        /// Diagnostic describing the invalid stored data.
193        detail: String,
194    },
195    /// A caller transaction remained blocked after the configured busy wait.
196    #[error("{operation}: busy lock exhausted by the caller transaction: {source}")]
197    BusyExhausted {
198        /// Operation being performed when the lock wait was exhausted.
199        operation: &'static str,
200        #[source]
201        /// Original underlying `SQLite` error.
202        source: sqlx::Error,
203    },
204    /// A non-busy `SQLx` operation failed.
205    #[error("{operation}: {source}")]
206    Sql {
207        /// Operation being performed when SQL failed.
208        operation: &'static str,
209        #[source]
210        /// Original underlying `SQLite` error.
211        source: sqlx::Error,
212    },
213}
214
215impl FinalizeError {
216    pub(crate) fn sql(operation: &'static str, source: sqlx::Error) -> Self {
217        if is_busy(&source) {
218            Self::BusyExhausted { operation, source }
219        } else {
220            Self::Sql { operation, source }
221        }
222    }
223}
224
225impl ImportError {
226    pub(crate) fn sql(operation: &'static str, source: sqlx::Error) -> Self {
227        if is_busy(&source) {
228            Self::BusyExhausted { operation, source }
229        } else {
230            Self::Sql { operation, source }
231        }
232    }
233
234    pub(crate) fn serialization(detail: impl Into<String>) -> Self {
235        Self::Serialization {
236            detail: detail.into(),
237        }
238    }
239}
240
241/// Errors returned while selecting and claiming a batch of events.
242#[derive(Debug, Error)]
243#[non_exhaustive]
244pub enum ClaimError {
245    #[cfg(test)]
246    /// Test-only failpoint used to verify claim rollback.
247    #[error("test claim failpoint triggered after delivery updates")]
248    InjectedFailure,
249    /// The delivery attempt counter cannot be incremented.
250    #[error("attempt counter overflow for row {row_id:?}")]
251    CounterOverflow {
252        /// Dovecote row whose attempt counter overflowed.
253        row_id: RowId,
254    },
255    /// The operating system could not provide a fresh claim token.
256    #[error("operating-system entropy unavailable: {source}")]
257    EntropyUnavailable {
258        #[source]
259        /// Original entropy error.
260        source: getrandom::Error,
261    },
262    /// Stored or returned data could not be represented safely.
263    #[error("serialization: {detail}")]
264    Serialization {
265        /// Diagnostic describing the invalid stored data.
266        detail: String,
267    },
268    /// Installed durable schema is incompatible with this adapter.
269    #[error("migration mismatch: {detail}")]
270    MigrationMismatch {
271        /// Diagnostic describing the incompatible schema.
272        detail: String,
273    },
274    /// The adapter's bounded busy policy is not representable by `SQLite`.
275    #[error("invalid SQLite busy configuration: {detail}")]
276    Configuration {
277        /// Diagnostic describing the invalid configuration.
278        detail: String,
279    },
280    /// A claim could not finish after the configured busy retries.
281    #[error("{operation}: busy lock exhausted after bounded retries: {source}")]
282    BusyExhausted {
283        /// Operation being performed when retries were exhausted.
284        operation: &'static str,
285        #[source]
286        /// Original underlying `SQLite` error.
287        source: sqlx::Error,
288    },
289    /// A non-busy `SQLx` operation failed.
290    #[error("{operation}: {source}")]
291    Sql {
292        /// Operation being performed when SQL failed.
293        operation: &'static str,
294        #[source]
295        /// Original underlying `SQLite` error.
296        source: sqlx::Error,
297    },
298}
299
300impl ClaimError {
301    pub(crate) const fn sql(operation: &'static str, source: sqlx::Error) -> Self {
302        Self::Sql { operation, source }
303    }
304
305    pub(crate) fn serialization(detail: impl Into<String>) -> Self {
306        Self::Serialization {
307            detail: detail.into(),
308        }
309    }
310
311    pub(crate) fn busy_source(&self) -> Option<&sqlx::Error> {
312        match self {
313            Self::Sql { source, .. } | Self::BusyExhausted { source, .. } if is_busy(source) => {
314                Some(source)
315            }
316            _ => None,
317        }
318    }
319
320    pub(crate) fn into_busy_exhausted(self) -> Self {
321        match self {
322            Self::Sql { operation, source } => Self::BusyExhausted { operation, source },
323            other => other,
324        }
325    }
326}
327
328/// Errors returned by claim-token-fenced delivery mutations.
329#[derive(Debug, Error)]
330#[non_exhaustive]
331pub enum MutationError {
332    /// No event row exists for the requested delivery.
333    #[error("event row not found")]
334    NotFound,
335    /// The requested mutation is invalid for the current delivery state.
336    #[error("illegal delivery transition from {state:?}")]
337    IllegalTransition {
338        /// Current durable delivery state.
339        state: DeliveryState,
340    },
341    /// The supplied claim token no longer owns the delivery.
342    #[error("claim was lost")]
343    LostClaim,
344    /// Installed durable schema is incompatible with this adapter.
345    #[error("migration mismatch: {detail}")]
346    MigrationMismatch {
347        /// Diagnostic describing the incompatible schema.
348        detail: String,
349    },
350    /// The adapter's bounded busy policy is not representable by `SQLite`.
351    #[error("invalid SQLite busy configuration: {detail}")]
352    Configuration {
353        /// Diagnostic describing the invalid configuration.
354        detail: String,
355    },
356    /// Stored or returned data could not be represented safely.
357    #[error("serialization: {detail}")]
358    Serialization {
359        /// Diagnostic describing the invalid stored data.
360        detail: String,
361    },
362    /// A mutation could not finish after the configured busy retries.
363    #[error("{operation}: busy lock exhausted after bounded retries: {source}")]
364    BusyExhausted {
365        /// Operation being performed when retries were exhausted.
366        operation: &'static str,
367        #[source]
368        /// Original underlying `SQLite` error.
369        source: sqlx::Error,
370    },
371    /// A non-busy `SQLx` operation failed.
372    #[error("{operation}: {source}")]
373    Sql {
374        /// Operation being performed when SQL failed.
375        operation: &'static str,
376        #[source]
377        /// Original underlying `SQLite` error.
378        source: sqlx::Error,
379    },
380}
381
382impl MutationError {
383    pub(crate) const fn sql(operation: &'static str, source: sqlx::Error) -> Self {
384        Self::Sql { operation, source }
385    }
386
387    pub(crate) fn serialization(detail: impl Into<String>) -> Self {
388        Self::Serialization {
389            detail: detail.into(),
390        }
391    }
392    pub(crate) fn into_busy_exhausted(self) -> Self {
393        match self {
394            Self::Sql { operation, source } => Self::BusyExhausted { operation, source },
395            other => other,
396        }
397    }
398    pub(crate) fn is_busy(&self) -> bool {
399        matches!(self, Self::Sql { source, .. } if is_busy(source))
400    }
401}
402
403/// Errors returned by live and snapshot paging.
404#[derive(Debug, Error)]
405#[non_exhaustive]
406pub enum PageError {
407    /// The snapshot pager has already been finished or rolled back.
408    #[error("snapshot pager is closed")]
409    Closed,
410    /// Stored or returned data could not be represented safely.
411    #[error("serialization: {detail}")]
412    Serialization {
413        /// Diagnostic describing the invalid stored data.
414        detail: String,
415    },
416    /// A page operation could not finish after the configured busy retries.
417    #[error("{operation}: busy lock exhausted after bounded retries: {source}")]
418    BusyExhausted {
419        /// Operation being performed when retries were exhausted.
420        operation: &'static str,
421        #[source]
422        /// Original underlying `SQLite` error.
423        source: sqlx::Error,
424    },
425    /// A non-busy `SQLx` operation failed.
426    #[error("{operation}: {source}")]
427    Sql {
428        /// Operation being performed when SQL failed.
429        operation: &'static str,
430        #[source]
431        /// Original underlying `SQLite` error.
432        source: sqlx::Error,
433    },
434}
435
436impl PageError {
437    pub(crate) fn sql(operation: &'static str, source: sqlx::Error) -> Self {
438        if is_busy(&source) {
439            Self::BusyExhausted { operation, source }
440        } else {
441            Self::Sql { operation, source }
442        }
443    }
444
445    pub(crate) fn serialization(detail: impl Into<String>) -> Self {
446        Self::Serialization {
447            detail: detail.into(),
448        }
449    }
450}
451
452/// Errors returned while checking the installed schema.
453#[derive(Debug, Error)]
454#[non_exhaustive]
455pub enum SchemaError {
456    /// Installed durable schema is incompatible with this adapter.
457    #[error("migration mismatch: {detail}")]
458    MigrationMismatch {
459        /// Diagnostic describing the incompatible schema.
460        detail: String,
461    },
462    /// A schema query could not finish after the configured busy wait.
463    #[error("{operation}: busy lock exhausted after bounded retries: {source}")]
464    BusyExhausted {
465        /// Operation being performed when the lock wait was exhausted.
466        operation: &'static str,
467        #[source]
468        /// Original underlying `SQLite` error.
469        source: sqlx::Error,
470    },
471    /// A non-busy `SQLx` operation failed.
472    #[error("{operation}: {source}")]
473    Sql {
474        /// Operation being performed when SQL failed.
475        operation: &'static str,
476        #[source]
477        /// Original underlying `SQLite` error.
478        source: sqlx::Error,
479    },
480}
481
482impl SchemaError {
483    pub(crate) fn sql(operation: &'static str, source: sqlx::Error) -> Self {
484        if is_busy(&source) {
485            Self::BusyExhausted { operation, source }
486        } else {
487            Self::Sql { operation, source }
488        }
489    }
490}