1use std::path::PathBuf;
2
3use thiserror::Error;
4
5#[derive(Error, Debug)]
11pub enum FrankenError {
12 #[error("database not found: '{path}'")]
15 DatabaseNotFound { path: PathBuf },
16
17 #[error("database is locked: '{path}'")]
19 DatabaseLocked { path: PathBuf },
20
21 #[error("multi-process contract violation: {detail}")]
30 MultiProcessContractViolation { detail: String },
31
32 #[error("database disk image is malformed: {detail}")]
34 DatabaseCorrupt { detail: String },
35
36 #[error("file is not a database: '{path}'")]
38 NotADatabase { path: PathBuf },
39
40 #[error("database is full")]
42 DatabaseFull,
43
44 #[error("database schema has changed")]
46 SchemaChanged,
47
48 #[error("I/O error: {0}")]
51 Io(#[from] std::io::Error),
52
53 #[error("disk I/O error reading page {page}")]
55 IoRead { page: u32 },
56
57 #[error("disk I/O error writing page {page}")]
59 IoWrite { page: u32 },
60
61 #[error("short read: expected {expected} bytes, got {actual}")]
63 ShortRead { expected: usize, actual: usize },
64
65 #[error("near \"{token}\": syntax error")]
68 SyntaxError { token: String },
69
70 #[error("SQL error at offset {offset}: {detail}")]
72 ParseError { offset: usize, detail: String },
73
74 #[error("query returned no rows")]
76 QueryReturnedNoRows,
77
78 #[error("query returned more than one row")]
80 QueryReturnedMultipleRows,
81
82 #[error("no such table: {name}")]
84 NoSuchTable { name: String },
85
86 #[error("no such column: {name}")]
88 NoSuchColumn { name: String },
89
90 #[error("no such index: {name}")]
92 NoSuchIndex { name: String },
93
94 #[error("table {name} already exists")]
96 TableExists { name: String },
97
98 #[error("index {name} already exists")]
100 IndexExists { name: String },
101
102 #[error("ambiguous column name: {name}")]
104 AmbiguousColumn { name: String },
105
106 #[error("UNIQUE constraint failed: {columns}")]
109 UniqueViolation { columns: String },
110
111 #[error("NOT NULL constraint failed: {column}")]
113 NotNullViolation { column: String },
114
115 #[error("CHECK constraint failed: {name}")]
117 CheckViolation { name: String },
118
119 #[error("FOREIGN KEY constraint failed")]
121 ForeignKeyViolation,
122
123 #[error("PRIMARY KEY constraint failed")]
125 PrimaryKeyViolation,
126
127 #[error("cannot store {actual} value in {column_type} column {column}")]
129 DatatypeViolation {
130 column: String,
131 column_type: String,
132 actual: String,
133 },
134
135 #[error("cannot start a transaction within a transaction")]
138 NestedTransaction,
139
140 #[error("cannot commit - no transaction is active")]
142 NoActiveTransaction,
143
144 #[error("cannot VACUUM from within a transaction")]
146 VacuumWithinTransaction,
147
148 #[error("transaction rolled back: {reason}")]
150 TransactionRolledBack { reason: String },
151
152 #[error("write conflict on page {page}: held by transaction {holder}")]
155 WriteConflict { page: u32, holder: u64 },
156
157 #[error("serialization failure: page {page} was modified after snapshot")]
159 SerializationFailure { page: u32 },
160
161 #[error("snapshot too old: transaction {txn_id} is below GC horizon")]
163 SnapshotTooOld { txn_id: u64 },
164
165 #[error("database is busy")]
168 Busy,
169
170 #[error("database is busy (recovery in progress)")]
172 BusyRecovery,
173
174 #[error("database is busy (snapshot conflict on pages: {conflicting_pages})")]
178 BusySnapshot { conflicting_pages: String },
179
180 #[error(
182 "BEGIN CONCURRENT unavailable: fsqlite-shm not present (multi-writer MVCC requires shared memory coordination)"
183 )]
184 ConcurrentUnavailable,
185
186 #[error("type mismatch: expected {expected}, got {actual}")]
189 TypeMismatch { expected: String, actual: String },
190
191 #[error("integer overflow")]
193 IntegerOverflow,
194
195 #[error("{what} out of range: {value}")]
197 OutOfRange { what: String, value: String },
198
199 #[error("string or BLOB exceeds size limit")]
202 TooBig,
203
204 #[error("too many columns: {count} (max {max})")]
206 TooManyColumns { count: usize, max: usize },
207
208 #[error("SQL statement too long: {length} bytes (max {max})")]
210 SqlTooLong { length: usize, max: usize },
211
212 #[error("expression tree too deep (max {max})")]
214 ExpressionTooDeep { max: usize },
215
216 #[error("too many attached databases (max {max})")]
218 TooManyAttached { max: usize },
219
220 #[error("too many arguments to function {name}")]
222 TooManyArguments { name: String },
223
224 #[error("WAL file is corrupt: {detail}")]
227 WalCorrupt { detail: String },
228
229 #[error("WAL checkpoint failed: {detail}")]
231 CheckpointFailed { detail: String },
232
233 #[error("file locking failed: {detail}")]
236 LockFailed { detail: String },
237
238 #[error("unable to open database file: '{path}'")]
240 CannotOpen { path: PathBuf },
241
242 #[error("internal error: {0}")]
245 Internal(String),
246
247 #[error("unsupported operation")]
249 Unsupported,
250
251 #[error("not implemented: {0}")]
253 NotImplemented(String),
254
255 #[error("callback requested query abort")]
257 Abort,
258
259 #[error("authorization denied")]
261 AuthDenied,
262
263 #[error("out of memory")]
265 OutOfMemory,
266
267 #[error("{0}")]
269 FunctionError(String),
270
271 #[error("{0}")]
279 RaiseFail(String),
280
281 #[error("background worker failed: {0}")]
283 BackgroundWorkerFailed(String),
284
285 #[error("attempt to write a readonly database")]
287 ReadOnly,
288
289 #[error("interrupted")]
291 Interrupt,
292
293 #[error("VDBE execution error: {detail}")]
295 VdbeExecutionError { detail: String },
296}
297
298#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
302#[repr(i32)]
303pub enum ErrorCode {
304 Ok = 0,
306 Error = 1,
308 Internal = 2,
310 Perm = 3,
312 Abort = 4,
314 Busy = 5,
316 Locked = 6,
318 NoMem = 7,
320 ReadOnly = 8,
322 Interrupt = 9,
324 IoErr = 10,
326 Corrupt = 11,
328 NotFound = 12,
330 Full = 13,
332 CantOpen = 14,
334 Protocol = 15,
336 Empty = 16,
338 Schema = 17,
340 TooBig = 18,
342 Constraint = 19,
344 Mismatch = 20,
346 Misuse = 21,
348 NoLfs = 22,
350 Auth = 23,
352 Format = 24,
354 Range = 25,
356 NotADb = 26,
358 Notice = 27,
360 Warning = 28,
362 Row = 100,
364 Done = 101,
366}
367
368impl FrankenError {
369 #[allow(clippy::match_same_arms)]
371 pub const fn error_code(&self) -> ErrorCode {
372 match self {
373 Self::DatabaseNotFound { .. } | Self::CannotOpen { .. } => ErrorCode::CantOpen,
374 Self::DatabaseLocked { .. } | Self::MultiProcessContractViolation { .. } => {
375 ErrorCode::Busy
376 }
377 Self::DatabaseCorrupt { .. } | Self::WalCorrupt { .. } => ErrorCode::Corrupt,
378 Self::NotADatabase { .. } => ErrorCode::NotADb,
379 Self::DatabaseFull => ErrorCode::Full,
380 Self::SchemaChanged => ErrorCode::Schema,
381 Self::Io(_)
382 | Self::IoRead { .. }
383 | Self::IoWrite { .. }
384 | Self::ShortRead { .. }
385 | Self::CheckpointFailed { .. } => ErrorCode::IoErr,
386 Self::SyntaxError { .. }
387 | Self::ParseError { .. }
388 | Self::QueryReturnedNoRows
389 | Self::QueryReturnedMultipleRows
390 | Self::NoSuchTable { .. }
391 | Self::NoSuchColumn { .. }
392 | Self::NoSuchIndex { .. }
393 | Self::TableExists { .. }
394 | Self::IndexExists { .. }
395 | Self::AmbiguousColumn { .. }
396 | Self::NestedTransaction
397 | Self::VacuumWithinTransaction
398 | Self::NoActiveTransaction
399 | Self::TransactionRolledBack { .. }
400 | Self::TooManyColumns { .. }
401 | Self::SqlTooLong { .. }
402 | Self::ExpressionTooDeep { .. }
403 | Self::TooManyAttached { .. }
404 | Self::TooManyArguments { .. }
405 | Self::NotImplemented(_)
406 | Self::FunctionError(_)
407 | Self::RaiseFail(_)
408 | Self::BackgroundWorkerFailed(_)
409 | Self::ConcurrentUnavailable => ErrorCode::Error,
410 Self::UniqueViolation { .. }
411 | Self::NotNullViolation { .. }
412 | Self::CheckViolation { .. }
413 | Self::ForeignKeyViolation
414 | Self::PrimaryKeyViolation
415 | Self::DatatypeViolation { .. } => ErrorCode::Constraint,
416 Self::WriteConflict { .. }
417 | Self::SerializationFailure { .. }
418 | Self::Busy
419 | Self::BusyRecovery
420 | Self::BusySnapshot { .. }
421 | Self::SnapshotTooOld { .. }
422 | Self::LockFailed { .. } => ErrorCode::Busy,
423 Self::TypeMismatch { .. } => ErrorCode::Mismatch,
424 Self::IntegerOverflow | Self::OutOfRange { .. } => ErrorCode::Range,
425 Self::TooBig => ErrorCode::TooBig,
426 Self::Internal(_) => ErrorCode::Internal,
427 Self::Abort => ErrorCode::Abort,
428 Self::AuthDenied => ErrorCode::Auth,
429 Self::OutOfMemory => ErrorCode::NoMem,
430 Self::Unsupported => ErrorCode::NoLfs,
431 Self::ReadOnly => ErrorCode::ReadOnly,
432 Self::Interrupt => ErrorCode::Interrupt,
433 Self::VdbeExecutionError { .. } => ErrorCode::Error,
434 }
435 }
436
437 pub const fn is_user_recoverable(&self) -> bool {
439 matches!(
440 self,
441 Self::DatabaseNotFound { .. }
442 | Self::DatabaseLocked { .. }
443 | Self::Busy
444 | Self::BusyRecovery
445 | Self::BusySnapshot { .. }
446 | Self::Unsupported
447 | Self::SyntaxError { .. }
448 | Self::ParseError { .. }
449 | Self::QueryReturnedNoRows
450 | Self::QueryReturnedMultipleRows
451 | Self::NoSuchTable { .. }
452 | Self::NoSuchColumn { .. }
453 | Self::TypeMismatch { .. }
454 | Self::CannotOpen { .. }
455 )
456 }
457
458 pub const fn suggestion(&self) -> Option<&'static str> {
460 match self {
461 Self::DatabaseNotFound { .. } => Some("Check the file path or create a new database"),
462 Self::DatabaseLocked { .. } => {
463 Some("Close other connections or wait for the lock to be released")
464 }
465 Self::Busy | Self::BusyRecovery => Some("Retry the operation after a short delay"),
466 Self::BusySnapshot { .. } => {
467 Some("Retry the transaction; another writer committed to the same pages")
468 }
469 Self::WriteConflict { .. } | Self::SerializationFailure { .. } => {
470 Some("Retry the transaction; the conflict is transient")
471 }
472 Self::SnapshotTooOld { .. } => Some("Begin a new transaction to get a fresh snapshot"),
473 Self::DatabaseCorrupt { .. } => {
474 Some("Run PRAGMA integrity_check; restore from backup if needed")
475 }
476 Self::TooBig => Some("Reduce the size of the value being inserted"),
477 Self::NotImplemented(_) => Some("This feature is not yet available in FrankenSQLite"),
478 Self::ConcurrentUnavailable => Some(
479 "Use a filesystem that supports shared memory, or use BEGIN (serialized) instead",
480 ),
481 Self::BackgroundWorkerFailed(_) => {
482 Some("Close and reopen the database; inspect the logged worker failure details")
483 }
484 Self::QueryReturnedNoRows => Some("Use query() when zero rows are acceptable"),
485 Self::QueryReturnedMultipleRows => {
486 Some("Use query() when multiple rows are acceptable, or tighten the query")
487 }
488 _ => None,
489 }
490 }
491
492 pub const fn is_transient(&self) -> bool {
494 matches!(
495 self,
496 Self::Busy
497 | Self::BusyRecovery
498 | Self::BusySnapshot { .. }
499 | Self::DatabaseLocked { .. }
500 | Self::WriteConflict { .. }
501 | Self::SerializationFailure { .. }
502 )
503 }
504
505 pub const fn exit_code(&self) -> i32 {
507 self.error_code() as i32
508 }
509
510 pub const fn extended_error_code(&self) -> i32 {
520 match self {
521 Self::Busy => 5, Self::BusyRecovery => 5 | (1 << 8), Self::BusySnapshot { .. } => 5 | (2 << 8), Self::DatatypeViolation { .. } => 3091, _ => self.error_code() as i32,
526 }
527 }
528
529 pub fn syntax(token: impl Into<String>) -> Self {
531 Self::SyntaxError {
532 token: token.into(),
533 }
534 }
535
536 pub fn parse(offset: usize, detail: impl Into<String>) -> Self {
538 Self::ParseError {
539 offset,
540 detail: detail.into(),
541 }
542 }
543
544 pub fn internal(msg: impl Into<String>) -> Self {
546 Self::Internal(msg.into())
547 }
548
549 pub fn not_implemented(feature: impl Into<String>) -> Self {
551 Self::NotImplemented(feature.into())
552 }
553
554 pub fn function_error(msg: impl Into<String>) -> Self {
556 Self::FunctionError(msg.into())
557 }
558}
559
560pub type Result<T> = std::result::Result<T, FrankenError>;
562
563#[cfg(test)]
564mod tests {
565 use super::*;
566
567 #[test]
568 fn error_display() {
569 let err = FrankenError::syntax("SELEC");
570 assert_eq!(err.to_string(), r#"near "SELEC": syntax error"#);
571 }
572
573 #[test]
574 fn error_display_corrupt() {
575 let err = FrankenError::DatabaseCorrupt {
576 detail: "invalid page header".to_owned(),
577 };
578 assert_eq!(
579 err.to_string(),
580 "database disk image is malformed: invalid page header"
581 );
582 }
583
584 #[test]
585 fn error_display_write_conflict() {
586 let err = FrankenError::WriteConflict {
587 page: 42,
588 holder: 7,
589 };
590 assert_eq!(
591 err.to_string(),
592 "write conflict on page 42: held by transaction 7"
593 );
594 }
595
596 #[test]
597 fn error_code_mapping() {
598 assert_eq!(FrankenError::syntax("x").error_code(), ErrorCode::Error);
599 assert_eq!(
600 FrankenError::QueryReturnedNoRows.error_code(),
601 ErrorCode::Error
602 );
603 assert_eq!(
604 FrankenError::QueryReturnedMultipleRows.error_code(),
605 ErrorCode::Error
606 );
607 assert_eq!(FrankenError::Busy.error_code(), ErrorCode::Busy);
608 assert_eq!(FrankenError::Abort.error_code(), ErrorCode::Abort);
609 assert_eq!(
610 FrankenError::DatabaseCorrupt {
611 detail: String::new()
612 }
613 .error_code(),
614 ErrorCode::Corrupt
615 );
616 assert_eq!(FrankenError::DatabaseFull.error_code(), ErrorCode::Full);
617 assert_eq!(FrankenError::TooBig.error_code(), ErrorCode::TooBig);
618 assert_eq!(FrankenError::OutOfMemory.error_code(), ErrorCode::NoMem);
619 assert_eq!(FrankenError::AuthDenied.error_code(), ErrorCode::Auth);
620 }
621
622 #[test]
623 fn user_recoverable() {
624 assert!(FrankenError::Busy.is_user_recoverable());
625 assert!(FrankenError::QueryReturnedNoRows.is_user_recoverable());
626 assert!(FrankenError::QueryReturnedMultipleRows.is_user_recoverable());
627 assert!(FrankenError::syntax("x").is_user_recoverable());
628 assert!(!FrankenError::internal("bug").is_user_recoverable());
629 assert!(!FrankenError::DatabaseFull.is_user_recoverable());
630 }
631
632 #[test]
633 fn is_transient() {
634 assert!(FrankenError::Busy.is_transient());
635 assert!(FrankenError::BusyRecovery.is_transient());
636 assert!(FrankenError::WriteConflict { page: 1, holder: 1 }.is_transient());
637 assert!(!FrankenError::DatabaseFull.is_transient());
638 assert!(!FrankenError::syntax("x").is_transient());
639 }
640
641 #[test]
642 fn suggestions() {
643 assert!(FrankenError::Busy.suggestion().is_some());
644 assert!(FrankenError::not_implemented("CTE").suggestion().is_some());
645 assert!(
646 FrankenError::QueryReturnedMultipleRows
647 .suggestion()
648 .is_some()
649 );
650 assert!(FrankenError::DatabaseFull.suggestion().is_none());
651 }
652
653 #[test]
654 fn convenience_constructors() {
655 let expected_kw = "kw_where";
657 let err = FrankenError::syntax(expected_kw);
658 assert!(matches!(
659 err,
660 FrankenError::SyntaxError { token: got_kw } if got_kw == expected_kw
661 ));
662
663 let err = FrankenError::parse(42, "unexpected token");
664 assert!(matches!(err, FrankenError::ParseError { offset: 42, .. }));
665
666 let err = FrankenError::internal("assertion failed");
667 let actual = match &err {
668 FrankenError::Internal(msg) => Some(msg.as_str()),
669 _ => None,
670 };
671 assert_eq!(actual, Some("assertion failed"));
672
673 let err = FrankenError::not_implemented("window functions");
674 let actual = match &err {
675 FrankenError::NotImplemented(msg) => Some(msg.as_str()),
676 _ => None,
677 };
678 assert_eq!(actual, Some("window functions"));
679 }
680
681 #[test]
682 fn io_error_from() {
683 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
684 let err: FrankenError = io_err.into();
685 assert!(matches!(err, FrankenError::Io(_)));
686 assert_eq!(err.error_code(), ErrorCode::IoErr);
687 }
688
689 #[test]
690 fn error_code_values() {
691 assert_eq!(ErrorCode::Ok as i32, 0);
692 assert_eq!(ErrorCode::Error as i32, 1);
693 assert_eq!(ErrorCode::Busy as i32, 5);
694 assert_eq!(ErrorCode::Constraint as i32, 19);
695 assert_eq!(ErrorCode::Row as i32, 100);
696 assert_eq!(ErrorCode::Done as i32, 101);
697 }
698
699 #[test]
700 fn exit_code() {
701 assert_eq!(FrankenError::Busy.exit_code(), 5);
702 assert_eq!(FrankenError::internal("x").exit_code(), 2);
703 assert_eq!(FrankenError::syntax("x").exit_code(), 1);
704 }
705
706 #[test]
707 fn extended_error_codes() {
708 assert_eq!(FrankenError::Busy.extended_error_code(), 5);
710 assert_eq!(FrankenError::Busy.error_code(), ErrorCode::Busy);
711
712 assert_eq!(FrankenError::BusyRecovery.extended_error_code(), 261);
714 assert_eq!(FrankenError::BusyRecovery.error_code(), ErrorCode::Busy);
715
716 let busy_snapshot = FrankenError::BusySnapshot {
718 conflicting_pages: "1, 2, 3".to_owned(),
719 };
720 assert_eq!(busy_snapshot.extended_error_code(), 517);
721 assert_eq!(busy_snapshot.error_code(), ErrorCode::Busy);
722
723 assert_eq!(FrankenError::Busy.error_code(), ErrorCode::Busy);
725 assert_eq!(FrankenError::BusyRecovery.error_code(), ErrorCode::Busy);
726 assert_eq!(busy_snapshot.error_code(), ErrorCode::Busy);
727 assert_ne!(
728 FrankenError::Busy.extended_error_code(),
729 busy_snapshot.extended_error_code()
730 );
731 assert_ne!(
732 FrankenError::BusyRecovery.extended_error_code(),
733 busy_snapshot.extended_error_code()
734 );
735 }
736
737 #[test]
738 fn constraint_errors() {
739 let err = FrankenError::UniqueViolation {
740 columns: "users.email".to_owned(),
741 };
742 assert_eq!(err.to_string(), "UNIQUE constraint failed: users.email");
743 assert_eq!(err.error_code(), ErrorCode::Constraint);
744
745 let err = FrankenError::NotNullViolation {
746 column: "name".to_owned(),
747 };
748 assert_eq!(err.to_string(), "NOT NULL constraint failed: name");
749
750 assert_eq!(
751 FrankenError::ForeignKeyViolation.to_string(),
752 "FOREIGN KEY constraint failed"
753 );
754 }
755
756 #[test]
757 fn mvcc_errors() {
758 let err = FrankenError::WriteConflict {
759 page: 5,
760 holder: 10,
761 };
762 assert!(err.is_transient());
763 assert_eq!(err.error_code(), ErrorCode::Busy);
764
765 let err = FrankenError::SerializationFailure { page: 5 };
766 assert!(err.is_transient());
767
768 let err = FrankenError::SnapshotTooOld { txn_id: 42 };
769 assert!(!err.is_transient());
770 assert!(err.suggestion().is_some());
771 }
772
773 #[test]
776 fn display_database_not_found() {
777 let err = FrankenError::DatabaseNotFound {
778 path: PathBuf::from("/tmp/test.db"),
779 };
780 assert_eq!(err.to_string(), "database not found: '/tmp/test.db'");
781 }
782
783 #[test]
784 fn display_database_locked() {
785 let err = FrankenError::DatabaseLocked {
786 path: PathBuf::from("/tmp/test.db"),
787 };
788 assert_eq!(err.to_string(), "database is locked: '/tmp/test.db'");
789 }
790
791 #[test]
792 fn display_not_a_database() {
793 let err = FrankenError::NotADatabase {
794 path: PathBuf::from("/tmp/random.bin"),
795 };
796 assert_eq!(err.to_string(), "file is not a database: '/tmp/random.bin'");
797 }
798
799 #[test]
800 fn display_database_full() {
801 assert_eq!(FrankenError::DatabaseFull.to_string(), "database is full");
802 }
803
804 #[test]
805 fn display_schema_changed() {
806 assert_eq!(
807 FrankenError::SchemaChanged.to_string(),
808 "database schema has changed"
809 );
810 }
811
812 #[test]
813 fn display_io_read_write() {
814 let err = FrankenError::IoRead { page: 17 };
815 assert_eq!(err.to_string(), "disk I/O error reading page 17");
816
817 let err = FrankenError::IoWrite { page: 42 };
818 assert_eq!(err.to_string(), "disk I/O error writing page 42");
819 }
820
821 #[test]
822 fn display_short_read() {
823 let err = FrankenError::ShortRead {
824 expected: 4096,
825 actual: 2048,
826 };
827 assert_eq!(err.to_string(), "short read: expected 4096 bytes, got 2048");
828 }
829
830 #[test]
831 fn display_no_such_table_column_index() {
832 assert_eq!(
833 FrankenError::NoSuchTable {
834 name: "users".to_owned()
835 }
836 .to_string(),
837 "no such table: users"
838 );
839 assert_eq!(
840 FrankenError::NoSuchColumn {
841 name: "email".to_owned()
842 }
843 .to_string(),
844 "no such column: email"
845 );
846 assert_eq!(
847 FrankenError::NoSuchIndex {
848 name: "idx_email".to_owned()
849 }
850 .to_string(),
851 "no such index: idx_email"
852 );
853 }
854
855 #[test]
856 fn display_already_exists() {
857 assert_eq!(
858 FrankenError::TableExists {
859 name: "t1".to_owned()
860 }
861 .to_string(),
862 "table t1 already exists"
863 );
864 assert_eq!(
865 FrankenError::IndexExists {
866 name: "i1".to_owned()
867 }
868 .to_string(),
869 "index i1 already exists"
870 );
871 }
872
873 #[test]
874 fn display_ambiguous_column() {
875 let err = FrankenError::AmbiguousColumn {
876 name: "id".to_owned(),
877 };
878 assert_eq!(err.to_string(), "ambiguous column name: id");
879 }
880
881 #[test]
882 fn display_transaction_errors() {
883 assert_eq!(
884 FrankenError::NestedTransaction.to_string(),
885 "cannot start a transaction within a transaction"
886 );
887 assert_eq!(
888 FrankenError::NoActiveTransaction.to_string(),
889 "cannot commit - no transaction is active"
890 );
891 assert_eq!(
892 FrankenError::VacuumWithinTransaction.to_string(),
893 "cannot VACUUM from within a transaction"
894 );
895 assert_eq!(
896 FrankenError::TransactionRolledBack {
897 reason: "constraint".to_owned()
898 }
899 .to_string(),
900 "transaction rolled back: constraint"
901 );
902 }
903
904 #[test]
905 fn display_serialization_failure() {
906 let err = FrankenError::SerializationFailure { page: 99 };
907 assert_eq!(
908 err.to_string(),
909 "serialization failure: page 99 was modified after snapshot"
910 );
911 }
912
913 #[test]
914 fn display_snapshot_too_old() {
915 let err = FrankenError::SnapshotTooOld { txn_id: 100 };
916 assert_eq!(
917 err.to_string(),
918 "snapshot too old: transaction 100 is below GC horizon"
919 );
920 }
921
922 #[test]
923 fn display_busy_variants() {
924 assert_eq!(FrankenError::Busy.to_string(), "database is busy");
925 assert_eq!(
926 FrankenError::BusyRecovery.to_string(),
927 "database is busy (recovery in progress)"
928 );
929 }
930
931 #[test]
932 fn display_concurrent_unavailable() {
933 let err = FrankenError::ConcurrentUnavailable;
934 assert!(err.to_string().contains("BEGIN CONCURRENT unavailable"));
935 }
936
937 #[test]
938 fn display_type_errors() {
939 let err = FrankenError::TypeMismatch {
940 expected: "INTEGER".to_owned(),
941 actual: "TEXT".to_owned(),
942 };
943 assert_eq!(err.to_string(), "type mismatch: expected INTEGER, got TEXT");
944
945 assert_eq!(
946 FrankenError::IntegerOverflow.to_string(),
947 "integer overflow"
948 );
949
950 let err = FrankenError::OutOfRange {
951 what: "page number".to_owned(),
952 value: "0".to_owned(),
953 };
954 assert_eq!(err.to_string(), "page number out of range: 0");
955 }
956
957 #[test]
958 fn display_limit_errors() {
959 assert_eq!(
960 FrankenError::TooBig.to_string(),
961 "string or BLOB exceeds size limit"
962 );
963
964 let err = FrankenError::TooManyColumns {
965 count: 2001,
966 max: 2000,
967 };
968 assert_eq!(err.to_string(), "too many columns: 2001 (max 2000)");
969
970 let err = FrankenError::SqlTooLong {
971 length: 2_000_000,
972 max: 1_000_000,
973 };
974 assert_eq!(
975 err.to_string(),
976 "SQL statement too long: 2000000 bytes (max 1000000)"
977 );
978
979 let err = FrankenError::ExpressionTooDeep { max: 1000 };
980 assert_eq!(err.to_string(), "expression tree too deep (max 1000)");
981
982 let err = FrankenError::TooManyAttached { max: 10 };
983 assert_eq!(err.to_string(), "too many attached databases (max 10)");
984
985 let err = FrankenError::TooManyArguments {
986 name: "my_func".to_owned(),
987 };
988 assert_eq!(err.to_string(), "too many arguments to function my_func");
989 }
990
991 #[test]
992 fn display_wal_errors() {
993 let err = FrankenError::WalCorrupt {
994 detail: "invalid checksum".to_owned(),
995 };
996 assert_eq!(err.to_string(), "WAL file is corrupt: invalid checksum");
997
998 let err = FrankenError::CheckpointFailed {
999 detail: "busy".to_owned(),
1000 };
1001 assert_eq!(err.to_string(), "WAL checkpoint failed: busy");
1002 }
1003
1004 #[test]
1005 fn display_vfs_errors() {
1006 let err = FrankenError::LockFailed {
1007 detail: "permission denied".to_owned(),
1008 };
1009 assert_eq!(err.to_string(), "file locking failed: permission denied");
1010
1011 let err = FrankenError::CannotOpen {
1012 path: PathBuf::from("/readonly/test.db"),
1013 };
1014 assert_eq!(
1015 err.to_string(),
1016 "unable to open database file: '/readonly/test.db'"
1017 );
1018 }
1019
1020 #[test]
1021 fn display_internal_errors() {
1022 assert_eq!(
1023 FrankenError::Internal("assertion failed".to_owned()).to_string(),
1024 "internal error: assertion failed"
1025 );
1026 assert_eq!(
1027 FrankenError::Unsupported.to_string(),
1028 "unsupported operation"
1029 );
1030 assert_eq!(
1031 FrankenError::NotImplemented("CTE".to_owned()).to_string(),
1032 "not implemented: CTE"
1033 );
1034 assert_eq!(
1035 FrankenError::Abort.to_string(),
1036 "callback requested query abort"
1037 );
1038 assert_eq!(FrankenError::AuthDenied.to_string(), "authorization denied");
1039 assert_eq!(FrankenError::OutOfMemory.to_string(), "out of memory");
1040 assert_eq!(
1041 FrankenError::ReadOnly.to_string(),
1042 "attempt to write a readonly database"
1043 );
1044 }
1045
1046 #[test]
1047 fn display_function_error() {
1048 let err = FrankenError::FunctionError("domain error".to_owned());
1049 assert_eq!(err.to_string(), "domain error");
1050 }
1051
1052 #[test]
1053 #[allow(clippy::too_many_lines)]
1054 fn error_code_comprehensive_mapping() {
1055 assert_eq!(
1057 FrankenError::DatabaseNotFound {
1058 path: PathBuf::new()
1059 }
1060 .error_code(),
1061 ErrorCode::CantOpen
1062 );
1063 assert_eq!(
1064 FrankenError::DatabaseLocked {
1065 path: PathBuf::new()
1066 }
1067 .error_code(),
1068 ErrorCode::Busy
1069 );
1070 assert_eq!(
1071 FrankenError::NotADatabase {
1072 path: PathBuf::new()
1073 }
1074 .error_code(),
1075 ErrorCode::NotADb
1076 );
1077 assert_eq!(FrankenError::SchemaChanged.error_code(), ErrorCode::Schema);
1078
1079 assert_eq!(
1081 FrankenError::IoRead { page: 1 }.error_code(),
1082 ErrorCode::IoErr
1083 );
1084 assert_eq!(
1085 FrankenError::IoWrite { page: 1 }.error_code(),
1086 ErrorCode::IoErr
1087 );
1088 assert_eq!(
1089 FrankenError::ShortRead {
1090 expected: 1,
1091 actual: 0
1092 }
1093 .error_code(),
1094 ErrorCode::IoErr
1095 );
1096
1097 assert_eq!(
1099 FrankenError::NoSuchTable {
1100 name: String::new()
1101 }
1102 .error_code(),
1103 ErrorCode::Error
1104 );
1105 assert_eq!(
1106 FrankenError::NoSuchColumn {
1107 name: String::new()
1108 }
1109 .error_code(),
1110 ErrorCode::Error
1111 );
1112 assert_eq!(
1113 FrankenError::NoSuchIndex {
1114 name: String::new()
1115 }
1116 .error_code(),
1117 ErrorCode::Error
1118 );
1119 assert_eq!(
1120 FrankenError::TableExists {
1121 name: String::new()
1122 }
1123 .error_code(),
1124 ErrorCode::Error
1125 );
1126 assert_eq!(
1127 FrankenError::IndexExists {
1128 name: String::new()
1129 }
1130 .error_code(),
1131 ErrorCode::Error
1132 );
1133 assert_eq!(
1134 FrankenError::AmbiguousColumn {
1135 name: String::new()
1136 }
1137 .error_code(),
1138 ErrorCode::Error
1139 );
1140
1141 assert_eq!(
1143 FrankenError::NestedTransaction.error_code(),
1144 ErrorCode::Error
1145 );
1146 assert_eq!(
1147 FrankenError::VacuumWithinTransaction.error_code(),
1148 ErrorCode::Error
1149 );
1150 assert_eq!(
1151 FrankenError::NoActiveTransaction.error_code(),
1152 ErrorCode::Error
1153 );
1154
1155 assert_eq!(
1157 FrankenError::SerializationFailure { page: 1 }.error_code(),
1158 ErrorCode::Busy
1159 );
1160 assert_eq!(
1161 FrankenError::SnapshotTooOld { txn_id: 1 }.error_code(),
1162 ErrorCode::Busy
1163 );
1164 assert_eq!(
1165 FrankenError::LockFailed {
1166 detail: String::new()
1167 }
1168 .error_code(),
1169 ErrorCode::Busy
1170 );
1171
1172 assert_eq!(
1174 FrankenError::TypeMismatch {
1175 expected: String::new(),
1176 actual: String::new()
1177 }
1178 .error_code(),
1179 ErrorCode::Mismatch
1180 );
1181 assert_eq!(FrankenError::IntegerOverflow.error_code(), ErrorCode::Range);
1182 assert_eq!(
1183 FrankenError::OutOfRange {
1184 what: String::new(),
1185 value: String::new()
1186 }
1187 .error_code(),
1188 ErrorCode::Range
1189 );
1190
1191 assert_eq!(
1193 FrankenError::TooManyColumns { count: 1, max: 1 }.error_code(),
1194 ErrorCode::Error
1195 );
1196 assert_eq!(
1197 FrankenError::SqlTooLong { length: 1, max: 1 }.error_code(),
1198 ErrorCode::Error
1199 );
1200 assert_eq!(
1201 FrankenError::ExpressionTooDeep { max: 1 }.error_code(),
1202 ErrorCode::Error
1203 );
1204 assert_eq!(
1205 FrankenError::TooManyAttached { max: 1 }.error_code(),
1206 ErrorCode::Error
1207 );
1208 assert_eq!(
1209 FrankenError::TooManyArguments {
1210 name: String::new()
1211 }
1212 .error_code(),
1213 ErrorCode::Error
1214 );
1215
1216 assert_eq!(
1218 FrankenError::WalCorrupt {
1219 detail: String::new()
1220 }
1221 .error_code(),
1222 ErrorCode::Corrupt
1223 );
1224 assert_eq!(
1225 FrankenError::CheckpointFailed {
1226 detail: String::new()
1227 }
1228 .error_code(),
1229 ErrorCode::IoErr
1230 );
1231
1232 assert_eq!(
1234 FrankenError::CannotOpen {
1235 path: PathBuf::new()
1236 }
1237 .error_code(),
1238 ErrorCode::CantOpen
1239 );
1240
1241 assert_eq!(
1243 FrankenError::Internal(String::new()).error_code(),
1244 ErrorCode::Internal
1245 );
1246 assert_eq!(FrankenError::Unsupported.error_code(), ErrorCode::NoLfs);
1247 assert_eq!(FrankenError::Abort.error_code(), ErrorCode::Abort);
1248 assert_eq!(FrankenError::ReadOnly.error_code(), ErrorCode::ReadOnly);
1249 assert_eq!(
1250 FrankenError::FunctionError(String::new()).error_code(),
1251 ErrorCode::Error
1252 );
1253 assert_eq!(
1254 FrankenError::ConcurrentUnavailable.error_code(),
1255 ErrorCode::Error
1256 );
1257 }
1258
1259 #[test]
1260 fn is_user_recoverable_comprehensive() {
1261 assert!(
1263 FrankenError::DatabaseNotFound {
1264 path: PathBuf::new()
1265 }
1266 .is_user_recoverable()
1267 );
1268 assert!(
1269 FrankenError::DatabaseLocked {
1270 path: PathBuf::new()
1271 }
1272 .is_user_recoverable()
1273 );
1274 assert!(FrankenError::BusyRecovery.is_user_recoverable());
1275 assert!(FrankenError::Unsupported.is_user_recoverable());
1276 assert!(
1277 FrankenError::ParseError {
1278 offset: 0,
1279 detail: String::new()
1280 }
1281 .is_user_recoverable()
1282 );
1283 assert!(
1284 FrankenError::NoSuchTable {
1285 name: String::new()
1286 }
1287 .is_user_recoverable()
1288 );
1289 assert!(
1290 FrankenError::NoSuchColumn {
1291 name: String::new()
1292 }
1293 .is_user_recoverable()
1294 );
1295 assert!(
1296 FrankenError::TypeMismatch {
1297 expected: String::new(),
1298 actual: String::new()
1299 }
1300 .is_user_recoverable()
1301 );
1302 assert!(
1303 FrankenError::CannotOpen {
1304 path: PathBuf::new()
1305 }
1306 .is_user_recoverable()
1307 );
1308
1309 assert!(
1311 !FrankenError::NotADatabase {
1312 path: PathBuf::new()
1313 }
1314 .is_user_recoverable()
1315 );
1316 assert!(!FrankenError::TooBig.is_user_recoverable());
1317 assert!(!FrankenError::OutOfMemory.is_user_recoverable());
1318 assert!(!FrankenError::WriteConflict { page: 1, holder: 1 }.is_user_recoverable());
1319 assert!(
1320 !FrankenError::UniqueViolation {
1321 columns: String::new()
1322 }
1323 .is_user_recoverable()
1324 );
1325 assert!(!FrankenError::ReadOnly.is_user_recoverable());
1326 assert!(!FrankenError::Abort.is_user_recoverable());
1327 }
1328
1329 #[test]
1330 fn is_transient_comprehensive() {
1331 assert!(
1333 FrankenError::DatabaseLocked {
1334 path: PathBuf::new()
1335 }
1336 .is_transient()
1337 );
1338 assert!(FrankenError::SerializationFailure { page: 1 }.is_transient());
1339
1340 assert!(
1342 !FrankenError::DatabaseCorrupt {
1343 detail: String::new()
1344 }
1345 .is_transient()
1346 );
1347 assert!(
1348 !FrankenError::NotADatabase {
1349 path: PathBuf::new()
1350 }
1351 .is_transient()
1352 );
1353 assert!(!FrankenError::TooBig.is_transient());
1354 assert!(!FrankenError::Internal(String::new()).is_transient());
1355 assert!(!FrankenError::OutOfMemory.is_transient());
1356 assert!(
1357 !FrankenError::UniqueViolation {
1358 columns: String::new()
1359 }
1360 .is_transient()
1361 );
1362 assert!(!FrankenError::ReadOnly.is_transient());
1363 assert!(!FrankenError::ConcurrentUnavailable.is_transient());
1364 }
1365
1366 #[test]
1367 fn suggestion_comprehensive() {
1368 assert!(
1370 FrankenError::DatabaseNotFound {
1371 path: PathBuf::new()
1372 }
1373 .suggestion()
1374 .is_some()
1375 );
1376 assert!(
1377 FrankenError::DatabaseLocked {
1378 path: PathBuf::new()
1379 }
1380 .suggestion()
1381 .is_some()
1382 );
1383 assert!(FrankenError::BusyRecovery.suggestion().is_some());
1384 assert!(
1385 FrankenError::WriteConflict { page: 1, holder: 1 }
1386 .suggestion()
1387 .is_some()
1388 );
1389 assert!(
1390 FrankenError::SerializationFailure { page: 1 }
1391 .suggestion()
1392 .is_some()
1393 );
1394 assert!(
1395 FrankenError::SnapshotTooOld { txn_id: 1 }
1396 .suggestion()
1397 .is_some()
1398 );
1399 assert!(
1400 FrankenError::DatabaseCorrupt {
1401 detail: String::new()
1402 }
1403 .suggestion()
1404 .is_some()
1405 );
1406 assert!(FrankenError::TooBig.suggestion().is_some());
1407 assert!(FrankenError::ConcurrentUnavailable.suggestion().is_some());
1408 assert!(FrankenError::QueryReturnedNoRows.suggestion().is_some());
1409 assert!(
1410 FrankenError::QueryReturnedMultipleRows
1411 .suggestion()
1412 .is_some()
1413 );
1414
1415 assert!(FrankenError::Abort.suggestion().is_none());
1417 assert!(FrankenError::AuthDenied.suggestion().is_none());
1418 assert!(FrankenError::OutOfMemory.suggestion().is_none());
1419 assert!(FrankenError::Internal(String::new()).suggestion().is_none());
1420 assert!(FrankenError::ReadOnly.suggestion().is_none());
1421 }
1422
1423 #[test]
1424 fn error_code_enum_repr_values() {
1425 assert_eq!(ErrorCode::Internal as i32, 2);
1427 assert_eq!(ErrorCode::Perm as i32, 3);
1428 assert_eq!(ErrorCode::Abort as i32, 4);
1429 assert_eq!(ErrorCode::Locked as i32, 6);
1430 assert_eq!(ErrorCode::NoMem as i32, 7);
1431 assert_eq!(ErrorCode::ReadOnly as i32, 8);
1432 assert_eq!(ErrorCode::Interrupt as i32, 9);
1433 assert_eq!(ErrorCode::IoErr as i32, 10);
1434 assert_eq!(ErrorCode::Corrupt as i32, 11);
1435 assert_eq!(ErrorCode::NotFound as i32, 12);
1436 assert_eq!(ErrorCode::Full as i32, 13);
1437 assert_eq!(ErrorCode::CantOpen as i32, 14);
1438 assert_eq!(ErrorCode::Protocol as i32, 15);
1439 assert_eq!(ErrorCode::Empty as i32, 16);
1440 assert_eq!(ErrorCode::Schema as i32, 17);
1441 assert_eq!(ErrorCode::TooBig as i32, 18);
1442 assert_eq!(ErrorCode::Mismatch as i32, 20);
1443 assert_eq!(ErrorCode::Misuse as i32, 21);
1444 assert_eq!(ErrorCode::NoLfs as i32, 22);
1445 assert_eq!(ErrorCode::Auth as i32, 23);
1446 assert_eq!(ErrorCode::Format as i32, 24);
1447 assert_eq!(ErrorCode::Range as i32, 25);
1448 assert_eq!(ErrorCode::NotADb as i32, 26);
1449 assert_eq!(ErrorCode::Notice as i32, 27);
1450 assert_eq!(ErrorCode::Warning as i32, 28);
1451 }
1452
1453 #[test]
1454 fn error_code_clone_eq() {
1455 let code = ErrorCode::Busy;
1456 let cloned = code;
1457 assert_eq!(code, cloned);
1458 assert_eq!(code, ErrorCode::Busy);
1459 assert_ne!(code, ErrorCode::Error);
1460 }
1461
1462 #[test]
1463 fn function_error_constructor() {
1464 let err = FrankenError::function_error("division by zero");
1465 let actual = match &err {
1466 FrankenError::FunctionError(msg) => Some(msg.as_str()),
1467 _ => None,
1468 };
1469 assert_eq!(actual, Some("division by zero"));
1470 assert_eq!(err.error_code(), ErrorCode::Error);
1471 }
1472
1473 #[test]
1474 fn background_worker_failed_is_non_transient_generic_error() {
1475 let err = FrankenError::BackgroundWorkerFailed("gc worker panic".to_owned());
1476 assert_eq!(err.to_string(), "background worker failed: gc worker panic");
1477 assert_eq!(err.error_code(), ErrorCode::Error);
1478 assert!(!err.is_transient());
1479 assert!(!err.is_user_recoverable());
1480 assert!(err.suggestion().is_some());
1481 }
1482
1483 #[test]
1484 fn constraint_error_codes_all_variants() {
1485 assert_eq!(
1486 FrankenError::CheckViolation {
1487 name: "ck1".to_owned()
1488 }
1489 .error_code(),
1490 ErrorCode::Constraint
1491 );
1492 assert_eq!(
1493 FrankenError::PrimaryKeyViolation.error_code(),
1494 ErrorCode::Constraint
1495 );
1496 assert_eq!(
1497 FrankenError::CheckViolation {
1498 name: "ck1".to_owned()
1499 }
1500 .to_string(),
1501 "CHECK constraint failed: ck1"
1502 );
1503 assert_eq!(
1504 FrankenError::PrimaryKeyViolation.to_string(),
1505 "PRIMARY KEY constraint failed"
1506 );
1507
1508 let err = FrankenError::DatatypeViolation {
1509 column: "t1.name".to_owned(),
1510 column_type: "INTEGER".to_owned(),
1511 actual: "TEXT".to_owned(),
1512 };
1513 assert_eq!(
1514 err.to_string(),
1515 "cannot store TEXT value in INTEGER column t1.name"
1516 );
1517 assert_eq!(err.error_code(), ErrorCode::Constraint);
1518 assert_eq!(err.extended_error_code(), 3091);
1519 }
1520
1521 #[test]
1522 fn exit_code_matches_error_code() {
1523 let cases: Vec<FrankenError> = vec![
1525 FrankenError::DatabaseFull,
1526 FrankenError::TooBig,
1527 FrankenError::OutOfMemory,
1528 FrankenError::AuthDenied,
1529 FrankenError::Abort,
1530 FrankenError::ReadOnly,
1531 FrankenError::Unsupported,
1532 ];
1533 for err in cases {
1534 assert_eq!(err.exit_code(), err.error_code() as i32);
1535 }
1536 }
1537}