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