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