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 fn from(error: mongreldb_core::MongrelError) -> Self {
92 Self::Core(error)
93 }
94}
95
96impl MongrelQueryError {
97 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 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 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 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}