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