Skip to main content

clt_database/
error.rs

1use thiserror::Error;
2
3use crate::storage::page_cache::CacheError;
4
5#[derive(Debug, Clone, Error, miette::Diagnostic)]
6pub enum LimboError {
7    #[error("Corrupt database: {0}")]
8    Corrupt(String),
9    #[error("File is not a database")]
10    NotADB,
11    #[error("Internal error: {0}")]
12    InternalError(String),
13    #[error(transparent)]
14    CacheError(#[from] CacheError),
15    #[error("Database is full: {0}")]
16    DatabaseFull(String),
17    #[error("Parse error: {0}")]
18    ParseError(String),
19    #[error(transparent)]
20    #[diagnostic(transparent)]
21    LexerError(#[from] turso_parser::error::Error),
22    #[error("Conversion error: {0}")]
23    ConversionError(String),
24    #[error("Env variable error: {0}")]
25    EnvVarError(#[from] std::env::VarError),
26    #[error("Transaction error: {0}")]
27    TxError(String),
28    #[error(transparent)]
29    CompletionError(#[from] CompletionError),
30    #[error("Locking error: {0}")]
31    LockingError(String),
32    #[error("Parse error: {0}")]
33    ParseIntError(#[from] std::num::ParseIntError),
34    #[error("Parse error: {0}")]
35    ParseFloatError(#[from] std::num::ParseFloatError),
36    #[error("Parse error: {0}")]
37    InvalidDate(String),
38    #[error("Parse error: {0}")]
39    InvalidTime(String),
40    #[error("Modifier parsing error: {0}")]
41    InvalidModifier(String),
42    #[error("Invalid argument supplied: {0}")]
43    InvalidArgument(String),
44    #[error("Invalid formatter supplied: {0}")]
45    InvalidFormatter(String),
46    #[error("Runtime error: {0}")]
47    Constraint(String),
48    #[error("Runtime error: {0}")]
49    /// We need to specify for ROLLBACK|FAIL resolve types when to roll the tx back
50    /// so instead of matching on the string, we introduce a specific ForeignKeyConstraint error
51    ForeignKeyConstraint(String),
52    #[error("Runtime error: {1}")]
53    Raise(turso_parser::ast::ResolveType, String),
54    #[error("RaiseIgnore")]
55    RaiseIgnore,
56    #[error("Extension error: {0}")]
57    ExtensionError(String),
58    #[error("Runtime error: integer overflow")]
59    IntegerOverflow,
60    #[error("Runtime error: string or blob too big")]
61    TooBig,
62    #[error("Runtime error: database table is locked")]
63    TableLocked,
64    #[error("Error: Resource is read-only")]
65    ReadOnly,
66    #[error("Database is busy")]
67    Busy,
68    /// A transaction-control or savepoint operation, or a second concurrent
69    /// write statement, was rejected because another statement on the same
70    /// connection is still in progress. This carries SQLITE_BUSY semantics at
71    /// the API surface (SQLite reports these as SQLITE_BUSY, e.g. "cannot
72    /// commit transaction - SQL statements in progress"), but unlike `Busy`
73    /// it is not retryable in place and must never invoke the busy handler:
74    /// waiting cannot help, only the application finishing or resetting its
75    /// own statement can. The payload names the rejected operation.
76    #[error("{0} - SQL statements in progress")]
77    StatementsInProgress(&'static str),
78    #[error("interrupt")]
79    Interrupt,
80    #[error("Database snapshot is stale. You must rollback and retry the whole transaction.")]
81    BusySnapshot,
82    #[error("Conflict: {0}")]
83    Conflict(String),
84    #[error("Database schema changed")]
85    SchemaUpdated,
86    #[error("Database schema conflict")]
87    SchemaConflict,
88    #[error(
89        "Database is empty, header does not exist - page 1 should've been allocated before this"
90    )]
91    Page1NotAlloc,
92    #[error("Transaction terminated")]
93    TxTerminated,
94    #[error("Write-write conflict")]
95    WriteWriteConflict,
96    #[error("Commit dependency aborted")]
97    CommitDependencyAborted,
98    #[error("No such transaction ID: {0}")]
99    NoSuchTransactionID(String),
100    #[error("Null value")]
101    NullValue,
102    #[error("invalid column type")]
103    InvalidColumnType,
104    #[error("Invalid blob size, expected {0}")]
105    InvalidBlobSize(usize),
106    #[error("Planning error: {0}")]
107    PlanningError(String),
108    #[error("Checkpoint failed: {0}")]
109    CheckpointFailed(String),
110    #[error("Unsupported text encoding: {0}. Only UTF-8 is supported.")]
111    UnsupportedEncoding(String),
112    #[error("Out of memory")]
113    OutOfMemory,
114}
115
116impl From<crate::alloc::AllocError> for LimboError {
117    fn from(_: crate::alloc::AllocError) -> Self {
118        Self::OutOfMemory
119    }
120}
121
122impl From<crate::alloc::TryReserveError> for LimboError {
123    fn from(_: crate::alloc::TryReserveError) -> Self {
124        Self::OutOfMemory
125    }
126}
127
128#[cfg(not(nightly))]
129impl From<allocator_api2::collections::TryReserveError> for LimboError {
130    fn from(_: allocator_api2::collections::TryReserveError) -> Self {
131        Self::OutOfMemory
132    }
133}
134
135impl From<std::collections::TryReserveError> for LimboError {
136    fn from(_: std::collections::TryReserveError) -> Self {
137        Self::OutOfMemory
138    }
139}
140
141impl From<bumpalo::AllocErr> for LimboError {
142    fn from(_: bumpalo::AllocErr) -> Self {
143        Self::OutOfMemory
144    }
145}
146
147impl From<bumpalo::collections::CollectionAllocErr> for LimboError {
148    fn from(_: bumpalo::collections::CollectionAllocErr) -> Self {
149        Self::OutOfMemory
150    }
151}
152
153#[cfg(target_family = "unix")]
154impl From<rustix::io::Errno> for LimboError {
155    fn from(value: rustix::io::Errno) -> Self {
156        CompletionError::from(value).into()
157    }
158}
159
160#[cfg(all(target_os = "linux", clt_turso_feature = "io_uring"))]
161impl From<&'static str> for LimboError {
162    fn from(value: &'static str) -> Self {
163        CompletionError::UringIOError(value).into()
164    }
165}
166
167#[derive(Debug, Copy, Clone, PartialEq, Error)]
168pub enum CompletionError {
169    #[error("I/O error ({1}): {0}")]
170    IOError(std::io::ErrorKind, &'static str),
171    #[cfg(target_family = "unix")]
172    #[error("I/O error: {0}")]
173    RustixIOError(#[from] rustix::io::Errno),
174    #[cfg(all(target_os = "linux", clt_turso_feature = "io_uring"))]
175    #[error("I/O error: {0}")]
176    // TODO: if needed create an enum for IO Uring errors so that we don't have to pass strings around
177    UringIOError(&'static str),
178    #[error("Completion was aborted")]
179    Aborted,
180    #[error("Decryption failed for page={page_idx}")]
181    DecryptionError { page_idx: usize },
182    #[error("I/O error: partial write")]
183    ShortWrite,
184    #[error("I/O error: short read on page {page_idx}: expected {expected} bytes, got {actual}")]
185    ShortRead {
186        page_idx: usize,
187        expected: usize,
188        actual: usize,
189    },
190    #[error("I/O error: short read on WAL frame at offset {offset}: expected {expected} bytes, got {actual}")]
191    ShortReadWalFrame {
192        offset: u64,
193        expected: usize,
194        actual: usize,
195    },
196    #[error("WAL frame page mismatch at frame {frame_id}: expected page {expected}, got {actual}")]
197    WalFramePageMismatch {
198        frame_id: u64,
199        expected: usize,
200        actual: u32,
201    },
202    #[error("Checksum mismatch on page {page_id}: expected {expected}, got {actual}")]
203    ChecksumMismatch {
204        page_id: usize,
205        expected: u64,
206        actual: u64,
207    },
208    #[error("tursodb not compiled with checksum feature")]
209    ChecksumNotEnabled,
210}
211
212/// Convert a `std::io::Error` into a `LimboError` with an operation label.
213pub fn io_error(e: std::io::Error, op: &'static str) -> LimboError {
214    LimboError::CompletionError(CompletionError::IOError(e.kind(), op))
215}
216
217#[cold]
218// makes all branches that return errors marked as unlikely
219pub(crate) const fn cold_return<T>(v: T) -> T {
220    v
221}
222
223#[macro_export]
224macro_rules! bail_parse_error {
225    ($($arg:tt)*) => {
226        return $crate::error::cold_return(Err($crate::error::LimboError::ParseError(format!($($arg)*))))
227    };
228}
229
230#[macro_export]
231macro_rules! bail_corrupt_error {
232    ($($arg:tt)*) => {
233        return $crate::error::cold_return(Err($crate::error::LimboError::Corrupt(format!($($arg)*))))
234    };
235}
236
237/// Bounds-checked buffer slicing that returns `LimboError::Corrupt` on out-of-bounds.
238///
239/// Accepts any range expression: `buf, pos..`, `buf, start..end`, etc.
240#[macro_export]
241macro_rules! slice_in_bounds_or_corrupt {
242    ($buf:expr, $range:expr) => {
243        $buf.get($range).ok_or_else(|| {
244            $crate::error::cold_return($crate::error::LimboError::Corrupt(format!(
245                "range {:?} out of bounds for buffer size {}",
246                $range,
247                $buf.len()
248            )))
249        })?
250    };
251}
252
253/// Asserts a condition or bails with `LimboError::Corrupt`.
254///
255/// Usage:
256///   `assert_or_bail_corrupt!(condition, "message {}", arg)`
257#[macro_export]
258macro_rules! assert_or_bail_corrupt {
259    ($cond:expr, $($arg:tt)*) => {
260        if !($cond) {
261            $crate::bail_corrupt_error!($($arg)*);
262        }
263    };
264}
265
266#[macro_export]
267macro_rules! bail_constraint_error {
268    ($($arg:tt)*) => {
269        return $crate::error::cold_return(Err($crate::error::LimboError::Constraint(format!($($arg)*))))
270    };
271}
272
273impl From<turso_ext::ResultCode> for LimboError {
274    fn from(err: turso_ext::ResultCode) -> Self {
275        cold_return(LimboError::ExtensionError(err.to_string()))
276    }
277}
278
279pub const SQLITE_ERROR: usize = 1;
280pub const SQLITE_CONSTRAINT: usize = 19;
281pub const SQLITE_CONSTRAINT_CHECK: usize = SQLITE_CONSTRAINT | (1 << 8);
282pub const SQLITE_CONSTRAINT_PRIMARYKEY: usize = SQLITE_CONSTRAINT | (6 << 8);
283#[allow(dead_code)]
284pub const SQLITE_CONSTRAINT_FOREIGNKEY: usize = SQLITE_CONSTRAINT | (3 << 8);
285pub const SQLITE_CONSTRAINT_NOTNULL: usize = SQLITE_CONSTRAINT | (5 << 8);
286pub const SQLITE_CONSTRAINT_TRIGGER: usize = SQLITE_CONSTRAINT | (7 << 8);
287pub const SQLITE_FULL: usize = 13; // we want this in autoincrement - incase if user inserts max allowed int
288pub const SQLITE_CONSTRAINT_UNIQUE: usize = 2067;
289// Standard SQLite error code; kept for documentation and potential
290// reuse. The sequence inner-tx wrap used to emit Insn::Halt with this
291// code, but halt()'s constraint catch-all mis-wrapped it; Busy is now
292// returned directly via Err(LimboError::Busy) from
293// op_sequence_commit_inner_tx.
294#[allow(dead_code)]
295pub const SQLITE_BUSY: usize = 5;