1use std::fmt;
20
21use thiserror::Error;
22
23mod code;
24mod context;
25
26pub use code::{ErrorCategory, ErrorCode};
27pub use context::ErrorContext;
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
34pub enum NavigationErrorCode {
35 UnknownRoot,
36 AmbiguousRoot,
37 NotAReference,
38 UnsupportedReferenceShape,
39 TargetColumnNotFound,
40 ReadOnly,
41 SchemaChanged,
42 TargetMissing,
43 TargetNotUnique,
44}
45
46impl NavigationErrorCode {
47 pub const fn as_str(self) -> &'static str {
48 match self {
49 Self::UnknownRoot => "NAVIGATION_UNKNOWN_ROOT",
50 Self::AmbiguousRoot => "NAVIGATION_AMBIGUOUS_ROOT",
51 Self::NotAReference => "NAVIGATION_NOT_A_REFERENCE",
52 Self::UnsupportedReferenceShape => "NAVIGATION_UNSUPPORTED_REFERENCE_SHAPE",
53 Self::TargetColumnNotFound => "NAVIGATION_TARGET_COLUMN_NOT_FOUND",
54 Self::ReadOnly => "NAVIGATION_READ_ONLY",
55 Self::SchemaChanged => "NAVIGATION_SCHEMA_CHANGED",
56 Self::TargetMissing => "REFERENCE_TARGET_MISSING",
57 Self::TargetNotUnique => "REFERENCE_TARGET_NOT_UNIQUE",
58 }
59 }
60}
61
62impl fmt::Display for NavigationErrorCode {
63 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64 f.write_str(self.as_str())
65 }
66}
67
68pub type Result<T> = std::result::Result<T, Error>;
70
71#[derive(Error, Debug, Clone, PartialEq, Eq)]
76pub enum Error {
77 #[error("table '{0}' not found")]
82 TableNotFound(String),
83
84 #[error("table '{0}' already exists")]
86 TableAlreadyExists(String),
87
88 #[error("table closed")]
90 TableClosed,
91
92 #[error("table columns don't match, expected {expected}, got {got}")]
94 TableColumnsNotMatch { expected: usize, got: usize },
95
96 #[error("cannot truncate table: active transactions have uncommitted changes")]
98 TableHasActiveTransactions,
99
100 #[error("column '{0}' not found")]
105 ColumnNotFound(String),
106
107 #[error("column '{0}' is ambiguous")]
109 AmbiguousColumn(String),
110
111 #[error("invalid column type")]
113 InvalidColumnType,
114
115 #[error("Vector dimension mismatch: expected {expected}, got {got}")]
117 VectorDimensionMismatch { expected: u16, got: u16 },
118
119 #[error("duplicate column")]
121 DuplicateColumn,
122
123 #[error("invalid value")]
128 InvalidValue,
129
130 #[error("invalid argument: {0}")]
132 InvalidArgument(String),
133
134 #[error("authorization denied: {0}")]
136 AuthorizationDenied(String),
137
138 #[error("value for column {column} is too long, max {max}, got {got}")]
140 ValueTooLong {
141 column: String,
142 max: usize,
143 got: usize,
144 },
145
146 #[error("not null constraint failed for column {column}")]
151 NotNullConstraint { column: String },
152
153 #[error("primary key constraint failed with {row_id} already exists in this table")]
155 PrimaryKeyConstraint { row_id: i64 },
156
157 #[error("unique constraint failed for index {index} on column {column} with value {value}")]
159 UniqueConstraint {
160 index: String,
161 column: String,
162 value: String,
163 row_id: i64,
165 },
166
167 #[error("CHECK constraint failed for column {column}: {expression}")]
169 CheckConstraintViolation { column: String, expression: String },
170
171 #[error("foreign key constraint violation: column '{column}' in table '{table}' references '{ref_table}({ref_column})' — {detail}")]
173 ForeignKeyViolation {
174 table: String,
175 column: String,
176 ref_table: String,
177 ref_column: String,
178 detail: String,
179 },
180
181 #[error("transaction not started")]
186 TransactionNotStarted,
187
188 #[error("transaction already started")]
190 TransactionAlreadyStarted,
191
192 #[error("transaction already ended")]
194 TransactionEnded,
195
196 #[error("transaction aborted")]
198 TransactionAborted,
199
200 #[error("transaction already committed")]
202 TransactionCommitted,
203
204 #[error("transaction already closed")]
206 TransactionClosed,
207
208 #[error("invalid transaction {txn_id} transition: expected {expected}, found {actual}")]
211 InvalidTransactionTransition {
212 txn_id: i64,
213 expected: String,
214 actual: String,
215 },
216
217 #[error(
219 "transaction serialization conflict while acquiring row {row_id}; retry the transaction"
220 )]
221 TransactionSerializationConflict { row_id: i64 },
222
223 #[error("transaction timed out while waiting to update row {row_id} after {timeout_ms} ms")]
225 RowLockTimeout { row_id: i64, timeout_ms: u64 },
226
227 #[error(
230 "COMPACTION_BACKPRESSURE: table '{table}' has L0 debt {segments} segments/{physical_bytes} bytes; hard limit {hard_segments} segments/{hard_bytes} bytes; retry after compaction"
231 )]
232 CompactionBackpressure {
233 table: String,
234 segments: u64,
235 physical_bytes: u64,
236 hard_segments: u64,
237 hard_bytes: u64,
238 },
239
240 #[error("index '{0}' not found")]
245 IndexNotFound(String),
246
247 #[error("index '{0}' already exists")]
249 IndexAlreadyExists(String),
250
251 #[error("index column not found")]
253 IndexColumnNotFound,
254
255 #[error("index is closed")]
257 IndexClosed,
258
259 #[error("engine is not open")]
264 EngineNotOpen,
265
266 #[error("engine is already open")]
268 EngineAlreadyOpen,
269
270 #[error("view '{0}' already exists")]
275 ViewAlreadyExists(String),
276
277 #[error("view '{0}' not found")]
279 ViewNotFound(String),
280
281 #[error("failed to acquire lock: {0}")]
286 LockAcquisitionFailed(String),
287
288 #[error("query returned no rows")]
293 NoRowsReturned,
294
295 #[error("no statements to execute")]
297 NoStatementsToExecute,
298
299 #[error("column index {index} out of bounds")]
301 ColumnIndexOutOfBounds { index: usize },
302
303 #[error("cursor is not positioned on a row")]
306 CursorNotPositioned,
307
308 #[error("WAL manager is not running")]
313 WalNotRunning,
314
315 #[error("WAL file is closed")]
317 WalFileClosed,
318
319 #[error("WAL durability outcome is uncertain: {detail}")]
323 WalDurabilityUncertain { detail: String },
324
325 #[error("{operation} failed after committing {committed_rows} rows: {cause}")]
328 PartialCommit {
329 operation: String,
330 committed_rows: i64,
331 cause: String,
332 },
333
334 #[error(
336 "COPY transaction memory budget exceeded at row {row}: limit {limit_bytes} bytes, attempted {attempted_bytes} bytes"
337 )]
338 CopyTransactionMemoryLimit {
339 row: i64,
340 limit_bytes: usize,
341 attempted_bytes: usize,
342 },
343
344 #[error("WAL not initialized")]
346 WalNotInitialized,
347
348 #[error("database is locked by another process")]
353 DatabaseLocked,
354
355 #[error("cannot drop primary key column")]
357 CannotDropPrimaryKey,
358
359 #[error("cannot compare NULL with non-NULL value")]
364 NullComparison,
365
366 #[error("cannot compare incompatible types")]
368 IncomparableTypes,
369
370 #[error("not supported: {0}")]
375 NotSupported(String),
376
377 #[error("native function '{function}' failed with plugin status {status}")]
380 NativeFunction { function: String, status: u32 },
381
382 #[error("{code}: {detail}")]
384 Navigation {
385 code: NavigationErrorCode,
386 detail: String,
387 },
388
389 #[error("segment not found")]
391 SegmentNotFound,
392
393 #[error("expression evaluation failed")]
395 ExpressionEvaluation,
396
397 #[error("expression evaluation failed: {message}")]
399 ExpressionEvaluationWithMessage { message: String },
400
401 #[error("type conversion error: cannot convert {from} to {to}")]
403 TypeConversion { from: String, to: String },
404
405 #[error("parse error: {0}")]
407 Parse(String),
408
409 #[error("IO error: {message}")]
411 Io { message: String },
412
413 #[error("{message}")]
415 Internal { message: String },
416
417 #[error("table or view '{0}' not found")]
422 TableOrViewNotFound(String),
423
424 #[error("type error: {0}")]
426 Type(String),
427
428 #[error("division by zero")]
430 DivisionByZero,
431
432 #[error("query cancelled")]
434 QueryCancelled,
435}
436
437impl Error {
438 pub fn code(&self) -> ErrorCode {
440 let code = match self {
441 Self::TableNotFound(_) => "TABLE_NOT_FOUND",
442 Self::TableAlreadyExists(_) => "TABLE_ALREADY_EXISTS",
443 Self::TableClosed => "TABLE_CLOSED",
444 Self::TableColumnsNotMatch { .. } => "TABLE_COLUMNS_NOT_MATCH",
445 Self::TableHasActiveTransactions => "TABLE_HAS_ACTIVE_TRANSACTIONS",
446 Self::ColumnNotFound(_) => "COLUMN_NOT_FOUND",
447 Self::AmbiguousColumn(_) => "AMBIGUOUS_COLUMN",
448 Self::InvalidColumnType => "INVALID_COLUMN_TYPE",
449 Self::VectorDimensionMismatch { .. } => "VECTOR_DIMENSION_MISMATCH",
450 Self::DuplicateColumn => "DUPLICATE_COLUMN",
451 Self::InvalidValue => "INVALID_VALUE",
452 Self::InvalidArgument(_) => "INVALID_ARGUMENT",
453 Self::AuthorizationDenied(_) => "AUTHORIZATION_DENIED",
454 Self::ValueTooLong { .. } => "VALUE_TOO_LONG",
455 Self::NotNullConstraint { .. } => "NOT_NULL_CONSTRAINT",
456 Self::PrimaryKeyConstraint { .. } => "PRIMARY_KEY_CONSTRAINT",
457 Self::UniqueConstraint { .. } => "UNIQUE_CONSTRAINT",
458 Self::CheckConstraintViolation { .. } => "CHECK_CONSTRAINT",
459 Self::ForeignKeyViolation { .. } => "FOREIGN_KEY_CONSTRAINT",
460 Self::TransactionNotStarted => "TRANSACTION_NOT_STARTED",
461 Self::TransactionAlreadyStarted => "TRANSACTION_ALREADY_STARTED",
462 Self::TransactionEnded => "TRANSACTION_ENDED",
463 Self::TransactionAborted => "TRANSACTION_ABORTED",
464 Self::TransactionCommitted => "TRANSACTION_COMMITTED",
465 Self::TransactionClosed => "TRANSACTION_CLOSED",
466 Self::InvalidTransactionTransition { .. } => "INVALID_TRANSACTION_TRANSITION",
467 Self::TransactionSerializationConflict { .. } => "TRANSACTION_SERIALIZATION_CONFLICT",
468 Self::RowLockTimeout { .. } => "ROW_LOCK_TIMEOUT",
469 Self::CompactionBackpressure { .. } => "COMPACTION_BACKPRESSURE",
470 Self::IndexNotFound(_) => "INDEX_NOT_FOUND",
471 Self::IndexAlreadyExists(_) => "INDEX_ALREADY_EXISTS",
472 Self::IndexColumnNotFound => "INDEX_COLUMN_NOT_FOUND",
473 Self::IndexClosed => "INDEX_CLOSED",
474 Self::EngineNotOpen => "ENGINE_NOT_OPEN",
475 Self::EngineAlreadyOpen => "ENGINE_ALREADY_OPEN",
476 Self::ViewAlreadyExists(_) => "VIEW_ALREADY_EXISTS",
477 Self::ViewNotFound(_) => "VIEW_NOT_FOUND",
478 Self::LockAcquisitionFailed(_) => "LOCK_ACQUISITION_FAILED",
479 Self::NoRowsReturned => "NO_ROWS_RETURNED",
480 Self::NoStatementsToExecute => "NO_STATEMENTS_TO_EXECUTE",
481 Self::ColumnIndexOutOfBounds { .. } => "COLUMN_INDEX_OUT_OF_BOUNDS",
482 Self::CursorNotPositioned => "CURSOR_NOT_POSITIONED",
483 Self::WalNotRunning => "WAL_NOT_RUNNING",
484 Self::WalFileClosed => "WAL_FILE_CLOSED",
485 Self::WalDurabilityUncertain { .. } => "WAL_DURABILITY_UNCERTAIN",
486 Self::PartialCommit { .. } => "PARTIAL_COMMIT",
487 Self::CopyTransactionMemoryLimit { .. } => "COPY_TRANSACTION_MEMORY_LIMIT",
488 Self::WalNotInitialized => "WAL_NOT_INITIALIZED",
489 Self::DatabaseLocked => "DATABASE_LOCKED",
490 Self::CannotDropPrimaryKey => "CANNOT_DROP_PRIMARY_KEY",
491 Self::NullComparison => "NULL_COMPARISON",
492 Self::IncomparableTypes => "INCOMPARABLE_TYPES",
493 Self::NotSupported(_) => "NOT_SUPPORTED",
494 Self::NativeFunction { .. } => "NATIVE_FUNCTION_ERROR",
495 Self::Navigation { code, .. } => code.as_str(),
496 Self::SegmentNotFound => "SEGMENT_NOT_FOUND",
497 Self::ExpressionEvaluation => "EXPRESSION_EVALUATION",
498 Self::ExpressionEvaluationWithMessage { .. } => "EXPRESSION_EVALUATION",
499 Self::TypeConversion { .. } => "TYPE_CONVERSION",
500 Self::Parse(_) => "PARSE_ERROR",
501 Self::Io { .. } => "IO_ERROR",
502 Self::Internal { .. } => "INTERNAL_ERROR",
503 Self::TableOrViewNotFound(_) => "TABLE_OR_VIEW_NOT_FOUND",
504 Self::Type(_) => "TYPE_ERROR",
505 Self::DivisionByZero => "DIVISION_BY_ZERO",
506 Self::QueryCancelled => "QUERY_CANCELLED",
507 };
508 ErrorCode::new(code)
509 }
510
511 pub fn context(&self) -> ErrorContext {
513 use ErrorCategory as Category;
514
515 let category = match self {
516 Self::TableNotFound(_)
517 | Self::TableAlreadyExists(_)
518 | Self::TableClosed
519 | Self::TableColumnsNotMatch { .. }
520 | Self::TableHasActiveTransactions
521 | Self::ColumnNotFound(_)
522 | Self::AmbiguousColumn(_)
523 | Self::DuplicateColumn
524 | Self::ViewAlreadyExists(_)
525 | Self::ViewNotFound(_)
526 | Self::CannotDropPrimaryKey
527 | Self::Navigation { .. }
528 | Self::TableOrViewNotFound(_) => Category::Catalog,
529 Self::InvalidColumnType
530 | Self::VectorDimensionMismatch { .. }
531 | Self::InvalidValue
532 | Self::InvalidArgument(_)
533 | Self::ValueTooLong { .. }
534 | Self::NullComparison
535 | Self::IncomparableTypes
536 | Self::TypeConversion { .. }
537 | Self::Type(_) => Category::Value,
538 Self::AuthorizationDenied(_) => Category::Security,
539 Self::NotNullConstraint { .. }
540 | Self::PrimaryKeyConstraint { .. }
541 | Self::UniqueConstraint { .. }
542 | Self::CheckConstraintViolation { .. }
543 | Self::ForeignKeyViolation { .. } => Category::Constraint,
544 Self::TransactionNotStarted
545 | Self::TransactionAlreadyStarted
546 | Self::TransactionEnded
547 | Self::TransactionAborted
548 | Self::TransactionCommitted
549 | Self::TransactionClosed
550 | Self::InvalidTransactionTransition { .. }
551 | Self::TransactionSerializationConflict { .. }
552 | Self::RowLockTimeout { .. }
553 | Self::CompactionBackpressure { .. }
554 | Self::PartialCommit { .. }
555 | Self::CopyTransactionMemoryLimit { .. } => Category::Transaction,
556 Self::IndexNotFound(_)
557 | Self::IndexAlreadyExists(_)
558 | Self::IndexColumnNotFound
559 | Self::IndexClosed => Category::Index,
560 Self::EngineNotOpen | Self::EngineAlreadyOpen | Self::LockAcquisitionFailed(_) => {
561 Category::Engine
562 }
563 Self::NoRowsReturned
564 | Self::NoStatementsToExecute
565 | Self::ColumnIndexOutOfBounds { .. }
566 | Self::CursorNotPositioned
567 | Self::NotSupported(_)
568 | Self::NativeFunction { .. }
569 | Self::QueryCancelled => Category::Query,
570 Self::WalNotRunning
571 | Self::WalFileClosed
572 | Self::WalDurabilityUncertain { .. }
573 | Self::WalNotInitialized
574 | Self::SegmentNotFound => Category::Durability,
575 Self::DatabaseLocked => Category::Database,
576 Self::ExpressionEvaluation
577 | Self::ExpressionEvaluationWithMessage { .. }
578 | Self::DivisionByZero => Category::Evaluation,
579 Self::Parse(_) => Category::Syntax,
580 Self::Io { .. } => Category::Io,
581 Self::Internal { .. } => Category::Internal,
582 };
583 ErrorContext::new(
584 self.code(),
585 category,
586 self.is_retryable(),
587 self.is_not_found(),
588 self.is_constraint_violation(),
589 )
590 }
591
592 pub fn table_columns_not_match(expected: usize, got: usize) -> Self {
594 Error::TableColumnsNotMatch { expected, got }
595 }
596
597 pub fn value_too_long(column: impl Into<String>, max: usize, got: usize) -> Self {
599 Error::ValueTooLong {
600 column: column.into(),
601 max,
602 got,
603 }
604 }
605
606 pub fn not_null_constraint(column: impl Into<String>) -> Self {
608 Error::NotNullConstraint {
609 column: column.into(),
610 }
611 }
612
613 pub fn primary_key_constraint(row_id: i64) -> Self {
615 Error::PrimaryKeyConstraint { row_id }
616 }
617
618 pub fn unique_constraint(
620 index: impl Into<String>,
621 column: impl Into<String>,
622 value: impl Into<String>,
623 ) -> Self {
624 Error::UniqueConstraint {
625 index: index.into(),
626 column: column.into(),
627 value: value.into(),
628 row_id: -1,
629 }
630 }
631
632 pub fn foreign_key_violation(
634 table: impl Into<String>,
635 column: impl Into<String>,
636 ref_table: impl Into<String>,
637 ref_column: impl Into<String>,
638 detail: impl Into<String>,
639 ) -> Self {
640 Error::ForeignKeyViolation {
641 table: table.into(),
642 column: column.into(),
643 ref_table: ref_table.into(),
644 ref_column: ref_column.into(),
645 detail: detail.into(),
646 }
647 }
648
649 pub fn type_conversion(from: impl Into<String>, to: impl Into<String>) -> Self {
651 Error::TypeConversion {
652 from: from.into(),
653 to: to.into(),
654 }
655 }
656
657 pub fn parse(message: impl Into<String>) -> Self {
659 Error::Parse(message.into())
660 }
661
662 pub fn io(message: impl Into<String>) -> Self {
664 Error::Io {
665 message: message.into(),
666 }
667 }
668
669 pub fn internal(message: impl Into<String>) -> Self {
671 Error::Internal {
672 message: message.into(),
673 }
674 }
675
676 pub fn expression_evaluation(message: impl Into<String>) -> Self {
678 Error::ExpressionEvaluationWithMessage {
679 message: message.into(),
680 }
681 }
682
683 pub fn invalid_argument(message: impl Into<String>) -> Self {
685 Error::InvalidArgument(message.into())
686 }
687
688 pub fn authorization_denied(message: impl Into<String>) -> Self {
691 Error::AuthorizationDenied(message.into())
692 }
693
694 pub fn navigation(code: NavigationErrorCode, detail: impl Into<String>) -> Self {
696 Error::Navigation {
697 code,
698 detail: detail.into(),
699 }
700 }
701
702 pub fn navigation_code(&self) -> Option<NavigationErrorCode> {
704 match self {
705 Error::Navigation { code, .. } => Some(*code),
706 _ => None,
707 }
708 }
709
710 pub fn is_not_found(&self) -> bool {
712 matches!(
713 self,
714 Error::TableNotFound(_)
715 | Error::ColumnNotFound(_)
716 | Error::IndexNotFound(_)
717 | Error::IndexColumnNotFound
718 | Error::SegmentNotFound
719 | Error::ViewNotFound(_)
720 | Error::TableOrViewNotFound(_)
721 )
722 }
723
724 pub fn is_constraint_violation(&self) -> bool {
726 matches!(
727 self,
728 Error::NotNullConstraint { .. }
729 | Error::PrimaryKeyConstraint { .. }
730 | Error::UniqueConstraint { .. }
731 | Error::CheckConstraintViolation { .. }
732 | Error::ForeignKeyViolation { .. }
733 )
734 }
735
736 pub fn is_pk_or_unique_violation(&self) -> bool {
738 matches!(
739 self,
740 Error::PrimaryKeyConstraint { .. } | Error::UniqueConstraint { .. }
741 )
742 }
743
744 pub fn is_transaction_error(&self) -> bool {
746 matches!(
747 self,
748 Error::TransactionNotStarted
749 | Error::TransactionAlreadyStarted
750 | Error::TransactionEnded
751 | Error::TransactionAborted
752 | Error::TransactionCommitted
753 | Error::TransactionClosed
754 | Error::TransactionSerializationConflict { .. }
755 | Error::RowLockTimeout { .. }
756 | Error::CompactionBackpressure { .. }
757 )
758 }
759
760 pub fn is_retryable(&self) -> bool {
763 matches!(
764 self,
765 Error::TransactionSerializationConflict { .. }
766 | Error::RowLockTimeout { .. }
767 | Error::CompactionBackpressure { .. }
768 )
769 }
770}
771
772impl From<std::io::Error> for Error {
773 fn from(err: std::io::Error) -> Self {
774 Error::Io {
775 message: err.to_string(),
776 }
777 }
778}
779
780#[cfg(test)]
781mod tests {
782 use super::*;
783
784 #[test]
785 fn test_error_display() {
786 assert_eq!(
787 Error::TableNotFound("users".to_string()).to_string(),
788 "table 'users' not found"
789 );
790 assert_eq!(
791 Error::TableAlreadyExists("users".to_string()).to_string(),
792 "table 'users' already exists"
793 );
794 assert_eq!(
795 Error::ColumnNotFound("email".to_string()).to_string(),
796 "column 'email' not found"
797 );
798 assert_eq!(Error::InvalidValue.to_string(), "invalid value");
799 assert_eq!(
800 Error::TransactionNotStarted.to_string(),
801 "transaction not started"
802 );
803 assert_eq!(
804 Error::IndexNotFound("idx_email".to_string()).to_string(),
805 "index 'idx_email' not found"
806 );
807 assert_eq!(
808 Error::NullComparison.to_string(),
809 "cannot compare NULL with non-NULL value"
810 );
811 }
812
813 #[test]
814 fn test_structured_error_display() {
815 let err = Error::table_columns_not_match(5, 3);
816 assert_eq!(
817 err.to_string(),
818 "table columns don't match, expected 5, got 3"
819 );
820
821 let err = Error::value_too_long("name", 100, 150);
822 assert_eq!(
823 err.to_string(),
824 "value for column name is too long, max 100, got 150"
825 );
826
827 let err = Error::not_null_constraint("email");
828 assert_eq!(
829 err.to_string(),
830 "not null constraint failed for column email"
831 );
832
833 let err = Error::primary_key_constraint(42);
834 assert_eq!(
835 err.to_string(),
836 "primary key constraint failed with 42 already exists in this table"
837 );
838
839 let err = Error::unique_constraint("idx_email", "email", "test@example.com");
840 assert_eq!(
841 err.to_string(),
842 "unique constraint failed for index idx_email on column email with value test@example.com"
843 );
844 }
845
846 #[test]
847 fn test_error_classification() {
848 assert!(Error::TableNotFound("t".to_string()).is_not_found());
849 assert!(Error::ColumnNotFound("c".to_string()).is_not_found());
850 assert!(Error::IndexNotFound("i".to_string()).is_not_found());
851 assert!(!Error::InvalidValue.is_not_found());
852
853 assert!(Error::not_null_constraint("col").is_constraint_violation());
854 assert!(Error::primary_key_constraint(1).is_constraint_violation());
855 assert!(Error::unique_constraint("idx", "col", "val").is_constraint_violation());
856 assert!(Error::CheckConstraintViolation {
857 column: "<table:t>".to_string(),
858 expression: "a > b".to_string(),
859 }
860 .is_constraint_violation());
861 assert!(!Error::TableNotFound("t".to_string()).is_constraint_violation());
862
863 assert!(Error::TransactionNotStarted.is_transaction_error());
864 assert!(Error::TransactionCommitted.is_transaction_error());
865 assert!(!Error::TableNotFound("t".to_string()).is_transaction_error());
866 }
867
868 #[test]
869 fn test_transaction_classifier_covers_retryable_transaction_variants() {
870 let transaction_errors = [
871 Error::TransactionNotStarted,
872 Error::TransactionAlreadyStarted,
873 Error::TransactionEnded,
874 Error::TransactionAborted,
875 Error::TransactionCommitted,
876 Error::TransactionClosed,
877 Error::TransactionSerializationConflict { row_id: 7 },
878 Error::RowLockTimeout {
879 row_id: 7,
880 timeout_ms: 250,
881 },
882 Error::CompactionBackpressure {
883 table: "items".to_string(),
884 segments: 32,
885 physical_bytes: 1024,
886 hard_segments: 32,
887 hard_bytes: 2048,
888 },
889 ];
890
891 for error in transaction_errors {
892 assert!(
893 error.is_transaction_error(),
894 "transaction classifier rejected {error:?}"
895 );
896 }
897
898 assert!(Error::CompactionBackpressure {
899 table: "items".to_string(),
900 segments: 32,
901 physical_bytes: 1024,
902 hard_segments: 32,
903 hard_bytes: 2048,
904 }
905 .is_retryable());
906 }
907
908 #[test]
909 fn test_error_equality() {
910 assert_eq!(
911 Error::TableNotFound("t".to_string()),
912 Error::TableNotFound("t".to_string())
913 );
914 assert_ne!(
915 Error::TableNotFound("t".to_string()),
916 Error::TableAlreadyExists("t".to_string())
917 );
918
919 let err1 = Error::table_columns_not_match(5, 3);
920 let err2 = Error::table_columns_not_match(5, 3);
921 let err3 = Error::table_columns_not_match(5, 4);
922 assert_eq!(err1, err2);
923 assert_ne!(err1, err3);
924 }
925
926 #[test]
927 fn navigation_errors_have_stable_machine_categories() {
928 let error = Error::navigation(
929 NavigationErrorCode::TargetColumnNotFound,
930 "target column 'profiles.missing' does not exist",
931 );
932 assert_eq!(
933 error.navigation_code(),
934 Some(NavigationErrorCode::TargetColumnNotFound)
935 );
936 assert_eq!(
937 error.to_string(),
938 "NAVIGATION_TARGET_COLUMN_NOT_FOUND: target column 'profiles.missing' does not exist"
939 );
940 }
941
942 #[test]
943 fn neutral_context_does_not_depend_on_presentation_text() {
944 let error = Error::RowLockTimeout {
945 row_id: 17,
946 timeout_ms: 250,
947 };
948 let context = error.context();
949 assert_eq!(context.code().as_str(), "ROW_LOCK_TIMEOUT");
950 assert_eq!(context.category(), ErrorCategory::Transaction);
951 assert!(context.retryable());
952 assert!(!context.not_found());
953 assert!(!context.constraint_violation());
954
955 let error = Error::ColumnNotFound("missing".to_owned());
956 let context = error.context();
957 assert_eq!(context.code().as_str(), "COLUMN_NOT_FOUND");
958 assert_eq!(context.category(), ErrorCategory::Catalog);
959 assert!(context.not_found());
960 }
961
962 #[test]
963 fn test_io_error_conversion() {
964 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
965 let err: Error = io_err.into();
966 assert!(matches!(err, Error::Io { .. }));
967 assert!(err.to_string().contains("file not found"));
968 }
969}