1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
//! Error and result types.
//!
//! graphitesql mirrors SQLite's primary result codes so that callers familiar
//! with SQLite get predictable, recognizable errors. The extended result codes
//! will be layered on as the engine grows (tracked in `ROADMAP.md`).
use alloc::string::String;
use core::fmt;
/// A `Result` whose error is graphitesql's [`Error`].
pub type Result<T> = core::result::Result<T, Error>;
/// An error returned by graphitesql.
///
/// Variants are named after the corresponding SQLite primary result codes
/// (`SQLITE_*`) to keep the mapping obvious. [`Error::code`] returns the numeric
/// code SQLite would use, which is handy for compatibility shims and tests.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Error {
/// Generic error (`SQLITE_ERROR`), with a human-readable message.
Error(String),
/// Like [`Error`](Error::Error) — same `SQLITE_ERROR` code and the *identical*
/// human-readable message — but carrying the byte offset of the offending token
/// within the parsed SQL (e.g. the position of a function call whose argument
/// count is wrong), so the shell can place its `^--- error here` caret exactly
/// even when the token text repeats. The `Display` is byte-for-byte the same as
/// `Error`; only [`Error::parse_offset`] differs.
ErrorAt(String, usize),
/// The database file is malformed (`SQLITE_CORRUPT`).
Corrupt(String),
/// A disk I/O error occurred in the VFS (`SQLITE_IOERR`).
Io(String),
/// The database file is locked (`SQLITE_BUSY`).
Busy,
/// Access permission denied (`SQLITE_PERM` / `SQLITE_CANTOPEN`).
CantOpen(String),
/// A constraint violation (`SQLITE_CONSTRAINT`).
Constraint(String),
/// SQL could not be tokenized or parsed, or a logic error in SQL
/// (`SQLITE_ERROR`, surfaced separately for clearer diagnostics).
Parse(String),
/// A syntax error carrying the byte offset of the offending token within the
/// parsed SQL (`SQLITE_ERROR`), like [`Parse`](Error::Parse) but with the
/// location SQLite's `sqlite3_error_offset` reports — used to place the shell's
/// `^--- error here` caret exactly even when the token text repeats. The message
/// is identical to the `Parse` form; the offset is into the SQL text passed to
/// the parser.
ParseAt(String, usize),
/// An operation was attempted that this build does not yet implement.
///
/// Not a SQLite code; it exists so the engine can fail loudly and
/// specifically while under construction rather than silently misbehave.
Unsupported(&'static str),
}
impl Error {
/// The SQLite primary result code corresponding to this error.
///
/// [`Error::Unsupported`] maps to `SQLITE_ERROR` (1) since SQLite has no
/// equivalent concept.
pub fn code(&self) -> i32 {
match self {
// A few generic (`Error`) messages map to a specific SQLite extended
// code, keyed off the fixed message rather than a dedicated variant: a
// "datatype mismatch" (non-integer `LIMIT`/`OFFSET`, incompatible INTEGER
// PRIMARY KEY value) is `SQLITE_MISMATCH` (20), and a "string or blob too
// big" is `SQLITE_TOOBIG` (18).
Error::Error(m) | Error::ErrorAt(m, _) if m == "datatype mismatch" => 20,
Error::Error(m) | Error::ErrorAt(m, _) if m == "string or blob too big" => 18,
Error::Error(_)
| Error::ErrorAt(..)
| Error::Parse(_)
| Error::ParseAt(..)
| Error::Unsupported(_) => 1, // SQLITE_ERROR
Error::Corrupt(_) => 11, // SQLITE_CORRUPT
Error::Io(_) => 10, // SQLITE_IOERR
Error::Busy => 5, // SQLITE_BUSY
Error::CantOpen(_) => 14, // SQLITE_CANTOPEN
Error::Constraint(_) => 19, // SQLITE_CONSTRAINT
}
}
/// The byte offset of the offending token within the parsed SQL, when known (a
/// [`ParseAt`](Error::ParseAt) syntax error or an [`ErrorAt`](Error::ErrorAt)
/// located resolution error) — the equivalent of SQLite's
/// `sqlite3_error_offset`. `None` for every other error, including a `Parse`
/// without a locatable token (`incomplete input`).
pub fn parse_offset(&self) -> Option<usize> {
match self {
Error::ParseAt(_, off) | Error::ErrorAt(_, off) => Some(*off),
_ => None,
}
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Error(m) | Error::ErrorAt(m, _) => write!(f, "error: {m}"),
Error::Corrupt(m) => write!(f, "database disk image is malformed: {m}"),
Error::Io(m) => write!(f, "disk I/O error: {m}"),
Error::Busy => write!(f, "database is locked"),
Error::CantOpen(m) => write!(f, "unable to open database file: {m}"),
// The message already names the specific constraint (`UNIQUE
// constraint failed: t.a`, `CHECK constraint failed: …`, a `RAISE()`
// string, the STRICT `cannot store …` text), matching sqlite's
// `errmsg` verbatim — so no redundant outer prefix is added.
Error::Constraint(m) => write!(f, "{m}"),
Error::Parse(m) | Error::ParseAt(m, _) => write!(f, "SQL error: {m}"),
Error::Unsupported(m) => write!(f, "not yet implemented: {m}"),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for Error {}
#[cfg(test)]
mod tests {
use super::Error;
use alloc::string::ToString;
#[test]
fn message_keyed_extended_codes() {
// A datatype mismatch is SQLITE_MISMATCH (20), a too-big value is
// SQLITE_TOOBIG (18); every other generic error stays SQLITE_ERROR (1).
assert_eq!(Error::Error("datatype mismatch".to_string()).code(), 20);
assert_eq!(
Error::ErrorAt("datatype mismatch".to_string(), 4).code(),
20
);
assert_eq!(
Error::Error("string or blob too big".to_string()).code(),
18
);
assert_eq!(Error::Error("no such column: x".to_string()).code(), 1);
assert_eq!(Error::Constraint("UNIQUE …".to_string()).code(), 19);
}
}