1use thiserror::Error;
8
9pub type Result<T> = std::result::Result<T, KitError>;
10
11#[derive(Debug, Clone, Default, PartialEq, Eq)]
12pub struct QueryErrorMetadata {
13 pub cancel_outcome: Option<String>,
14 pub cancellation_reason: Option<String>,
15 pub retryable: Option<bool>,
16 pub server_state: Option<String>,
17}
18
19#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
23pub struct QueryExecutionOutcome {
24 pub committed: bool,
25 pub committed_statements: Option<usize>,
26 pub last_commit_epoch: Option<u64>,
27 pub first_commit_statement_index: Option<usize>,
28 pub last_commit_statement_index: Option<usize>,
29 pub completed_statements: usize,
30 pub statement_index: usize,
31}
32
33#[derive(Debug, Error, Clone, PartialEq)]
35pub enum KitError {
36 #[error("validation error: {0}")]
37 Validation(String),
38 #[error("duplicate key: {0}")]
39 Duplicate(String),
40 #[error("foreign key violation: {0}")]
41 ForeignKey(String),
42 #[error("restrict violation: {0}")]
43 Restrict(String),
44 #[error("migration error: {0}")]
45 Migration(String),
46 #[error("conflict: {0}")]
47 Conflict(String),
48 #[error("trigger validation error: {0}")]
49 TriggerValidation(String),
50 #[error("storage error: {0}")]
51 Storage(String),
52 #[error("database locked: {0}")]
53 DatabaseLocked(String),
54 #[error("integrity error: {0}")]
55 Integrity(String),
56 #[error("authentication required: {0}")]
57 AuthRequired(String),
58 #[error("authentication not required: {0}")]
59 AuthNotRequired(String),
60 #[error("invalid credentials: {0}")]
61 InvalidCredentials(String),
62 #[error("permission denied: {0}")]
63 PermissionDenied(String),
64 #[error("query {query_id} cancelled: {reason}")]
65 Cancelled {
66 query_id: Box<str>,
67 reason: Box<str>,
68 outcome: Box<QueryExecutionOutcome>,
69 metadata: Box<QueryErrorMetadata>,
70 },
71 #[error("query {query_id} deadline exceeded")]
72 DeadlineExceeded {
73 query_id: Box<str>,
74 timeout_ms: Option<u64>,
75 outcome: Box<QueryExecutionOutcome>,
76 metadata: Box<QueryErrorMetadata>,
77 },
78 #[error("query id conflict: {query_id}")]
79 QueryConflict {
80 query_id: String,
81 metadata: Box<QueryErrorMetadata>,
82 },
83 #[error("SQL query registry full: {message}")]
84 QueryRegistryFull {
85 query_id: Option<String>,
86 message: Box<str>,
87 metadata: Box<QueryErrorMetadata>,
88 },
89 #[error("query {query_id} commit outcome {code}: {message}")]
90 CommitOutcome {
91 query_id: String,
92 code: Box<str>,
93 outcome: Box<QueryExecutionOutcome>,
94 message: Box<str>,
95 metadata: Box<QueryErrorMetadata>,
96 },
97 #[error("query {query_id} failed ({code}): {message}")]
98 QueryFailed {
99 query_id: String,
100 code: Box<str>,
101 outcome: Box<QueryExecutionOutcome>,
102 message: Box<str>,
103 metadata: Box<QueryErrorMetadata>,
104 },
105 #[error("remote protocol error {code} (HTTP {status}): {message}")]
106 RemoteProtocol {
107 status: u16,
108 code: Box<str>,
109 query_id: Option<String>,
110 message: Box<str>,
111 metadata: Box<QueryErrorMetadata>,
112 },
113 #[error("query {query_id:?} result limit exceeded: {message}")]
114 ResultLimitExceeded {
115 query_id: Option<Box<str>>,
116 max_rows: Option<Box<usize>>,
117 max_bytes: Option<Box<usize>>,
118 outcome: Box<QueryExecutionOutcome>,
119 message: Box<str>,
120 metadata: Box<QueryErrorMetadata>,
121 },
122 #[error("query {query_id:?} serialization failed: {message}")]
123 SerializationFailed {
124 query_id: Option<String>,
125 outcome: Box<QueryExecutionOutcome>,
126 message: Box<str>,
127 metadata: Box<QueryErrorMetadata>,
128 },
129 #[error("query {query_id} outcome unknown: {message}")]
130 OutcomeUnknown {
131 query_id: String,
132 message: String,
133 metadata: Box<QueryErrorMetadata>,
134 },
135 #[error("transaction aborted: {message}")]
136 TransactionAborted {
137 query_id: Option<String>,
138 message: String,
139 metadata: Box<QueryErrorMetadata>,
140 },
141 #[error("unsupported feature: {0}")]
142 Unsupported(String),
143 #[error("capability unsupported: {0}")]
144 CapabilityUnsupported(String),
145 #[error("transport error for query {query_id}: {message}")]
146 Transport {
147 query_id: String,
148 message: String,
149 metadata: Box<QueryErrorMetadata>,
150 },
151}
152
153impl KitError {
154 pub fn query_outcome(&self) -> Option<&QueryExecutionOutcome> {
155 match self {
156 Self::Cancelled { outcome, .. }
157 | Self::DeadlineExceeded { outcome, .. }
158 | Self::CommitOutcome { outcome, .. }
159 | Self::QueryFailed { outcome, .. }
160 | Self::ResultLimitExceeded { outcome, .. }
161 | Self::SerializationFailed { outcome, .. } => Some(outcome),
162 _ => None,
163 }
164 }
165
166 pub fn query_metadata(&self) -> Option<&QueryErrorMetadata> {
167 match self {
168 Self::Cancelled { metadata, .. }
169 | Self::DeadlineExceeded { metadata, .. }
170 | Self::QueryConflict { metadata, .. }
171 | Self::QueryRegistryFull { metadata, .. }
172 | Self::CommitOutcome { metadata, .. }
173 | Self::QueryFailed { metadata, .. }
174 | Self::RemoteProtocol { metadata, .. }
175 | Self::ResultLimitExceeded { metadata, .. }
176 | Self::SerializationFailed { metadata, .. }
177 | Self::OutcomeUnknown { metadata, .. }
178 | Self::TransactionAborted { metadata, .. }
179 | Self::Transport { metadata, .. } => Some(metadata),
180 _ => None,
181 }
182 }
183
184 fn query_metadata_mut(&mut self) -> Option<&mut QueryErrorMetadata> {
185 match self {
186 Self::Cancelled { metadata, .. }
187 | Self::DeadlineExceeded { metadata, .. }
188 | Self::QueryConflict { metadata, .. }
189 | Self::QueryRegistryFull { metadata, .. }
190 | Self::CommitOutcome { metadata, .. }
191 | Self::QueryFailed { metadata, .. }
192 | Self::RemoteProtocol { metadata, .. }
193 | Self::ResultLimitExceeded { metadata, .. }
194 | Self::SerializationFailed { metadata, .. }
195 | Self::OutcomeUnknown { metadata, .. }
196 | Self::TransactionAborted { metadata, .. }
197 | Self::Transport { metadata, .. } => Some(metadata),
198 _ => None,
199 }
200 }
201}
202
203pub(crate) fn boxed_query_metadata(
204 cancel_outcome: Option<&str>,
205 cancellation_reason: Option<&str>,
206 retryable: Option<bool>,
207 server_state: Option<&str>,
208) -> Box<QueryErrorMetadata> {
209 Box::new(QueryErrorMetadata {
210 cancel_outcome: cancel_outcome.map(str::to_owned),
211 cancellation_reason: cancellation_reason.map(str::to_owned),
212 retryable,
213 server_state: server_state.map(str::to_owned),
214 })
215}
216
217impl From<std::io::Error> for KitError {
218 fn from(e: std::io::Error) -> Self {
219 KitError::Storage(e.to_string())
220 }
221}
222
223impl From<mongreldb_core::MongrelError> for KitError {
224 fn from(e: mongreldb_core::MongrelError) -> Self {
225 use mongreldb_core::MongrelError;
226 match e {
227 MongrelError::TriggerValidation(msg) => KitError::TriggerValidation(msg),
228 MongrelError::Conflict(msg) => KitError::Conflict(msg),
229 MongrelError::InvalidArgument(msg) => KitError::Validation(msg),
230 MongrelError::Schema(msg) => KitError::Validation(msg),
231 MongrelError::ColumnNotFound(msg) => KitError::Integrity(msg),
232 MongrelError::NotFound(msg) => KitError::Integrity(msg),
233 MongrelError::Io(e) => KitError::Storage(e.to_string()),
234 MongrelError::DatabaseLocked { .. } => KitError::DatabaseLocked(e.to_string()),
235 MongrelError::Serialization(e) => KitError::Storage(e.to_string()),
236 MongrelError::ChecksumMismatch { .. }
237 | MongrelError::MagicMismatch { .. }
238 | MongrelError::CorruptWal { .. }
239 | MongrelError::TornWrite { .. } => KitError::Integrity(e.to_string()),
240 MongrelError::EncryptionDisabled
241 | MongrelError::Encryption(_)
242 | MongrelError::Decryption(_) => KitError::Integrity(e.to_string()),
243 MongrelError::Full(msg) => KitError::Storage(msg),
244 MongrelError::Other(msg) => KitError::Storage(msg),
245 MongrelError::AuthRequired => KitError::AuthRequired(e.to_string()),
246 MongrelError::AuthNotRequired => KitError::AuthNotRequired(e.to_string()),
247 MongrelError::InvalidCredentials { username } => KitError::InvalidCredentials(username),
248 MongrelError::PermissionDenied {
249 required,
250 principal,
251 } => KitError::PermissionDenied(format!("{principal} lacks {required}")),
252 MongrelError::DurableCommit { epoch, message } => KitError::CommitOutcome {
253 query_id: "unknown".into(),
254 code: "COMMIT_OUTCOME".into(),
255 outcome: Box::new(QueryExecutionOutcome {
256 committed: true,
257 committed_statements: None,
258 last_commit_epoch: Some(epoch),
259 first_commit_statement_index: None,
260 last_commit_statement_index: None,
261 completed_statements: 0,
262 statement_index: 0,
263 }),
264 message: message.into_boxed_str(),
265 metadata: boxed_query_metadata(None, None, Some(false), None),
266 },
267 MongrelError::CommitOutcomeUnknown { message, .. } => KitError::OutcomeUnknown {
268 query_id: "unknown".into(),
269 message,
270 metadata: boxed_query_metadata(None, None, Some(false), None),
271 },
272 _ => KitError::Storage(e.to_string()),
273 }
274 }
275}
276
277impl From<mongreldb_kit_core::schema::SchemaError> for KitError {
278 fn from(e: mongreldb_kit_core::schema::SchemaError) -> Self {
279 KitError::Validation(e.to_string())
280 }
281}
282
283impl From<mongreldb_kit_core::validation::ValidationError> for KitError {
284 fn from(e: mongreldb_kit_core::validation::ValidationError) -> Self {
285 KitError::Validation(e.to_string())
286 }
287}
288
289impl From<mongreldb_kit_core::planner::PlannerError> for KitError {
290 fn from(e: mongreldb_kit_core::planner::PlannerError) -> Self {
291 match e {
292 mongreldb_kit_core::planner::PlannerError::TableNotFound(msg) => {
293 KitError::Integrity(msg)
294 }
295 mongreldb_kit_core::planner::PlannerError::CircularDelete(msg) => {
296 KitError::Restrict(msg)
297 }
298 }
299 }
300}
301
302impl From<serde_json::Error> for KitError {
303 fn from(e: serde_json::Error) -> Self {
304 KitError::Storage(e.to_string())
305 }
306}
307
308impl From<mongreldb_query::MongrelQueryError> for KitError {
309 fn from(e: mongreldb_query::MongrelQueryError) -> Self {
310 use mongreldb_query::MongrelQueryError;
311 match e {
312 MongrelQueryError::Core(core) => KitError::from(core),
316 MongrelQueryError::Schema(msg) => KitError::Validation(msg),
317 MongrelQueryError::Arrow(msg) | MongrelQueryError::DataFusion(msg) => {
318 KitError::Storage(msg)
319 }
320 MongrelQueryError::QueryCancelled {
321 query_id,
322 reason,
323 committed,
324 committed_statements,
325 last_commit_epoch,
326 first_commit_statement_index,
327 last_commit_statement_index,
328 completed_statements,
329 cancelled_statement_index,
330 } => KitError::Cancelled {
331 query_id: query_id.to_string().into_boxed_str(),
332 reason: cancellation_reason_name(reason).into(),
333 outcome: Box::new(QueryExecutionOutcome {
334 committed,
335 committed_statements: Some(committed_statements),
336 last_commit_epoch,
337 first_commit_statement_index,
338 last_commit_statement_index,
339 completed_statements,
340 statement_index: cancelled_statement_index,
341 }),
342 metadata: boxed_query_metadata(
343 None,
344 Some(cancellation_reason_name(reason)),
345 Some(false),
346 None,
347 ),
348 },
349 MongrelQueryError::DeadlineExceeded {
350 query_id,
351 timeout_ms,
352 committed,
353 committed_statements,
354 last_commit_epoch,
355 first_commit_statement_index,
356 last_commit_statement_index,
357 completed_statements,
358 cancelled_statement_index,
359 } => KitError::DeadlineExceeded {
360 query_id: query_id.to_string().into_boxed_str(),
361 timeout_ms,
362 outcome: Box::new(QueryExecutionOutcome {
363 committed,
364 committed_statements: Some(committed_statements),
365 last_commit_epoch,
366 first_commit_statement_index,
367 last_commit_statement_index,
368 completed_statements,
369 statement_index: cancelled_statement_index,
370 }),
371 metadata: boxed_query_metadata(None, Some("deadline"), Some(false), None),
372 },
373 MongrelQueryError::QueryIdConflict { query_id } => KitError::QueryConflict {
374 query_id: query_id.to_string(),
375 metadata: boxed_query_metadata(None, None, Some(false), None),
376 },
377 MongrelQueryError::QueryRegistryFull => KitError::QueryRegistryFull {
378 query_id: None,
379 message: "SQL query registry is full".into(),
380 metadata: boxed_query_metadata(None, None, Some(true), None),
381 },
382 MongrelQueryError::CommitOutcome {
383 query_id,
384 committed,
385 committed_statements,
386 last_commit_epoch,
387 first_commit_statement_index,
388 last_commit_statement_index,
389 completed_statements,
390 statement_index,
391 message,
392 } => KitError::CommitOutcome {
393 query_id: query_id.to_string(),
394 code: "COMMIT_OUTCOME".into(),
395 outcome: Box::new(QueryExecutionOutcome {
396 committed,
397 committed_statements: Some(committed_statements),
398 last_commit_epoch,
399 first_commit_statement_index,
400 last_commit_statement_index,
401 completed_statements,
402 statement_index,
403 }),
404 message: message.into_boxed_str(),
405 metadata: boxed_query_metadata(None, None, Some(false), None),
406 },
407 MongrelQueryError::ResultLimitExceeded {
408 query_id,
409 committed,
410 committed_statements,
411 last_commit_epoch,
412 first_commit_statement_index,
413 last_commit_statement_index,
414 completed_statements,
415 statement_index,
416 message,
417 } => KitError::ResultLimitExceeded {
418 query_id: Some(query_id.to_string().into_boxed_str()),
419 max_rows: None,
420 max_bytes: None,
421 outcome: Box::new(QueryExecutionOutcome {
422 committed,
423 committed_statements: Some(committed_statements),
424 last_commit_epoch,
425 first_commit_statement_index,
426 last_commit_statement_index,
427 completed_statements,
428 statement_index,
429 }),
430 message: message.into_boxed_str(),
431 metadata: boxed_query_metadata(None, None, Some(false), None),
432 },
433 MongrelQueryError::OutcomeUnknown { query_id, message } => KitError::OutcomeUnknown {
434 query_id: query_id.to_string(),
435 message,
436 metadata: boxed_query_metadata(None, None, Some(false), None),
437 },
438 MongrelQueryError::TransactionAborted => KitError::TransactionAborted {
439 query_id: None,
440 message: "ROLLBACK or ROLLBACK TO SAVEPOINT is required".into(),
441 metadata: boxed_query_metadata(None, None, Some(false), None),
442 },
443 _ => KitError::Storage(e.to_string()),
444 }
445 }
446}
447
448fn cancellation_reason_name(reason: mongreldb_core::CancellationReason) -> &'static str {
449 use mongreldb_core::CancellationReason;
450 match reason {
451 CancellationReason::None => "none",
452 CancellationReason::ClientRequest => "client_request",
453 CancellationReason::Deadline => "deadline",
454 CancellationReason::ClientDisconnected => "client_disconnected",
455 CancellationReason::SessionClosed => "session_closed",
456 CancellationReason::ServerShutdown => "server_shutdown",
457 }
458}
459
460fn query_phase_name(phase: mongreldb_query::SqlQueryPhase) -> &'static str {
461 use mongreldb_query::SqlQueryPhase;
462 match phase {
463 SqlQueryPhase::Queued => "queued",
464 SqlQueryPhase::Planning => "planning",
465 SqlQueryPhase::Executing => "executing",
466 SqlQueryPhase::Streaming => "streaming",
467 SqlQueryPhase::Serializing => "serializing",
468 SqlQueryPhase::CommitCritical => "commit_critical",
469 SqlQueryPhase::Cancelling => "cancelling",
470 SqlQueryPhase::Completed => "completed",
471 SqlQueryPhase::Failed => "failed",
472 SqlQueryPhase::Cancelled => "cancelled",
473 }
474}
475
476fn execution_outcome_from_status(status: &mongreldb_query::QueryStatus) -> QueryExecutionOutcome {
477 QueryExecutionOutcome {
478 committed: status.durable_outcome.committed,
479 committed_statements: Some(status.durable_outcome.committed_statements),
480 last_commit_epoch: status.durable_outcome.last_commit_epoch,
481 first_commit_statement_index: status.durable_outcome.first_commit_statement_index,
482 last_commit_statement_index: status.durable_outcome.last_commit_statement_index,
483 completed_statements: status.completed_statements,
484 statement_index: status.statement_index,
485 }
486}
487
488fn merge_execution_outcome(
489 mut outcome: QueryExecutionOutcome,
490 status: Option<&mongreldb_query::QueryStatus>,
491) -> QueryExecutionOutcome {
492 let Some(status) = status else {
493 return outcome;
494 };
495 let current = execution_outcome_from_status(status);
496 outcome.committed |= current.committed;
497 outcome.committed_statements =
498 merge_count(outcome.committed_statements, current.committed_statements);
499 outcome.last_commit_epoch = current.last_commit_epoch.or(outcome.last_commit_epoch);
500 outcome.first_commit_statement_index = current
501 .first_commit_statement_index
502 .or(outcome.first_commit_statement_index);
503 outcome.last_commit_statement_index = current
504 .last_commit_statement_index
505 .or(outcome.last_commit_statement_index);
506 outcome.completed_statements = current.completed_statements;
507 outcome.statement_index = current.statement_index;
508 outcome
509}
510
511pub(crate) fn query_error_with_status(
512 error: mongreldb_query::MongrelQueryError,
513 status: Option<&mongreldb_query::QueryStatus>,
514) -> KitError {
515 let committed = status.is_some_and(|status| status.durable_outcome.committed);
516 let query_id = status.map(|status| status.query_id.to_string());
517 let mut error = KitError::from(error);
518 if let (Some(metadata), Some(status)) = (error.query_metadata_mut(), status) {
519 metadata
520 .cancellation_reason
521 .get_or_insert_with(|| cancellation_reason_name(status.cancellation_reason).into());
522 metadata
523 .server_state
524 .get_or_insert_with(|| query_phase_name(status.phase).into());
525 }
526 if status.is_some_and(|status| status.outcome_unknown) {
527 return KitError::OutcomeUnknown {
528 query_id: query_id.unwrap_or_else(|| "unknown".into()),
529 message: error.to_string(),
530 metadata: status.map_or_else(
531 || boxed_query_metadata(None, None, Some(false), None),
532 |status| {
533 boxed_query_metadata(
534 None,
535 Some(cancellation_reason_name(status.cancellation_reason)),
536 Some(false),
537 Some(query_phase_name(status.phase)),
538 )
539 },
540 ),
541 };
542 }
543 match error {
544 KitError::Cancelled {
545 query_id,
546 reason,
547 outcome,
548 metadata,
549 } => KitError::Cancelled {
550 query_id,
551 reason,
552 outcome: Box::new(merge_execution_outcome(*outcome, status)),
553 metadata,
554 },
555 KitError::DeadlineExceeded {
556 query_id,
557 timeout_ms,
558 outcome,
559 metadata,
560 } => KitError::DeadlineExceeded {
561 query_id,
562 timeout_ms,
563 outcome: Box::new(merge_execution_outcome(*outcome, status)),
564 metadata,
565 },
566 KitError::CommitOutcome {
567 query_id: error_query_id,
568 code,
569 outcome,
570 message,
571 metadata,
572 } => KitError::CommitOutcome {
573 query_id: if error_query_id == "unknown" {
574 query_id.unwrap_or(error_query_id)
575 } else {
576 error_query_id
577 },
578 code,
579 outcome: Box::new(merge_execution_outcome(*outcome, status)),
580 message,
581 metadata,
582 },
583 KitError::ResultLimitExceeded {
584 query_id: error_query_id,
585 max_rows,
586 max_bytes,
587 outcome,
588 message,
589 metadata,
590 } => KitError::ResultLimitExceeded {
591 query_id: error_query_id.or_else(|| query_id.map(String::into_boxed_str)),
592 max_rows,
593 max_bytes,
594 outcome: Box::new(merge_execution_outcome(*outcome, status)),
595 message,
596 metadata,
597 },
598 KitError::SerializationFailed {
599 query_id: error_query_id,
600 outcome,
601 message,
602 metadata,
603 } => KitError::SerializationFailed {
604 query_id: error_query_id.or(query_id),
605 outcome: Box::new(merge_execution_outcome(*outcome, status)),
606 message,
607 metadata,
608 },
609 KitError::TransactionAborted {
610 query_id: error_query_id,
611 message,
612 metadata,
613 } => KitError::TransactionAborted {
614 query_id: error_query_id.or(query_id),
615 message,
616 metadata,
617 },
618 KitError::OutcomeUnknown {
619 query_id: error_query_id,
620 message,
621 metadata,
622 } => KitError::OutcomeUnknown {
623 query_id: if error_query_id == "unknown" {
624 query_id.unwrap_or(error_query_id)
625 } else {
626 error_query_id
627 },
628 message,
629 metadata,
630 },
631 error if committed => KitError::CommitOutcome {
632 query_id: query_id.unwrap_or_else(|| "unknown".into()),
633 code: "COMMIT_OUTCOME".into(),
634 outcome: Box::new(
635 status
636 .map(execution_outcome_from_status)
637 .unwrap_or_default(),
638 ),
639 message: error.to_string().into_boxed_str(),
640 metadata: status.map_or_else(
641 || boxed_query_metadata(None, None, Some(false), None),
642 |status| {
643 boxed_query_metadata(
644 None,
645 Some(cancellation_reason_name(status.cancellation_reason)),
646 Some(false),
647 Some(query_phase_name(status.phase)),
648 )
649 },
650 ),
651 },
652 error => error,
653 }
654}
655
656fn merge_count(left: Option<usize>, right: Option<usize>) -> Option<usize> {
657 match (left, right) {
658 (Some(left), Some(right)) => Some(left.max(right)),
659 (left, right) => left.or(right),
660 }
661}
662
663#[cfg(test)]
664mod tests {
665 use super::{query_error_with_status, KitError};
666
667 #[test]
668 fn maps_trigger_core_errors_without_message_parsing() {
669 let trigger = KitError::from(mongreldb_core::MongrelError::TriggerValidation(
670 "trigger raised".into(),
671 ));
672 assert_eq!(
673 trigger,
674 KitError::TriggerValidation("trigger raised".into())
675 );
676
677 let conflict = KitError::from(mongreldb_core::MongrelError::Conflict(
678 "unrelated conflict contains trigger wording".into(),
679 ));
680 assert_eq!(
681 conflict,
682 KitError::Conflict("unrelated conflict contains trigger wording".into())
683 );
684
685 let invalid = KitError::from(mongreldb_core::MongrelError::InvalidArgument(
686 "unrelated invalid argument mentions external trigger bridge".into(),
687 ));
688 assert_eq!(
689 invalid,
690 KitError::Validation(
691 "unrelated invalid argument mentions external trigger bridge".into()
692 )
693 );
694 }
695
696 #[test]
697 fn maps_query_registry_and_commit_outcomes_without_strings() {
698 assert!(matches!(
699 KitError::from(mongreldb_query::MongrelQueryError::QueryRegistryFull),
700 KitError::QueryRegistryFull { query_id: None, .. }
701 ));
702 let query_id = mongreldb_query::QueryId::random().unwrap();
703 assert!(matches!(
704 KitError::from(mongreldb_query::MongrelQueryError::CommitOutcome {
705 query_id,
706 committed: true,
707 committed_statements: 1,
708 last_commit_epoch: Some(7),
709 first_commit_statement_index: Some(0),
710 last_commit_statement_index: Some(0),
711 completed_statements: 1,
712 statement_index: 0,
713 message: "post-commit refresh failed".into(),
714 }),
715 KitError::CommitOutcome {
716 outcome,
717 ..
718 } if outcome.committed
719 && outcome.committed_statements == Some(1)
720 && outcome.last_commit_epoch == Some(7)
721 ));
722 }
723
724 #[test]
725 fn committed_status_wraps_unknown_post_commit_errors() {
726 let registry = std::sync::Arc::new(mongreldb_query::SqlQueryRegistry::default());
727 let query = registry
728 .register(mongreldb_query::SqlQueryOptions::default())
729 .unwrap();
730 let query_id = query.id().to_string();
731 query.record_commit(2, 7);
732 let status = query.status();
733
734 let error = query_error_with_status(
735 mongreldb_query::MongrelQueryError::Schema("refresh failed".into()),
736 Some(&status),
737 );
738 assert!(matches!(
739 error,
740 KitError::CommitOutcome {
741 query_id: actual,
742 code,
743 outcome,
744 ..
745 } if actual == query_id
746 && code.as_ref() == "COMMIT_OUTCOME"
747 && outcome.committed
748 && outcome.committed_statements == Some(1)
749 && outcome.last_commit_epoch == Some(7)
750 && outcome.first_commit_statement_index == Some(2)
751 && outcome.last_commit_statement_index == Some(2)
752 ));
753 }
754
755 #[test]
756 fn cancellation_keeps_exact_durable_receipt() {
757 let query_id = mongreldb_query::QueryId::random().unwrap();
758 let error = KitError::from(mongreldb_query::MongrelQueryError::QueryCancelled {
759 query_id,
760 reason: mongreldb_core::CancellationReason::ClientRequest,
761 committed: true,
762 committed_statements: 2,
763 last_commit_epoch: Some(19),
764 first_commit_statement_index: Some(0),
765 last_commit_statement_index: Some(3),
766 completed_statements: 3,
767 cancelled_statement_index: 4,
768 });
769 assert!(matches!(
770 error,
771 KitError::Cancelled {
772 outcome,
773 ..
774 } if outcome.committed
775 && outcome.committed_statements == Some(2)
776 && outcome.last_commit_epoch == Some(19)
777 && outcome.first_commit_statement_index == Some(0)
778 && outcome.last_commit_statement_index == Some(3)
779 && outcome.completed_statements == 3
780 && outcome.statement_index == 4
781 ));
782 }
783
784 #[test]
785 fn kit_error_stays_compact() {
786 assert!(std::mem::size_of::<KitError>() <= 128);
787 }
788}