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