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