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