1use std::path::PathBuf;
2
3use thiserror::Error;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub enum DatabaseImagePublicationErrorClass {
14 SourceChanged,
19 PrecommitRolledBack,
24 OutcomeIndeterminate,
30}
31
32#[derive(Error, Debug)]
38pub enum FrankenError {
39 #[error("database not found: '{path}'")]
42 DatabaseNotFound { path: PathBuf },
43
44 #[error("database is locked: '{path}'")]
46 DatabaseLocked { path: PathBuf },
47
48 #[error("multi-process contract violation: {detail}")]
57 MultiProcessContractViolation { detail: String },
58
59 #[error("database disk image is malformed: {detail}")]
61 DatabaseCorrupt { detail: String },
62
63 #[error("file is not a database: '{path}'")]
65 NotADatabase { path: PathBuf },
66
67 #[error("database is full")]
69 DatabaseFull,
70
71 #[error("database schema has changed")]
73 SchemaChanged,
74
75 #[error("I/O error: {0}")]
78 Io(#[from] std::io::Error),
79
80 #[error("disk I/O error reading page {page}")]
82 IoRead { page: u32 },
83
84 #[error("disk I/O error writing page {page}")]
86 IoWrite { page: u32 },
87
88 #[error("short read: expected {expected} bytes, got {actual}")]
90 ShortRead { expected: usize, actual: usize },
91
92 #[error("near \"{token}\": syntax error")]
95 SyntaxError { token: String },
96
97 #[error("SQL error at offset {offset}: {detail}")]
99 ParseError { offset: usize, detail: String },
100
101 #[error("query returned no rows")]
103 QueryReturnedNoRows,
104
105 #[error("query returned more than one row")]
107 QueryReturnedMultipleRows,
108
109 #[error("no such table: {name}")]
111 NoSuchTable { name: String },
112
113 #[error("no such column: {name}")]
115 NoSuchColumn { name: String },
116
117 #[error("no such index: {name}")]
119 NoSuchIndex { name: String },
120
121 #[error("table {name} already exists")]
123 TableExists { name: String },
124
125 #[error("index {name} already exists")]
127 IndexExists { name: String },
128
129 #[error("ambiguous column name: {name}")]
131 AmbiguousColumn { name: String },
132
133 #[error("UNIQUE constraint failed: {columns}")]
136 UniqueViolation { columns: String },
137
138 #[error("NOT NULL constraint failed: {column}")]
140 NotNullViolation { column: String },
141
142 #[error("CHECK constraint failed: {name}")]
144 CheckViolation { name: String },
145
146 #[error("FOREIGN KEY constraint failed")]
148 ForeignKeyViolation,
149
150 #[error("PRIMARY KEY constraint failed")]
152 PrimaryKeyViolation,
153
154 #[error("cannot store {actual} value in {column_type} column {column}")]
156 DatatypeViolation {
157 column: String,
158 column_type: String,
159 actual: String,
160 },
161
162 #[error("cannot start a transaction within a transaction")]
165 NestedTransaction,
166
167 #[error("cannot commit - no transaction is active")]
169 NoActiveTransaction,
170
171 #[error("cannot VACUUM from within a transaction")]
173 VacuumWithinTransaction,
174
175 #[error("transaction rolled back: {reason}")]
177 TransactionRolledBack { reason: String },
178
179 #[error("database image publication outcome is indeterminate: {detail}")]
182 DatabaseImagePublicationOutcomeIndeterminate { detail: String },
183
184 #[error("write conflict on page {page}: held by transaction {holder}")]
187 WriteConflict { page: u32, holder: u64 },
188
189 #[error("serialization failure: page {page} was modified after snapshot")]
191 SerializationFailure { page: u32 },
192
193 #[error("snapshot too old: transaction {txn_id} is below GC horizon")]
195 SnapshotTooOld { txn_id: u64 },
196
197 #[error("database is busy")]
200 Busy,
201
202 #[error("database is busy (recovery in progress)")]
204 BusyRecovery,
205
206 #[error("database is busy (snapshot conflict on pages: {conflicting_pages})")]
210 BusySnapshot { conflicting_pages: String },
211
212 #[error(
214 "BEGIN CONCURRENT unavailable: fsqlite-shm not present (multi-writer MVCC requires shared memory coordination)"
215 )]
216 ConcurrentUnavailable,
217
218 #[error("type mismatch: expected {expected}, got {actual}")]
221 TypeMismatch { expected: String, actual: String },
222
223 #[error("integer overflow")]
225 IntegerOverflow,
226
227 #[error("{what} out of range: {value}")]
229 OutOfRange { what: String, value: String },
230
231 #[error("string or BLOB exceeds size limit")]
234 TooBig,
235
236 #[error("too many columns: {count} (max {max})")]
238 TooManyColumns { count: usize, max: usize },
239
240 #[error("SQL statement too long: {length} bytes (max {max})")]
242 SqlTooLong { length: usize, max: usize },
243
244 #[error("expression tree too deep (max {max})")]
246 ExpressionTooDeep { max: usize },
247
248 #[error("too many levels of trigger recursion")]
251 TriggerRecursionDepthExceeded,
252
253 #[error("too many attached databases (max {max})")]
255 TooManyAttached { max: usize },
256
257 #[error("too many arguments to function {name}")]
259 TooManyArguments { name: String },
260
261 #[error("WAL file is corrupt: {detail}")]
264 WalCorrupt { detail: String },
265
266 #[error("WAL checkpoint failed: {detail}")]
268 CheckpointFailed { detail: String },
269
270 #[error("file locking failed: {detail}")]
273 LockFailed { detail: String },
274
275 #[error("unable to open database file: '{path}'")]
277 CannotOpen { path: PathBuf },
278
279 #[error("internal error: {0}")]
282 Internal(String),
283
284 #[error(
288 "cannot start another operation on an AsyncConnection while its synchronous row stream is active; use a separate connection or defer it until the callback returns"
289 )]
290 SynchronousStreamReentrancy,
291
292 #[error("unsupported operation")]
294 Unsupported,
295
296 #[error("not implemented: {0}")]
298 NotImplemented(String),
299
300 #[error("callback requested query abort")]
302 Abort,
303
304 #[error("authorization denied")]
306 AuthDenied,
307
308 #[error("out of memory")]
310 OutOfMemory,
311
312 #[error(
317 "page buffer capacity exhausted during {operation}: page_size={page_size}, max_buffers={max_buffers}, total_buffers={total_buffers}, available_buffers={available_buffers}, cached_clean={cached_clean}, cached_dirty={cached_dirty}, successful_evictions={successful_evictions}"
318 )]
319 PageBufferCapacityExhausted {
320 operation: &'static str,
321 page_size: usize,
322 max_buffers: usize,
323 total_buffers: usize,
324 available_buffers: usize,
325 cached_clean: usize,
326 cached_dirty: usize,
327 successful_evictions: usize,
328 },
329
330 #[error("{0}")]
332 FunctionError(String),
333
334 #[error("{0}")]
342 RaiseFail(String),
343
344 #[error("background worker failed: {0}")]
346 BackgroundWorkerFailed(String),
347
348 #[error("attempt to write a readonly database")]
350 ReadOnly,
351
352 #[error("interrupted")]
354 Interrupt,
355
356 #[error("VDBE execution error: {detail}")]
358 VdbeExecutionError { detail: String },
359}
360
361#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
365#[repr(i32)]
366pub enum ErrorCode {
367 Ok = 0,
369 Error = 1,
371 Internal = 2,
373 Perm = 3,
375 Abort = 4,
377 Busy = 5,
379 Locked = 6,
381 NoMem = 7,
383 ReadOnly = 8,
385 Interrupt = 9,
387 IoErr = 10,
389 Corrupt = 11,
391 NotFound = 12,
393 Full = 13,
395 CantOpen = 14,
397 Protocol = 15,
399 Empty = 16,
401 Schema = 17,
403 TooBig = 18,
405 Constraint = 19,
407 Mismatch = 20,
409 Misuse = 21,
411 NoLfs = 22,
413 Auth = 23,
415 Format = 24,
417 Range = 25,
419 NotADb = 26,
421 Notice = 27,
423 Warning = 28,
425 Row = 100,
427 Done = 101,
429}
430
431impl FrankenError {
432 #[allow(clippy::match_same_arms)]
434 pub const fn error_code(&self) -> ErrorCode {
435 match self {
436 Self::DatabaseNotFound { .. } | Self::CannotOpen { .. } => ErrorCode::CantOpen,
437 Self::DatabaseLocked { .. } | Self::MultiProcessContractViolation { .. } => {
438 ErrorCode::Busy
439 }
440 Self::DatabaseCorrupt { .. } | Self::WalCorrupt { .. } => ErrorCode::Corrupt,
441 Self::NotADatabase { .. } => ErrorCode::NotADb,
442 Self::DatabaseFull => ErrorCode::Full,
443 Self::SchemaChanged => ErrorCode::Schema,
444 Self::Io(_)
445 | Self::IoRead { .. }
446 | Self::IoWrite { .. }
447 | Self::ShortRead { .. }
448 | Self::CheckpointFailed { .. } => ErrorCode::IoErr,
449 Self::SyntaxError { .. }
450 | Self::ParseError { .. }
451 | Self::QueryReturnedNoRows
452 | Self::QueryReturnedMultipleRows
453 | Self::NoSuchTable { .. }
454 | Self::NoSuchColumn { .. }
455 | Self::NoSuchIndex { .. }
456 | Self::TableExists { .. }
457 | Self::IndexExists { .. }
458 | Self::AmbiguousColumn { .. }
459 | Self::NestedTransaction
460 | Self::VacuumWithinTransaction
461 | Self::NoActiveTransaction
462 | Self::TransactionRolledBack { .. }
463 | Self::TooManyColumns { .. }
464 | Self::SqlTooLong { .. }
465 | Self::ExpressionTooDeep { .. }
466 | Self::TriggerRecursionDepthExceeded
467 | Self::TooManyAttached { .. }
468 | Self::TooManyArguments { .. }
469 | Self::NotImplemented(_)
470 | Self::FunctionError(_)
471 | Self::RaiseFail(_)
472 | Self::BackgroundWorkerFailed(_)
473 | Self::ConcurrentUnavailable => ErrorCode::Error,
474 Self::UniqueViolation { .. }
475 | Self::NotNullViolation { .. }
476 | Self::CheckViolation { .. }
477 | Self::ForeignKeyViolation
478 | Self::PrimaryKeyViolation
479 | Self::DatatypeViolation { .. } => ErrorCode::Constraint,
480 Self::WriteConflict { .. }
481 | Self::SerializationFailure { .. }
482 | Self::Busy
483 | Self::BusyRecovery
484 | Self::BusySnapshot { .. }
485 | Self::SnapshotTooOld { .. }
486 | Self::LockFailed { .. } => ErrorCode::Busy,
487 Self::TypeMismatch { .. } => ErrorCode::Mismatch,
488 Self::IntegerOverflow | Self::OutOfRange { .. } => ErrorCode::Range,
489 Self::TooBig => ErrorCode::TooBig,
490 Self::Internal(_) | Self::DatabaseImagePublicationOutcomeIndeterminate { .. } => {
491 ErrorCode::Internal
492 }
493 Self::SynchronousStreamReentrancy => ErrorCode::Misuse,
494 Self::Abort => ErrorCode::Abort,
495 Self::AuthDenied => ErrorCode::Auth,
496 Self::OutOfMemory | Self::PageBufferCapacityExhausted { .. } => ErrorCode::NoMem,
497 Self::Unsupported => ErrorCode::NoLfs,
498 Self::ReadOnly => ErrorCode::ReadOnly,
499 Self::Interrupt => ErrorCode::Interrupt,
500 Self::VdbeExecutionError { .. } => ErrorCode::Error,
501 }
502 }
503
504 pub const fn is_user_recoverable(&self) -> bool {
506 matches!(
507 self,
508 Self::DatabaseNotFound { .. }
509 | Self::DatabaseLocked { .. }
510 | Self::Busy
511 | Self::BusyRecovery
512 | Self::BusySnapshot { .. }
513 | Self::Unsupported
514 | Self::SyntaxError { .. }
515 | Self::ParseError { .. }
516 | Self::QueryReturnedNoRows
517 | Self::QueryReturnedMultipleRows
518 | Self::NoSuchTable { .. }
519 | Self::NoSuchColumn { .. }
520 | Self::TypeMismatch { .. }
521 | Self::PageBufferCapacityExhausted { .. }
522 | Self::CannotOpen { .. }
523 )
524 }
525
526 pub const fn suggestion(&self) -> Option<&'static str> {
528 match self {
529 Self::DatabaseNotFound { .. } => Some("Check the file path or create a new database"),
530 Self::DatabaseLocked { .. } => {
531 Some("Close other connections or wait for the lock to be released")
532 }
533 Self::Busy | Self::BusyRecovery => Some("Retry the operation after a short delay"),
534 Self::BusySnapshot { .. } => {
535 Some("Retry the transaction; another writer committed to the same pages")
536 }
537 Self::WriteConflict { .. } | Self::SerializationFailure { .. } => {
538 Some("Retry the transaction; the conflict is transient")
539 }
540 Self::SnapshotTooOld { .. } => Some("Begin a new transaction to get a fresh snapshot"),
541 Self::DatabaseCorrupt { .. } => {
542 Some("Run PRAGMA integrity_check; restore from backup if needed")
543 }
544 Self::TooBig => Some("Reduce the size of the value being inserted"),
545 Self::NotImplemented(_) => Some("This feature is not yet available in FrankenSQLite"),
546 Self::ConcurrentUnavailable => Some(
547 "Use a filesystem that supports shared memory, or use BEGIN (serialized) instead",
548 ),
549 Self::BackgroundWorkerFailed(_) => {
550 Some("Close and reopen the database; inspect the logged worker failure details")
551 }
552 Self::QueryReturnedNoRows => Some("Use query() when zero rows are acceptable"),
553 Self::QueryReturnedMultipleRows => {
554 Some("Use query() when multiple rows are acceptable, or tighten the query")
555 }
556 Self::PageBufferCapacityExhausted { .. } => Some(
557 "Finish or roll back active transactions, then retry; inspect page-buffer diagnostics before raising the configured limit",
558 ),
559 Self::SynchronousStreamReentrancy => Some(
560 "Use a separate connection inside the row callback, or defer the operation until streaming returns",
561 ),
562 _ => None,
563 }
564 }
565
566 pub const fn is_transient(&self) -> bool {
568 matches!(
569 self,
570 Self::Busy
571 | Self::BusyRecovery
572 | Self::BusySnapshot { .. }
573 | Self::DatabaseLocked { .. }
574 | Self::WriteConflict { .. }
575 | Self::SerializationFailure { .. }
576 | Self::PageBufferCapacityExhausted { .. }
577 )
578 }
579
580 #[must_use]
600 pub const fn database_image_publication_error_class(
601 &self,
602 ) -> DatabaseImagePublicationErrorClass {
603 match self {
604 Self::BusySnapshot { .. } => DatabaseImagePublicationErrorClass::SourceChanged,
605 Self::DatabaseImagePublicationOutcomeIndeterminate { .. } => {
606 DatabaseImagePublicationErrorClass::OutcomeIndeterminate
607 }
608 _ => DatabaseImagePublicationErrorClass::PrecommitRolledBack,
609 }
610 }
611
612 pub const fn exit_code(&self) -> i32 {
614 self.error_code() as i32
615 }
616
617 pub const fn extended_error_code(&self) -> i32 {
627 match self {
628 Self::Busy => 5, Self::BusyRecovery => 5 | (1 << 8), Self::BusySnapshot { .. } => 5 | (2 << 8), Self::DatatypeViolation { .. } => 3091, _ => self.error_code() as i32,
633 }
634 }
635
636 pub fn syntax(token: impl Into<String>) -> Self {
638 Self::SyntaxError {
639 token: token.into(),
640 }
641 }
642
643 pub fn parse(offset: usize, detail: impl Into<String>) -> Self {
645 Self::ParseError {
646 offset,
647 detail: detail.into(),
648 }
649 }
650
651 pub fn internal(msg: impl Into<String>) -> Self {
653 Self::Internal(msg.into())
654 }
655
656 pub fn not_implemented(feature: impl Into<String>) -> Self {
658 Self::NotImplemented(feature.into())
659 }
660
661 pub fn function_error(msg: impl Into<String>) -> Self {
663 Self::FunctionError(msg.into())
664 }
665}
666
667pub type Result<T> = std::result::Result<T, FrankenError>;
669
670#[cfg(test)]
671mod tests {
672 use super::*;
673
674 #[test]
675 fn error_display() {
676 let err = FrankenError::syntax("SELEC");
677 assert_eq!(err.to_string(), r#"near "SELEC": syntax error"#);
678 }
679
680 #[test]
681 fn error_display_corrupt() {
682 let err = FrankenError::DatabaseCorrupt {
683 detail: "invalid page header".to_owned(),
684 };
685 assert_eq!(
686 err.to_string(),
687 "database disk image is malformed: invalid page header"
688 );
689 }
690
691 #[test]
692 fn database_image_publication_error_class_is_typed_not_display_parsed() {
693 let source_changed = FrankenError::BusySnapshot {
694 conflicting_pages: "arbitrary generation mismatch detail".to_owned(),
695 };
696 let safe_failure = FrankenError::Internal(
697 "this wording deliberately says outcome is indeterminate but is not the typed variant"
698 .to_owned(),
699 );
700 let indeterminate = FrankenError::DatabaseImagePublicationOutcomeIndeterminate {
701 detail: "arbitrary durability detail with no magic phrase".to_owned(),
702 };
703
704 assert_eq!(
705 source_changed.database_image_publication_error_class(),
706 DatabaseImagePublicationErrorClass::SourceChanged
707 );
708 assert_eq!(
709 safe_failure.database_image_publication_error_class(),
710 DatabaseImagePublicationErrorClass::PrecommitRolledBack
711 );
712 assert_eq!(
713 indeterminate.database_image_publication_error_class(),
714 DatabaseImagePublicationErrorClass::OutcomeIndeterminate
715 );
716 assert_eq!(indeterminate.error_code(), ErrorCode::Internal);
717 assert!(!indeterminate.is_transient());
718 }
719
720 #[test]
721 fn error_display_write_conflict() {
722 let err = FrankenError::WriteConflict {
723 page: 42,
724 holder: 7,
725 };
726 assert_eq!(
727 err.to_string(),
728 "write conflict on page 42: held by transaction 7"
729 );
730 }
731
732 #[test]
733 fn page_buffer_capacity_error_is_structured_and_retryable() {
734 let error = FrankenError::PageBufferCapacityExhausted {
735 operation: "transaction_write_stage",
736 page_size: 4096,
737 max_buffers: 8,
738 total_buffers: 8,
739 available_buffers: 0,
740 cached_clean: 0,
741 cached_dirty: 8,
742 successful_evictions: 0,
743 };
744
745 assert_eq!(error.error_code(), ErrorCode::NoMem);
746 assert!(error.is_user_recoverable());
747 assert!(error.is_transient());
748 assert!(error.suggestion().is_some());
749 assert_eq!(
750 error.to_string(),
751 "page buffer capacity exhausted during transaction_write_stage: page_size=4096, max_buffers=8, total_buffers=8, available_buffers=0, cached_clean=0, cached_dirty=8, successful_evictions=0"
752 );
753 }
754
755 #[test]
756 fn error_code_mapping() {
757 assert_eq!(FrankenError::syntax("x").error_code(), ErrorCode::Error);
758 assert_eq!(
759 FrankenError::QueryReturnedNoRows.error_code(),
760 ErrorCode::Error
761 );
762 assert_eq!(
763 FrankenError::QueryReturnedMultipleRows.error_code(),
764 ErrorCode::Error
765 );
766 assert_eq!(FrankenError::Busy.error_code(), ErrorCode::Busy);
767 assert_eq!(FrankenError::Abort.error_code(), ErrorCode::Abort);
768 assert_eq!(
769 FrankenError::DatabaseCorrupt {
770 detail: String::new()
771 }
772 .error_code(),
773 ErrorCode::Corrupt
774 );
775 assert_eq!(FrankenError::DatabaseFull.error_code(), ErrorCode::Full);
776 assert_eq!(FrankenError::TooBig.error_code(), ErrorCode::TooBig);
777 assert_eq!(FrankenError::OutOfMemory.error_code(), ErrorCode::NoMem);
778 assert_eq!(FrankenError::AuthDenied.error_code(), ErrorCode::Auth);
779 }
780
781 #[test]
782 fn user_recoverable() {
783 assert!(FrankenError::Busy.is_user_recoverable());
784 assert!(FrankenError::QueryReturnedNoRows.is_user_recoverable());
785 assert!(FrankenError::QueryReturnedMultipleRows.is_user_recoverable());
786 assert!(FrankenError::syntax("x").is_user_recoverable());
787 assert!(!FrankenError::internal("bug").is_user_recoverable());
788 assert!(!FrankenError::DatabaseFull.is_user_recoverable());
789 }
790
791 #[test]
792 fn is_transient() {
793 assert!(FrankenError::Busy.is_transient());
794 assert!(FrankenError::BusyRecovery.is_transient());
795 assert!(FrankenError::WriteConflict { page: 1, holder: 1 }.is_transient());
796 assert!(!FrankenError::DatabaseFull.is_transient());
797 assert!(!FrankenError::syntax("x").is_transient());
798 }
799
800 #[test]
801 fn suggestions() {
802 assert!(FrankenError::Busy.suggestion().is_some());
803 assert!(FrankenError::not_implemented("CTE").suggestion().is_some());
804 assert!(
805 FrankenError::QueryReturnedMultipleRows
806 .suggestion()
807 .is_some()
808 );
809 assert!(FrankenError::DatabaseFull.suggestion().is_none());
810 }
811
812 #[test]
813 fn convenience_constructors() {
814 let expected_kw = "kw_where";
816 let err = FrankenError::syntax(expected_kw);
817 assert!(matches!(
818 err,
819 FrankenError::SyntaxError { token: got_kw } if got_kw == expected_kw
820 ));
821
822 let err = FrankenError::parse(42, "unexpected token");
823 assert!(matches!(err, FrankenError::ParseError { offset: 42, .. }));
824
825 let err = FrankenError::internal("assertion failed");
826 let actual = match &err {
827 FrankenError::Internal(msg) => Some(msg.as_str()),
828 _ => None,
829 };
830 assert_eq!(actual, Some("assertion failed"));
831
832 let err = FrankenError::not_implemented("window functions");
833 let actual = match &err {
834 FrankenError::NotImplemented(msg) => Some(msg.as_str()),
835 _ => None,
836 };
837 assert_eq!(actual, Some("window functions"));
838 }
839
840 #[test]
841 fn io_error_from() {
842 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
843 let err: FrankenError = io_err.into();
844 assert!(matches!(err, FrankenError::Io(_)));
845 assert_eq!(err.error_code(), ErrorCode::IoErr);
846 }
847
848 #[test]
849 fn error_code_values() {
850 assert_eq!(ErrorCode::Ok as i32, 0);
851 assert_eq!(ErrorCode::Error as i32, 1);
852 assert_eq!(ErrorCode::Busy as i32, 5);
853 assert_eq!(ErrorCode::Constraint as i32, 19);
854 assert_eq!(ErrorCode::Row as i32, 100);
855 assert_eq!(ErrorCode::Done as i32, 101);
856 }
857
858 #[test]
859 fn exit_code() {
860 assert_eq!(FrankenError::Busy.exit_code(), 5);
861 assert_eq!(FrankenError::internal("x").exit_code(), 2);
862 assert_eq!(FrankenError::syntax("x").exit_code(), 1);
863 }
864
865 #[test]
866 fn extended_error_codes() {
867 assert_eq!(FrankenError::Busy.extended_error_code(), 5);
869 assert_eq!(FrankenError::Busy.error_code(), ErrorCode::Busy);
870
871 assert_eq!(FrankenError::BusyRecovery.extended_error_code(), 261);
873 assert_eq!(FrankenError::BusyRecovery.error_code(), ErrorCode::Busy);
874
875 let busy_snapshot = FrankenError::BusySnapshot {
877 conflicting_pages: "1, 2, 3".to_owned(),
878 };
879 assert_eq!(busy_snapshot.extended_error_code(), 517);
880 assert_eq!(busy_snapshot.error_code(), ErrorCode::Busy);
881
882 assert_eq!(FrankenError::Busy.error_code(), ErrorCode::Busy);
884 assert_eq!(FrankenError::BusyRecovery.error_code(), ErrorCode::Busy);
885 assert_eq!(busy_snapshot.error_code(), ErrorCode::Busy);
886 assert_ne!(
887 FrankenError::Busy.extended_error_code(),
888 busy_snapshot.extended_error_code()
889 );
890 assert_ne!(
891 FrankenError::BusyRecovery.extended_error_code(),
892 busy_snapshot.extended_error_code()
893 );
894 }
895
896 #[test]
897 fn constraint_errors() {
898 let err = FrankenError::UniqueViolation {
899 columns: "users.email".to_owned(),
900 };
901 assert_eq!(err.to_string(), "UNIQUE constraint failed: users.email");
902 assert_eq!(err.error_code(), ErrorCode::Constraint);
903
904 let err = FrankenError::NotNullViolation {
905 column: "name".to_owned(),
906 };
907 assert_eq!(err.to_string(), "NOT NULL constraint failed: name");
908
909 assert_eq!(
910 FrankenError::ForeignKeyViolation.to_string(),
911 "FOREIGN KEY constraint failed"
912 );
913 }
914
915 #[test]
916 fn mvcc_errors() {
917 let err = FrankenError::WriteConflict {
918 page: 5,
919 holder: 10,
920 };
921 assert!(err.is_transient());
922 assert_eq!(err.error_code(), ErrorCode::Busy);
923
924 let err = FrankenError::SerializationFailure { page: 5 };
925 assert!(err.is_transient());
926
927 let err = FrankenError::SnapshotTooOld { txn_id: 42 };
928 assert!(!err.is_transient());
929 assert!(err.suggestion().is_some());
930 }
931
932 #[test]
935 fn display_database_not_found() {
936 let err = FrankenError::DatabaseNotFound {
937 path: PathBuf::from("/tmp/test.db"),
938 };
939 assert_eq!(err.to_string(), "database not found: '/tmp/test.db'");
940 }
941
942 #[test]
943 fn display_database_locked() {
944 let err = FrankenError::DatabaseLocked {
945 path: PathBuf::from("/tmp/test.db"),
946 };
947 assert_eq!(err.to_string(), "database is locked: '/tmp/test.db'");
948 }
949
950 #[test]
951 fn display_not_a_database() {
952 let err = FrankenError::NotADatabase {
953 path: PathBuf::from("/tmp/random.bin"),
954 };
955 assert_eq!(err.to_string(), "file is not a database: '/tmp/random.bin'");
956 }
957
958 #[test]
959 fn display_database_full() {
960 assert_eq!(FrankenError::DatabaseFull.to_string(), "database is full");
961 }
962
963 #[test]
964 fn display_schema_changed() {
965 assert_eq!(
966 FrankenError::SchemaChanged.to_string(),
967 "database schema has changed"
968 );
969 }
970
971 #[test]
972 fn display_io_read_write() {
973 let err = FrankenError::IoRead { page: 17 };
974 assert_eq!(err.to_string(), "disk I/O error reading page 17");
975
976 let err = FrankenError::IoWrite { page: 42 };
977 assert_eq!(err.to_string(), "disk I/O error writing page 42");
978 }
979
980 #[test]
981 fn display_short_read() {
982 let err = FrankenError::ShortRead {
983 expected: 4096,
984 actual: 2048,
985 };
986 assert_eq!(err.to_string(), "short read: expected 4096 bytes, got 2048");
987 }
988
989 #[test]
990 fn display_no_such_table_column_index() {
991 assert_eq!(
992 FrankenError::NoSuchTable {
993 name: "users".to_owned()
994 }
995 .to_string(),
996 "no such table: users"
997 );
998 assert_eq!(
999 FrankenError::NoSuchColumn {
1000 name: "email".to_owned()
1001 }
1002 .to_string(),
1003 "no such column: email"
1004 );
1005 assert_eq!(
1006 FrankenError::NoSuchIndex {
1007 name: "idx_email".to_owned()
1008 }
1009 .to_string(),
1010 "no such index: idx_email"
1011 );
1012 }
1013
1014 #[test]
1015 fn display_already_exists() {
1016 assert_eq!(
1017 FrankenError::TableExists {
1018 name: "t1".to_owned()
1019 }
1020 .to_string(),
1021 "table t1 already exists"
1022 );
1023 assert_eq!(
1024 FrankenError::IndexExists {
1025 name: "i1".to_owned()
1026 }
1027 .to_string(),
1028 "index i1 already exists"
1029 );
1030 }
1031
1032 #[test]
1033 fn display_ambiguous_column() {
1034 let err = FrankenError::AmbiguousColumn {
1035 name: "id".to_owned(),
1036 };
1037 assert_eq!(err.to_string(), "ambiguous column name: id");
1038 }
1039
1040 #[test]
1041 fn display_transaction_errors() {
1042 assert_eq!(
1043 FrankenError::NestedTransaction.to_string(),
1044 "cannot start a transaction within a transaction"
1045 );
1046 assert_eq!(
1047 FrankenError::NoActiveTransaction.to_string(),
1048 "cannot commit - no transaction is active"
1049 );
1050 assert_eq!(
1051 FrankenError::VacuumWithinTransaction.to_string(),
1052 "cannot VACUUM from within a transaction"
1053 );
1054 assert_eq!(
1055 FrankenError::TransactionRolledBack {
1056 reason: "constraint".to_owned()
1057 }
1058 .to_string(),
1059 "transaction rolled back: constraint"
1060 );
1061 }
1062
1063 #[test]
1064 fn display_serialization_failure() {
1065 let err = FrankenError::SerializationFailure { page: 99 };
1066 assert_eq!(
1067 err.to_string(),
1068 "serialization failure: page 99 was modified after snapshot"
1069 );
1070 }
1071
1072 #[test]
1073 fn display_snapshot_too_old() {
1074 let err = FrankenError::SnapshotTooOld { txn_id: 100 };
1075 assert_eq!(
1076 err.to_string(),
1077 "snapshot too old: transaction 100 is below GC horizon"
1078 );
1079 }
1080
1081 #[test]
1082 fn display_busy_variants() {
1083 assert_eq!(FrankenError::Busy.to_string(), "database is busy");
1084 assert_eq!(
1085 FrankenError::BusyRecovery.to_string(),
1086 "database is busy (recovery in progress)"
1087 );
1088 }
1089
1090 #[test]
1091 fn display_concurrent_unavailable() {
1092 let err = FrankenError::ConcurrentUnavailable;
1093 assert!(err.to_string().contains("BEGIN CONCURRENT unavailable"));
1094 }
1095
1096 #[test]
1097 fn display_type_errors() {
1098 let err = FrankenError::TypeMismatch {
1099 expected: "INTEGER".to_owned(),
1100 actual: "TEXT".to_owned(),
1101 };
1102 assert_eq!(err.to_string(), "type mismatch: expected INTEGER, got TEXT");
1103
1104 assert_eq!(
1105 FrankenError::IntegerOverflow.to_string(),
1106 "integer overflow"
1107 );
1108
1109 let err = FrankenError::OutOfRange {
1110 what: "page number".to_owned(),
1111 value: "0".to_owned(),
1112 };
1113 assert_eq!(err.to_string(), "page number out of range: 0");
1114 }
1115
1116 #[test]
1117 fn display_limit_errors() {
1118 assert_eq!(
1119 FrankenError::TooBig.to_string(),
1120 "string or BLOB exceeds size limit"
1121 );
1122
1123 let err = FrankenError::TooManyColumns {
1124 count: 2001,
1125 max: 2000,
1126 };
1127 assert_eq!(err.to_string(), "too many columns: 2001 (max 2000)");
1128
1129 let err = FrankenError::SqlTooLong {
1130 length: 2_000_000,
1131 max: 1_000_000,
1132 };
1133 assert_eq!(
1134 err.to_string(),
1135 "SQL statement too long: 2000000 bytes (max 1000000)"
1136 );
1137
1138 let err = FrankenError::ExpressionTooDeep { max: 1000 };
1139 assert_eq!(err.to_string(), "expression tree too deep (max 1000)");
1140
1141 let err = FrankenError::TooManyAttached { max: 10 };
1142 assert_eq!(err.to_string(), "too many attached databases (max 10)");
1143
1144 let err = FrankenError::TooManyArguments {
1145 name: "my_func".to_owned(),
1146 };
1147 assert_eq!(err.to_string(), "too many arguments to function my_func");
1148 }
1149
1150 #[test]
1151 fn display_wal_errors() {
1152 let err = FrankenError::WalCorrupt {
1153 detail: "invalid checksum".to_owned(),
1154 };
1155 assert_eq!(err.to_string(), "WAL file is corrupt: invalid checksum");
1156
1157 let err = FrankenError::CheckpointFailed {
1158 detail: "busy".to_owned(),
1159 };
1160 assert_eq!(err.to_string(), "WAL checkpoint failed: busy");
1161 }
1162
1163 #[test]
1164 fn display_vfs_errors() {
1165 let err = FrankenError::LockFailed {
1166 detail: "permission denied".to_owned(),
1167 };
1168 assert_eq!(err.to_string(), "file locking failed: permission denied");
1169
1170 let err = FrankenError::CannotOpen {
1171 path: PathBuf::from("/readonly/test.db"),
1172 };
1173 assert_eq!(
1174 err.to_string(),
1175 "unable to open database file: '/readonly/test.db'"
1176 );
1177 }
1178
1179 #[test]
1180 fn display_internal_errors() {
1181 assert_eq!(
1182 FrankenError::Internal("assertion failed".to_owned()).to_string(),
1183 "internal error: assertion failed"
1184 );
1185 assert_eq!(
1186 FrankenError::Unsupported.to_string(),
1187 "unsupported operation"
1188 );
1189 assert_eq!(
1190 FrankenError::NotImplemented("CTE".to_owned()).to_string(),
1191 "not implemented: CTE"
1192 );
1193 assert_eq!(
1194 FrankenError::Abort.to_string(),
1195 "callback requested query abort"
1196 );
1197 assert_eq!(FrankenError::AuthDenied.to_string(), "authorization denied");
1198 assert_eq!(FrankenError::OutOfMemory.to_string(), "out of memory");
1199 assert_eq!(
1200 FrankenError::ReadOnly.to_string(),
1201 "attempt to write a readonly database"
1202 );
1203 }
1204
1205 #[test]
1206 fn display_function_error() {
1207 let err = FrankenError::FunctionError("domain error".to_owned());
1208 assert_eq!(err.to_string(), "domain error");
1209 }
1210
1211 #[test]
1212 #[allow(clippy::too_many_lines)]
1213 fn error_code_comprehensive_mapping() {
1214 assert_eq!(
1216 FrankenError::DatabaseNotFound {
1217 path: PathBuf::new()
1218 }
1219 .error_code(),
1220 ErrorCode::CantOpen
1221 );
1222 assert_eq!(
1223 FrankenError::DatabaseLocked {
1224 path: PathBuf::new()
1225 }
1226 .error_code(),
1227 ErrorCode::Busy
1228 );
1229 assert_eq!(
1230 FrankenError::NotADatabase {
1231 path: PathBuf::new()
1232 }
1233 .error_code(),
1234 ErrorCode::NotADb
1235 );
1236 assert_eq!(FrankenError::SchemaChanged.error_code(), ErrorCode::Schema);
1237
1238 assert_eq!(
1240 FrankenError::IoRead { page: 1 }.error_code(),
1241 ErrorCode::IoErr
1242 );
1243 assert_eq!(
1244 FrankenError::IoWrite { page: 1 }.error_code(),
1245 ErrorCode::IoErr
1246 );
1247 assert_eq!(
1248 FrankenError::ShortRead {
1249 expected: 1,
1250 actual: 0
1251 }
1252 .error_code(),
1253 ErrorCode::IoErr
1254 );
1255
1256 assert_eq!(
1258 FrankenError::NoSuchTable {
1259 name: String::new()
1260 }
1261 .error_code(),
1262 ErrorCode::Error
1263 );
1264 assert_eq!(
1265 FrankenError::NoSuchColumn {
1266 name: String::new()
1267 }
1268 .error_code(),
1269 ErrorCode::Error
1270 );
1271 assert_eq!(
1272 FrankenError::NoSuchIndex {
1273 name: String::new()
1274 }
1275 .error_code(),
1276 ErrorCode::Error
1277 );
1278 assert_eq!(
1279 FrankenError::TableExists {
1280 name: String::new()
1281 }
1282 .error_code(),
1283 ErrorCode::Error
1284 );
1285 assert_eq!(
1286 FrankenError::IndexExists {
1287 name: String::new()
1288 }
1289 .error_code(),
1290 ErrorCode::Error
1291 );
1292 assert_eq!(
1293 FrankenError::AmbiguousColumn {
1294 name: String::new()
1295 }
1296 .error_code(),
1297 ErrorCode::Error
1298 );
1299
1300 assert_eq!(
1302 FrankenError::NestedTransaction.error_code(),
1303 ErrorCode::Error
1304 );
1305 assert_eq!(
1306 FrankenError::VacuumWithinTransaction.error_code(),
1307 ErrorCode::Error
1308 );
1309 assert_eq!(
1310 FrankenError::NoActiveTransaction.error_code(),
1311 ErrorCode::Error
1312 );
1313
1314 assert_eq!(
1316 FrankenError::SerializationFailure { page: 1 }.error_code(),
1317 ErrorCode::Busy
1318 );
1319 assert_eq!(
1320 FrankenError::SnapshotTooOld { txn_id: 1 }.error_code(),
1321 ErrorCode::Busy
1322 );
1323 assert_eq!(
1324 FrankenError::LockFailed {
1325 detail: String::new()
1326 }
1327 .error_code(),
1328 ErrorCode::Busy
1329 );
1330
1331 assert_eq!(
1333 FrankenError::TypeMismatch {
1334 expected: String::new(),
1335 actual: String::new()
1336 }
1337 .error_code(),
1338 ErrorCode::Mismatch
1339 );
1340 assert_eq!(FrankenError::IntegerOverflow.error_code(), ErrorCode::Range);
1341 assert_eq!(
1342 FrankenError::OutOfRange {
1343 what: String::new(),
1344 value: String::new()
1345 }
1346 .error_code(),
1347 ErrorCode::Range
1348 );
1349
1350 assert_eq!(
1352 FrankenError::TooManyColumns { count: 1, max: 1 }.error_code(),
1353 ErrorCode::Error
1354 );
1355 assert_eq!(
1356 FrankenError::SqlTooLong { length: 1, max: 1 }.error_code(),
1357 ErrorCode::Error
1358 );
1359 assert_eq!(
1360 FrankenError::ExpressionTooDeep { max: 1 }.error_code(),
1361 ErrorCode::Error
1362 );
1363 assert_eq!(
1364 FrankenError::TriggerRecursionDepthExceeded.error_code(),
1365 ErrorCode::Error
1366 );
1367 assert_eq!(
1368 FrankenError::TriggerRecursionDepthExceeded.to_string(),
1369 "too many levels of trigger recursion"
1370 );
1371 assert_eq!(
1372 FrankenError::TooManyAttached { max: 1 }.error_code(),
1373 ErrorCode::Error
1374 );
1375 assert_eq!(
1376 FrankenError::TooManyArguments {
1377 name: String::new()
1378 }
1379 .error_code(),
1380 ErrorCode::Error
1381 );
1382
1383 assert_eq!(
1385 FrankenError::WalCorrupt {
1386 detail: String::new()
1387 }
1388 .error_code(),
1389 ErrorCode::Corrupt
1390 );
1391 assert_eq!(
1392 FrankenError::CheckpointFailed {
1393 detail: String::new()
1394 }
1395 .error_code(),
1396 ErrorCode::IoErr
1397 );
1398
1399 assert_eq!(
1401 FrankenError::CannotOpen {
1402 path: PathBuf::new()
1403 }
1404 .error_code(),
1405 ErrorCode::CantOpen
1406 );
1407
1408 assert_eq!(
1410 FrankenError::Internal(String::new()).error_code(),
1411 ErrorCode::Internal
1412 );
1413 assert_eq!(
1414 FrankenError::SynchronousStreamReentrancy.error_code(),
1415 ErrorCode::Misuse
1416 );
1417 assert!(!FrankenError::SynchronousStreamReentrancy.is_transient());
1418 assert!(
1419 FrankenError::SynchronousStreamReentrancy
1420 .suggestion()
1421 .is_some()
1422 );
1423 assert_eq!(FrankenError::Unsupported.error_code(), ErrorCode::NoLfs);
1424 assert_eq!(FrankenError::Abort.error_code(), ErrorCode::Abort);
1425 assert_eq!(FrankenError::ReadOnly.error_code(), ErrorCode::ReadOnly);
1426 assert_eq!(
1427 FrankenError::FunctionError(String::new()).error_code(),
1428 ErrorCode::Error
1429 );
1430 assert_eq!(
1431 FrankenError::ConcurrentUnavailable.error_code(),
1432 ErrorCode::Error
1433 );
1434 }
1435
1436 #[test]
1437 fn is_user_recoverable_comprehensive() {
1438 assert!(
1440 FrankenError::DatabaseNotFound {
1441 path: PathBuf::new()
1442 }
1443 .is_user_recoverable()
1444 );
1445 assert!(
1446 FrankenError::DatabaseLocked {
1447 path: PathBuf::new()
1448 }
1449 .is_user_recoverable()
1450 );
1451 assert!(FrankenError::BusyRecovery.is_user_recoverable());
1452 assert!(FrankenError::Unsupported.is_user_recoverable());
1453 assert!(
1454 FrankenError::ParseError {
1455 offset: 0,
1456 detail: String::new()
1457 }
1458 .is_user_recoverable()
1459 );
1460 assert!(
1461 FrankenError::NoSuchTable {
1462 name: String::new()
1463 }
1464 .is_user_recoverable()
1465 );
1466 assert!(
1467 FrankenError::NoSuchColumn {
1468 name: String::new()
1469 }
1470 .is_user_recoverable()
1471 );
1472 assert!(
1473 FrankenError::TypeMismatch {
1474 expected: String::new(),
1475 actual: String::new()
1476 }
1477 .is_user_recoverable()
1478 );
1479 assert!(
1480 FrankenError::CannotOpen {
1481 path: PathBuf::new()
1482 }
1483 .is_user_recoverable()
1484 );
1485
1486 assert!(
1488 !FrankenError::NotADatabase {
1489 path: PathBuf::new()
1490 }
1491 .is_user_recoverable()
1492 );
1493 assert!(!FrankenError::TooBig.is_user_recoverable());
1494 assert!(!FrankenError::OutOfMemory.is_user_recoverable());
1495 assert!(!FrankenError::WriteConflict { page: 1, holder: 1 }.is_user_recoverable());
1496 assert!(
1497 !FrankenError::UniqueViolation {
1498 columns: String::new()
1499 }
1500 .is_user_recoverable()
1501 );
1502 assert!(!FrankenError::ReadOnly.is_user_recoverable());
1503 assert!(!FrankenError::Abort.is_user_recoverable());
1504 }
1505
1506 #[test]
1507 fn is_transient_comprehensive() {
1508 assert!(
1510 FrankenError::DatabaseLocked {
1511 path: PathBuf::new()
1512 }
1513 .is_transient()
1514 );
1515 assert!(FrankenError::SerializationFailure { page: 1 }.is_transient());
1516
1517 assert!(
1519 !FrankenError::DatabaseCorrupt {
1520 detail: String::new()
1521 }
1522 .is_transient()
1523 );
1524 assert!(
1525 !FrankenError::NotADatabase {
1526 path: PathBuf::new()
1527 }
1528 .is_transient()
1529 );
1530 assert!(!FrankenError::TooBig.is_transient());
1531 assert!(!FrankenError::Internal(String::new()).is_transient());
1532 assert!(!FrankenError::OutOfMemory.is_transient());
1533 assert!(
1534 !FrankenError::UniqueViolation {
1535 columns: String::new()
1536 }
1537 .is_transient()
1538 );
1539 assert!(!FrankenError::ReadOnly.is_transient());
1540 assert!(!FrankenError::ConcurrentUnavailable.is_transient());
1541 }
1542
1543 #[test]
1544 fn suggestion_comprehensive() {
1545 assert!(
1547 FrankenError::DatabaseNotFound {
1548 path: PathBuf::new()
1549 }
1550 .suggestion()
1551 .is_some()
1552 );
1553 assert!(
1554 FrankenError::DatabaseLocked {
1555 path: PathBuf::new()
1556 }
1557 .suggestion()
1558 .is_some()
1559 );
1560 assert!(FrankenError::BusyRecovery.suggestion().is_some());
1561 assert!(
1562 FrankenError::WriteConflict { page: 1, holder: 1 }
1563 .suggestion()
1564 .is_some()
1565 );
1566 assert!(
1567 FrankenError::SerializationFailure { page: 1 }
1568 .suggestion()
1569 .is_some()
1570 );
1571 assert!(
1572 FrankenError::SnapshotTooOld { txn_id: 1 }
1573 .suggestion()
1574 .is_some()
1575 );
1576 assert!(
1577 FrankenError::DatabaseCorrupt {
1578 detail: String::new()
1579 }
1580 .suggestion()
1581 .is_some()
1582 );
1583 assert!(FrankenError::TooBig.suggestion().is_some());
1584 assert!(FrankenError::ConcurrentUnavailable.suggestion().is_some());
1585 assert!(FrankenError::QueryReturnedNoRows.suggestion().is_some());
1586 assert!(
1587 FrankenError::QueryReturnedMultipleRows
1588 .suggestion()
1589 .is_some()
1590 );
1591
1592 assert!(FrankenError::Abort.suggestion().is_none());
1594 assert!(FrankenError::AuthDenied.suggestion().is_none());
1595 assert!(FrankenError::OutOfMemory.suggestion().is_none());
1596 assert!(FrankenError::Internal(String::new()).suggestion().is_none());
1597 assert!(FrankenError::ReadOnly.suggestion().is_none());
1598 }
1599
1600 #[test]
1601 fn error_code_enum_repr_values() {
1602 assert_eq!(ErrorCode::Internal as i32, 2);
1604 assert_eq!(ErrorCode::Perm as i32, 3);
1605 assert_eq!(ErrorCode::Abort as i32, 4);
1606 assert_eq!(ErrorCode::Locked as i32, 6);
1607 assert_eq!(ErrorCode::NoMem as i32, 7);
1608 assert_eq!(ErrorCode::ReadOnly as i32, 8);
1609 assert_eq!(ErrorCode::Interrupt as i32, 9);
1610 assert_eq!(ErrorCode::IoErr as i32, 10);
1611 assert_eq!(ErrorCode::Corrupt as i32, 11);
1612 assert_eq!(ErrorCode::NotFound as i32, 12);
1613 assert_eq!(ErrorCode::Full as i32, 13);
1614 assert_eq!(ErrorCode::CantOpen as i32, 14);
1615 assert_eq!(ErrorCode::Protocol as i32, 15);
1616 assert_eq!(ErrorCode::Empty as i32, 16);
1617 assert_eq!(ErrorCode::Schema as i32, 17);
1618 assert_eq!(ErrorCode::TooBig as i32, 18);
1619 assert_eq!(ErrorCode::Mismatch as i32, 20);
1620 assert_eq!(ErrorCode::Misuse as i32, 21);
1621 assert_eq!(ErrorCode::NoLfs as i32, 22);
1622 assert_eq!(ErrorCode::Auth as i32, 23);
1623 assert_eq!(ErrorCode::Format as i32, 24);
1624 assert_eq!(ErrorCode::Range as i32, 25);
1625 assert_eq!(ErrorCode::NotADb as i32, 26);
1626 assert_eq!(ErrorCode::Notice as i32, 27);
1627 assert_eq!(ErrorCode::Warning as i32, 28);
1628 }
1629
1630 #[test]
1631 fn error_code_clone_eq() {
1632 let code = ErrorCode::Busy;
1633 let cloned = code;
1634 assert_eq!(code, cloned);
1635 assert_eq!(code, ErrorCode::Busy);
1636 assert_ne!(code, ErrorCode::Error);
1637 }
1638
1639 #[test]
1640 fn function_error_constructor() {
1641 let err = FrankenError::function_error("division by zero");
1642 let actual = match &err {
1643 FrankenError::FunctionError(msg) => Some(msg.as_str()),
1644 _ => None,
1645 };
1646 assert_eq!(actual, Some("division by zero"));
1647 assert_eq!(err.error_code(), ErrorCode::Error);
1648 }
1649
1650 #[test]
1651 fn background_worker_failed_is_non_transient_generic_error() {
1652 let err = FrankenError::BackgroundWorkerFailed("gc worker panic".to_owned());
1653 assert_eq!(err.to_string(), "background worker failed: gc worker panic");
1654 assert_eq!(err.error_code(), ErrorCode::Error);
1655 assert!(!err.is_transient());
1656 assert!(!err.is_user_recoverable());
1657 assert!(err.suggestion().is_some());
1658 }
1659
1660 #[test]
1661 fn constraint_error_codes_all_variants() {
1662 assert_eq!(
1663 FrankenError::CheckViolation {
1664 name: "ck1".to_owned()
1665 }
1666 .error_code(),
1667 ErrorCode::Constraint
1668 );
1669 assert_eq!(
1670 FrankenError::PrimaryKeyViolation.error_code(),
1671 ErrorCode::Constraint
1672 );
1673 assert_eq!(
1674 FrankenError::CheckViolation {
1675 name: "ck1".to_owned()
1676 }
1677 .to_string(),
1678 "CHECK constraint failed: ck1"
1679 );
1680 assert_eq!(
1681 FrankenError::PrimaryKeyViolation.to_string(),
1682 "PRIMARY KEY constraint failed"
1683 );
1684
1685 let err = FrankenError::DatatypeViolation {
1686 column: "t1.name".to_owned(),
1687 column_type: "INTEGER".to_owned(),
1688 actual: "TEXT".to_owned(),
1689 };
1690 assert_eq!(
1691 err.to_string(),
1692 "cannot store TEXT value in INTEGER column t1.name"
1693 );
1694 assert_eq!(err.error_code(), ErrorCode::Constraint);
1695 assert_eq!(err.extended_error_code(), 3091);
1696 }
1697
1698 #[test]
1699 fn exit_code_matches_error_code() {
1700 let cases: Vec<FrankenError> = vec![
1702 FrankenError::DatabaseFull,
1703 FrankenError::TooBig,
1704 FrankenError::OutOfMemory,
1705 FrankenError::AuthDenied,
1706 FrankenError::Abort,
1707 FrankenError::ReadOnly,
1708 FrankenError::Unsupported,
1709 ];
1710 for err in cases {
1711 assert_eq!(err.exit_code(), err.error_code() as i32);
1712 }
1713 }
1714}