Skip to main content

fsqlite_error/
lib.rs

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