Skip to main content

mongreldb_query/
error.rs

1use thiserror::Error;
2
3pub type Result<T> = std::result::Result<T, MongrelQueryError>;
4
5#[non_exhaustive]
6#[derive(Debug, Error)]
7pub enum MongrelQueryError {
8    #[error("mongreldb error: {0}")]
9    Core(mongreldb_core::MongrelError),
10    #[error("arrow error: {0}")]
11    Arrow(String),
12    #[error("datafusion error: {0}")]
13    DataFusion(String),
14    #[error("schema error: {0}")]
15    Schema(String),
16    #[error("query {query_id} cancelled: {reason:?}")]
17    QueryCancelled {
18        query_id: crate::QueryId,
19        reason: mongreldb_core::CancellationReason,
20        committed: bool,
21        committed_statements: usize,
22        last_commit_epoch: Option<u64>,
23        first_commit_statement_index: Option<usize>,
24        last_commit_statement_index: Option<usize>,
25        completed_statements: usize,
26        cancelled_statement_index: usize,
27    },
28    #[error("query {query_id} deadline exceeded")]
29    DeadlineExceeded {
30        query_id: crate::QueryId,
31        timeout_ms: Option<u64>,
32        committed: bool,
33        committed_statements: usize,
34        last_commit_epoch: Option<u64>,
35        first_commit_statement_index: Option<usize>,
36        last_commit_statement_index: Option<usize>,
37        completed_statements: usize,
38        cancelled_statement_index: usize,
39    },
40    #[error("query id {query_id} is already active or retained")]
41    QueryIdConflict { query_id: crate::QueryId },
42    #[error("SQL query registry is full")]
43    QueryRegistryFull,
44    #[error("query {query_id} result limit exceeded: {message}")]
45    ResultLimitExceeded {
46        query_id: crate::QueryId,
47        committed: bool,
48        committed_statements: usize,
49        last_commit_epoch: Option<u64>,
50        first_commit_statement_index: Option<usize>,
51        last_commit_statement_index: Option<usize>,
52        completed_statements: usize,
53        statement_index: usize,
54        message: String,
55    },
56    #[error("transaction is aborted; ROLLBACK or ROLLBACK TO SAVEPOINT is required")]
57    TransactionAborted,
58    #[error("no SQL transaction is open")]
59    NoSqlTransaction,
60    #[error("no savepoint named '{name}'")]
61    SavepointNotFound { name: String },
62    #[error(
63        "query {query_id} commit outcome: committed={committed}, committed_statements={committed_statements}, last_commit_epoch={last_commit_epoch:?}: {message}"
64    )]
65    CommitOutcome {
66        query_id: crate::QueryId,
67        committed: bool,
68        committed_statements: usize,
69        last_commit_epoch: Option<u64>,
70        first_commit_statement_index: Option<usize>,
71        last_commit_statement_index: Option<usize>,
72        completed_statements: usize,
73        statement_index: usize,
74        message: String,
75    },
76    #[error("query {query_id} durable outcome is unknown: {message}")]
77    OutcomeUnknown {
78        query_id: crate::QueryId,
79        message: String,
80    },
81    #[error("invalid query state: {0}")]
82    InvalidQueryState(String),
83}
84
85impl From<mongreldb_core::MongrelError> for MongrelQueryError {
86    /// Core errors arrive on their precise taxonomy variants — the commit
87    /// path raises SSI certification aborts as
88    /// `MongrelError::SerializationFailure` (category 8) natively and
89    /// `From<LockError>` bridges deadlock victims onto `MongrelError::Deadlock`
90    /// (category 9) — so the SQL boundary is a straight passthrough.
91    fn from(error: mongreldb_core::MongrelError) -> Self {
92        Self::Core(error)
93    }
94}
95
96impl MongrelQueryError {
97    /// Stable machine-readable category for language bindings and protocols.
98    pub fn code(&self) -> &'static str {
99        match self {
100            Self::Core(_) | Self::Arrow(_) | Self::DataFusion(_) | Self::Schema(_) => {
101                "SQL_EXECUTION_FAILED"
102            }
103            Self::QueryCancelled {
104                committed: true, ..
105            } => "QUERY_CANCELLED_AFTER_COMMIT",
106            Self::QueryCancelled { .. } => "QUERY_CANCELLED",
107            Self::DeadlineExceeded {
108                committed: true, ..
109            } => "DEADLINE_AFTER_COMMIT",
110            Self::DeadlineExceeded { .. } => "DEADLINE_EXCEEDED",
111            Self::QueryIdConflict { .. } => "QUERY_ID_CONFLICT",
112            Self::QueryRegistryFull => "QUERY_REGISTRY_FULL",
113            Self::ResultLimitExceeded { .. } => "RESULT_LIMIT_EXCEEDED",
114            Self::TransactionAborted => "TRANSACTION_ABORTED",
115            Self::NoSqlTransaction => "NO_SQL_TRANSACTION",
116            Self::SavepointNotFound { .. } => "SAVEPOINT_NOT_FOUND",
117            Self::CommitOutcome { .. } => "COMMIT_OUTCOME",
118            Self::OutcomeUnknown { .. } => "QUERY_OUTCOME_UNKNOWN",
119            Self::InvalidQueryState(_) => "INVALID_QUERY_STATE",
120        }
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127    use mongreldb_core::MongrelError;
128
129    #[test]
130    fn serialization_failure_passes_through_precise_with_category_8() {
131        // The exact message core's commit path constructs (database.rs); the
132        // variant now arrives native from core, so the boundary must preserve
133        // it — and with it the precise taxonomy category 8.
134        let core = MongrelError::SerializationFailure {
135            message: "a concurrent commit invalidated this transaction's reads (pre-validate)"
136                .into(),
137        };
138        let error = MongrelQueryError::from(core);
139        match error {
140            MongrelQueryError::Core(MongrelError::SerializationFailure { message }) => {
141                assert_eq!(
142                    message,
143                    "a concurrent commit invalidated this transaction's reads (pre-validate)"
144                );
145                let precise = MongrelError::SerializationFailure { message };
146                assert_eq!(precise.category().code(), 8);
147                assert_eq!(
148                    precise.to_string(),
149                    "serialization failure: a concurrent commit invalidated this transaction's reads (pre-validate)"
150                );
151            }
152            other => panic!("expected SerializationFailure, got {other:?}"),
153        }
154    }
155
156    #[test]
157    fn marker_prefixed_conflict_stays_a_plain_conflict() {
158        // No string-marker reinterpretation survives at the boundary: even a
159        // `Conflict` whose message begins with the legacy prefix keeps its
160        // variant (and the generic transaction-conflict category).
161        let core = MongrelError::Conflict("serialization failure: hand-built".into());
162        let error = MongrelQueryError::from(core);
163        assert!(
164            matches!(
165                &error,
166                MongrelQueryError::Core(MongrelError::Conflict(message))
167                    if message == "serialization failure: hand-built"
168            ),
169            "a marker-prefixed Conflict is never re-homed: {error:?}"
170        );
171    }
172
173    #[test]
174    fn non_marker_conflict_passes_through_untouched() {
175        let core = MongrelError::Conflict(
176            "write-write conflict (pre-validate, first-committer-wins)".into(),
177        );
178        let error = MongrelQueryError::from(core);
179        assert!(
180            matches!(
181                &error,
182                MongrelQueryError::Core(MongrelError::Conflict(message))
183                    if message == "write-write conflict (pre-validate, first-committer-wins)"
184            ),
185            "plain conflicts keep their variant: {error:?}"
186        );
187    }
188
189    #[test]
190    fn deadlock_passes_through_precise() {
191        // LockError::Deadlock bridges onto MongrelError::Deadlock in core;
192        // the SQL boundary must preserve the dedicated variant.
193        let core = MongrelError::from(mongreldb_core::locks::LockError::Deadlock {
194            victim: 5,
195            cycle: "5 → 2 → 5".into(),
196        });
197        let error = MongrelQueryError::from(core);
198        assert!(
199            matches!(
200                &error,
201                MongrelQueryError::Core(MongrelError::Deadlock { victim: 5, cycle })
202                    if cycle == "5 → 2 → 5"
203            ),
204            "deadlock keeps victim and cycle: {error:?}"
205        );
206    }
207
208    #[test]
209    fn already_precise_variants_pass_through() {
210        let error = MongrelQueryError::from(MongrelError::SerializationFailure {
211            message: "native".into(),
212        });
213        assert!(matches!(
214            error,
215            MongrelQueryError::Core(MongrelError::SerializationFailure { .. })
216        ));
217        let error = MongrelQueryError::from(MongrelError::Cancelled);
218        assert!(matches!(
219            error,
220            MongrelQueryError::Core(MongrelError::Cancelled)
221        ));
222    }
223}