Skip to main content

inillucent_base/
error.rs

1//! The stable error model every inillucent layer reports through.
2//!
3//! Invariant: an error's numeric code, its effect on the connection, and its
4//! effect on the transaction are decided by `compat/errors.toml` and nothing
5//! else. Code that wants a new failure mode adds a manifest row; it cannot
6//! invent a code at the call site.
7//!
8//! A `DbError` carries two messages. `message` is safe to hand to a caller and
9//! never contains a file-system path, a bound value, or page bytes. `detail` is
10//! for diagnostics that stay inside the process unless an explicit diagnostic
11//! callback asks for them.
12
13use core::fmt;
14
15mod generated {
16    //! The table generated from `compat/errors.toml` at build time.
17    #![allow(missing_docs)]
18    use super::ExtendedCode;
19    include!(concat!(env!("OUT_DIR"), "/errors_generated.rs"));
20}
21
22pub use generated::{
23    ExtendedRow, PrimaryCode, PrimaryRow, ERROR_TABLE_REFERENCE, ERROR_TABLE_SOURCE, EXTENDED_ROWS,
24    PRIMARY_ROWS,
25};
26
27/// The result type used everywhere below the public facade.
28pub type DbResult<T> = Result<T, DbError>;
29
30/// The row returned for a primary code the generated table does not list.
31///
32/// Every `PrimaryCode` variant is generated from the manifest, so this is
33/// unreachable in practice. It exists so that `PrimaryCode::row` is total
34/// without an `unwrap` on a hot path.
35static UNRECOGNISED_PRIMARY_ROW: PrimaryRow = PrimaryRow {
36    code: PrimaryCode::Error,
37    value: 1,
38    c_name: "SQLITE_ERROR",
39    message: "SQL logic error",
40    connection_usable: true,
41    statement_resettable: true,
42    transaction_rolled_back: false,
43};
44
45/// A SQLite extended result code.
46///
47/// This is a newtype over the numeric value rather than an enum because the
48/// extended space is open: an unrecognised code from an extension or a future
49/// SQLite release still has to round-trip through the C surface unchanged.
50#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
51pub struct ExtendedCode(pub i32);
52
53impl ExtendedCode {
54    /// Returns the extended code for a primary code with no refinement, which
55    /// is the primary code's own numeric value.
56    pub fn from_primary(code: PrimaryCode) -> ExtendedCode {
57        ExtendedCode(code.value())
58    }
59
60    /// Returns the numeric value a C caller sees.
61    pub fn value(self) -> i32 {
62        self.0
63    }
64
65    /// Returns the generated row for this code, if the manifest knows it.
66    pub fn row(self) -> Option<&'static ExtendedRow> {
67        EXTENDED_ROWS.iter().find(|row| row.value == self.0)
68    }
69
70    /// Returns the primary code this extended code refines.
71    ///
72    /// SQLite derives the primary code from the low eight bits, so an unknown
73    /// extended code still resolves to a usable primary code rather than an
74    /// error of its own.
75    pub fn primary(self) -> PrimaryCode {
76        match self.row() {
77            Some(row) => row.primary,
78            None => PrimaryCode::from_code(self.0 & 0xff).unwrap_or(PrimaryCode::Error),
79        }
80    }
81
82    /// Returns the C macro name when the manifest knows this code.
83    pub fn c_name(self) -> Option<&'static str> {
84        self.row().map(|row| row.c_name)
85    }
86
87    /// Returns the default English message for this code.
88    pub fn message(self) -> &'static str {
89        match self.row() {
90            Some(row) => row.message,
91            None => self.primary().message(),
92        }
93    }
94}
95
96impl PrimaryCode {
97    /// Returns the numeric value a C caller sees.
98    pub fn value(self) -> i32 {
99        self.row().value
100    }
101
102    /// Returns the generated row for this code.
103    ///
104    /// Every variant comes from the manifest, so the lookup always succeeds;
105    /// the fallback keeps the function total without an `unwrap`.
106    pub fn row(self) -> &'static PrimaryRow {
107        match PRIMARY_ROWS.iter().find(|row| row.code == self) {
108            Some(row) => row,
109            None => &UNRECOGNISED_PRIMARY_ROW,
110        }
111    }
112
113    /// Resolves a numeric primary code, returning `None` for a number the
114    /// manifest does not list.
115    ///
116    /// Named for what it takes. It was `from_value` until task-1961, which is
117    /// the name of the datum-to-SQL-value conversion everywhere else in this
118    /// workspace, and this function has nothing to do with that one: its
119    /// argument is an error number out of `compat/errors.toml`.
120    pub fn from_code(number: i32) -> Option<PrimaryCode> {
121        PRIMARY_ROWS
122            .iter()
123            .find(|row| row.value == number)
124            .map(|row| row.code)
125    }
126
127    /// Returns the C macro name.
128    pub fn c_name(self) -> &'static str {
129        self.row().c_name
130    }
131
132    /// Returns the default English message for this code.
133    pub fn message(self) -> &'static str {
134        self.row().message
135    }
136}
137
138/// The name of an attached database, used to attribute an error to one file.
139#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
140pub struct DatabaseName(String);
141
142impl DatabaseName {
143    /// Wraps a schema name such as `main`, `temp`, or an attached alias.
144    pub fn new(name: impl Into<String>) -> DatabaseName {
145        DatabaseName(name.into())
146    }
147
148    /// Returns the schema name.
149    pub fn as_str(&self) -> &str {
150        &self.0
151    }
152}
153
154impl fmt::Display for DatabaseName {
155    /// Writes the schema name.
156    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
157        formatter.write_str(&self.0)
158    }
159}
160
161/// What a failing statement leaves behind.
162///
163/// **SQLite's five conflict algorithms differ in three ways, and only this one
164/// belongs to the layer above the write path.** `IGNORE` and `REPLACE` resolve
165/// the row and carry on, which is a decision the write path makes for itself;
166/// the other three all report the failure and differ solely in how much of what
167/// has already been written goes back. The write path cannot make *that*
168/// decision - it owns neither the undo buffer nor the transaction - so it says
169/// what it wants and the engine does it.
170///
171/// An error that was never tagged reads as [`Unwind::Statement`], which is
172/// SQLite's default `ABORT`. That is deliberate, and it is why a `STRICT` type
173/// failure, a foreign-key violation and a trigger's `RAISE` all undo the
174/// statement without any of their raise sites having heard of this type.
175#[derive(Clone, Copy, Debug, Eq, PartialEq)]
176pub enum Unwind {
177    /// The statement's own writes go back and the transaction is kept -
178    /// SQLite's `ABORT`, and what every unqualified statement gets.
179    Statement,
180    /// Nothing goes back: the rows written before the failure stay - SQLite's
181    /// `FAIL`.
182    Nothing,
183    /// The statement's writes and the whole open transaction go back - SQLite's
184    /// `ROLLBACK`.
185    Transaction,
186}
187
188/// Everything an error carries beyond its code.
189///
190/// Held behind a pointer because almost every error has none of it: the code's
191/// own manifest message is the message, and there is no offset, database or
192/// detail. Keeping the four fields inline made `DbError` 88 bytes, and a
193/// `DbResult` is returned once per bytecode instruction - so every instruction
194/// in the machine, and every `?` on every path in the engine, was moving 88
195/// bytes to describe a failure that had not happened. Boxing it makes the error
196/// half of a `Result` one pointer, and costs an allocation only on the paths
197/// that are already failing or already building a message.
198#[derive(Clone, Debug, Eq, PartialEq, Default)]
199struct ErrorContext {
200    /// A message that replaces the code's own, when one was attached.
201    message: Option<String>,
202    /// The byte offset in the SQL text, when the error has one.
203    sql_offset: Option<u32>,
204    /// The schema the error was attributed to, when it was attributed.
205    database: Option<DatabaseName>,
206    /// Diagnostic text that never leaves the process.
207    detail: Option<String>,
208    /// The construct the engine has not implemented, when that is why it
209    /// refused.
210    ///
211    /// **A fact the engine already knows, carried rather than re-derived.**
212    /// `inillucent-exec`'s physical pass and `inillucent-sql`'s binder each
213    /// have a single `unsupported` helper, and both used to be flattened into
214    /// an ordinary `SQLITE_MISUSE` whose only distinguishing mark was the
215    /// wording of its sentence. A caller that wanted to tell "this engine
216    /// cannot do that yet" from "you typed it wrong" therefore had to match on
217    /// prose, which works until somebody improves the prose. This field is that
218    /// caller's answer, and it is set at the same two places the sentence is
219    /// written so the two cannot disagree.
220    ///
221    /// It changes no code and no message: an error carrying it reports the same
222    /// `SQLITE_MISUSE` and the same text it always did.
223    unsupported: Option<String>,
224    /// What this installation has not got, when that is why it refused.
225    ///
226    /// **The same mechanism as `unsupported`, for the other reason a call that
227    /// is written correctly cannot be answered.** `unsupported` means the
228    /// engine never built the construct, and nothing done on this machine will
229    /// change that. This field means the engine built it and the machine is
230    /// missing something the caller can go and install - the embedding model
231    /// `embed(TEXT)` runs, and the ONNX Runtime under it.
232    ///
233    /// The two are kept apart because the answer a caller needs is different:
234    /// one says stop asking, the other says run this command. Before this
235    /// field, a refusal of the second kind left the engine as a bare
236    /// `SQLITE_MISUSE` and reached `inillucent-driver` as the status `syntax`,
237    /// so `SELECT length(embed('hello'))` on a machine that had never run
238    /// `inillucent setup-embeddings` told a person their SQL was malformed.
239    ///
240    /// Like `unsupported` it is safe to show a caller: it names a component,
241    /// never a path and never a bound value.
242    requirement: Option<String>,
243    /// How much of what has been written this failure undoes, when a conflict
244    /// algorithm said.
245    ///
246    /// `None` reads as [`Unwind::Statement`]. It is set at the innermost site
247    /// that knows - a `RAISE`, then a constraint carrying its own clause, then
248    /// the statement's own `OR` - and only when it is not already set, so the
249    /// precedence is the order those sites run in rather than a rule written
250    /// down anywhere.
251    unwind: Option<Unwind>,
252    /// Whether the unwind was written out by a `RAISE`, which nothing overrides.
253    ///
254    /// **The one place SQLite's precedence is not innermost-first.** A trigger
255    /// body's `RAISE(ROLLBACK)` beats the statement that fired it, and the
256    /// statement that fired it beats a nested statement's own `OR` clause and
257    /// any clause written on a constraint - so "set if absent" gets two of the
258    /// three right and this flag gets the third.
259    unwind_explicit: bool,
260}
261
262/// A inillucent error: a stable code plus the context a caller may safely see.
263///
264/// Equality is over what the error *says*, not over how it is stored: an error
265/// carrying no context and one carrying a message equal to its code's own
266/// message are the same error, and were the same error before the context was
267/// boxed. `PartialEq` is written out below for that reason rather than derived.
268#[derive(Clone, Debug)]
269pub struct DbError {
270    extended: ExtendedCode,
271    context: Option<Box<ErrorContext>>,
272}
273
274impl PartialEq for DbError {
275    /// Compares the code and every effective field, so the boxing is invisible.
276    ///
277    /// **`unwind` is deliberately not one of them.** Equality here is over what
278    /// the error *says*; the unwind is about what the engine does with it, and
279    /// folding it in would make an error tagged at a raise site unequal to the
280    /// same error written out in a test - a comparison that would start failing
281    /// for a reason nothing in the message could show.
282    fn eq(&self, other: &DbError) -> bool {
283        self.extended == other.extended
284            && self.message() == other.message()
285            && self.sql_offset() == other.sql_offset()
286            && self.database() == other.database()
287            && self.detail() == other.detail()
288            && self.unsupported() == other.unsupported()
289            && self.requirement() == other.requirement()
290    }
291}
292
293impl Eq for DbError {}
294
295impl DbError {
296    /// Returns the context, creating an empty one to write into.
297    fn context_mut(&mut self) -> &mut ErrorContext {
298        self.context
299            .get_or_insert_with(Box::<ErrorContext>::default)
300    }
301
302    /// Builds an error from an extended code, taking the manifest's message.
303    pub fn new(extended: ExtendedCode) -> DbError {
304        DbError {
305            extended,
306            context: None,
307        }
308    }
309
310    /// Builds an error from a primary code with no extended refinement.
311    pub fn primary(code: PrimaryCode) -> DbError {
312        DbError::new(ExtendedCode::from_primary(code))
313    }
314
315    /// Replaces the caller-visible message.
316    ///
317    /// The replacement must stay free of paths, bound values, and page bytes;
318    /// anything sensitive belongs in `with_detail` instead.
319    pub fn with_message(mut self, message: impl Into<String>) -> DbError {
320        self.context_mut().message = Some(message.into());
321        self
322    }
323
324    /// Attaches diagnostic text that stays inside the process.
325    pub fn with_detail(mut self, detail: impl Into<String>) -> DbError {
326        self.context_mut().detail = Some(detail.into());
327        self
328    }
329
330    /// Records that this refusal is a construct the engine has not implemented.
331    ///
332    /// Unlike a message, this is safe to show a caller and is meant to be: it
333    /// names a SQL construct and never a path or a bound value.
334    ///
335    /// @param what - the construct, in the words the refusal already uses
336    pub fn with_unsupported(mut self, what: impl Into<String>) -> DbError {
337        self.context_mut().unsupported = Some(what.into());
338        self
339    }
340
341    /// Records that this refusal is about something this installation has not
342    /// got, which the caller can install.
343    ///
344    /// Safe to show a caller, and meant to be: it names a component, such as
345    /// "an embedding model", and never a path or a bound value.
346    ///
347    /// @param what - the missing component, in the words the refusal already uses
348    pub fn with_requirement(mut self, what: impl Into<String>) -> DbError {
349        self.context_mut().requirement = Some(what.into());
350        self
351    }
352
353    /// Records how much of what has been written this failure undoes, unless
354    /// something closer to the failure has already said.
355    ///
356    /// **Set if absent, never overwritten**, because the sites that know run
357    /// innermost first and SQLite's precedence is exactly that order: an
358    /// explicit `RAISE(ROLLBACK)` beats the enclosing statement's `OR FAIL`,
359    /// and a statement's `OR` beats the clause written on the constraint. A
360    /// setter that overwrote would invert it, and the inversion is invisible -
361    /// the error text is the same either way and only the rows differ.
362    ///
363    /// @param unwind - what this failure undoes
364    pub fn or_unwind(mut self, unwind: Unwind) -> DbError {
365        let context = self.context_mut();
366        if context.unwind.is_none() {
367            context.unwind = Some(unwind);
368        }
369        self
370    }
371
372    /// Records an unwind a `RAISE` wrote out, which nothing overrides.
373    ///
374    /// `RAISE(ROLLBACK, ...)` in a trigger body rolls the transaction back
375    /// whatever the statement that fired the trigger asked for, which is the
376    /// one direction [`DbError::or_unwind`]'s innermost-first rule gets wrong.
377    ///
378    /// @param unwind - what the `RAISE` undoes
379    pub fn with_raised_unwind(mut self, unwind: Unwind) -> DbError {
380        let context = self.context_mut();
381        context.unwind = Some(unwind);
382        context.unwind_explicit = true;
383        self
384    }
385
386    /// Records the unwind of the statement a trigger's statements are nested
387    /// in, which beats theirs and beats a constraint's own clause.
388    ///
389    /// SQLite's rule: "if an `ON CONFLICT` clause is specified as part of the
390    /// statement causing the trigger to fire, then conflict handling policy of
391    /// the outer statement is used instead". So this is the one setter that
392    /// overwrites - and it still yields to a `RAISE`.
393    ///
394    /// @param unwind - what the outermost statement's `OR` clause undoes
395    pub fn with_outer_unwind(mut self, unwind: Unwind) -> DbError {
396        let context = self.context_mut();
397        if !context.unwind_explicit {
398            context.unwind = Some(unwind);
399        }
400        self
401    }
402
403    /// Returns how much of what has been written this failure undoes.
404    ///
405    /// An untagged error undoes the statement, which is SQLite's `ABORT`.
406    pub fn unwind(&self) -> Unwind {
407        self.context
408            .as_ref()
409            .and_then(|context| context.unwind)
410            .unwrap_or(Unwind::Statement)
411    }
412
413    /// Attaches the byte offset in the SQL text that produced the error.
414    pub fn with_sql_offset(mut self, offset: u32) -> DbError {
415        self.context_mut().sql_offset = Some(offset);
416        self
417    }
418
419    /// Attaches the schema name of the database the error came from.
420    pub fn with_database(mut self, database: DatabaseName) -> DbError {
421        self.context_mut().database = Some(database);
422        self
423    }
424
425    /// Returns the primary result code.
426    pub fn code(&self) -> PrimaryCode {
427        self.extended.primary()
428    }
429
430    /// Returns the extended result code.
431    pub fn extended(&self) -> ExtendedCode {
432        self.extended
433    }
434
435    /// Returns the caller-visible message.
436    ///
437    /// An error that was never given one answers with its code's own message,
438    /// which is what it was constructed with before the context was boxed.
439    pub fn message(&self) -> &str {
440        match self
441            .context
442            .as_ref()
443            .and_then(|context| context.message.as_deref())
444        {
445            Some(message) => message,
446            None => self.extended.message(),
447        }
448    }
449
450    /// Returns the internal diagnostic text, if any was attached.
451    pub fn detail(&self) -> Option<&str> {
452        self.context
453            .as_ref()
454            .and_then(|context| context.detail.as_deref())
455    }
456
457    /// Returns the construct the engine has not implemented, when that is why
458    /// it refused.
459    ///
460    /// `None` for every other failure, including a statement that is simply
461    /// wrong - which is the distinction it exists to make.
462    pub fn unsupported(&self) -> Option<&str> {
463        self.context
464            .as_ref()
465            .and_then(|context| context.unsupported.as_deref())
466    }
467
468    /// Returns what this installation is missing, when that is why it refused.
469    ///
470    /// `None` for every other failure, including a construct the engine has
471    /// never built - that is [`DbError::unsupported`], and it is a different
472    /// answer to give a caller.
473    pub fn requirement(&self) -> Option<&str> {
474        self.context
475            .as_ref()
476            .and_then(|context| context.requirement.as_deref())
477    }
478
479    /// Returns the SQL byte offset, if the error has one.
480    pub fn sql_offset(&self) -> Option<u32> {
481        self.context.as_ref().and_then(|context| context.sql_offset)
482    }
483
484    /// Returns the schema name, if the error was attributed to one.
485    pub fn database(&self) -> Option<&DatabaseName> {
486        self.context
487            .as_ref()
488            .and_then(|context| context.database.as_ref())
489    }
490
491    /// Reports whether the connection may still be used after this error.
492    pub fn connection_usable(&self) -> bool {
493        match self.extended.row() {
494            Some(row) => row.connection_usable,
495            None => self.code().row().connection_usable,
496        }
497    }
498
499    /// Reports whether the statement may be reset and stepped again.
500    pub fn statement_resettable(&self) -> bool {
501        match self.extended.row() {
502            Some(row) => row.statement_resettable,
503            None => self.code().row().statement_resettable,
504        }
505    }
506
507    /// Reports whether the error implicitly rolled the transaction back.
508    pub fn transaction_rolled_back(&self) -> bool {
509        match self.extended.row() {
510            Some(row) => row.transaction_rolled_back,
511            None => self.code().row().transaction_rolled_back,
512        }
513    }
514}
515
516impl fmt::Display for DbError {
517    /// Writes the safe message, and the SQL offset when one is known. The
518    /// internal detail is deliberately absent so that logging an error cannot
519    /// leak a path or a bound value.
520    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
521        match self.sql_offset() {
522            Some(offset) => write!(formatter, "{} (at SQL byte {offset})", self.message()),
523            None => formatter.write_str(self.message()),
524        }
525    }
526}
527
528impl std::error::Error for DbError {}
529
530/// Builds a `SQLITE_CORRUPT` error for malformed persistent bytes.
531pub fn corrupt(detail: impl Into<String>) -> DbError {
532    DbError::primary(PrimaryCode::Corrupt).with_detail(detail)
533}
534
535/// Builds a `SQLITE_TOOBIG` error for a value or arithmetic result that does
536/// not fit the format's range.
537pub fn too_big(detail: impl Into<String>) -> DbError {
538    DbError::primary(PrimaryCode::TooBig).with_detail(detail)
539}
540
541/// Builds a `SQLITE_NOMEM` error for a fallible allocation that failed.
542pub fn no_mem(detail: impl Into<String>) -> DbError {
543    DbError::primary(PrimaryCode::NoMem).with_detail(detail)
544}
545
546/// Builds a `SQLITE_MISUSE` error for an API contract the caller broke.
547///
548/// **What it is given becomes the internal detail and not the message**, so an
549/// error built this way answers `message()` with its primary code's own text:
550/// "bad parameter or other API misuse". That is right for the thing the name
551/// says - a contract the *caller* broke, whose explanation may name a file or a
552/// value - and wrong for a refusal about a statement, which is what most of the
553/// engine uses it for. [`refusal`] is the one to use for those.
554pub fn misuse(detail: impl Into<String>) -> DbError {
555    DbError::primary(PrimaryCode::Misuse).with_detail(detail)
556}
557
558/// Builds a `SQLITE_MISUSE` error whose sentence is about the caller's own
559/// statement, so it is the message **and** the detail.
560///
561/// The distinction from [`misuse`] is which field the sentence lands in, and it
562/// was a real defect for as long as only one of them existed. `no such table:
563/// peple`, `table t already exists` and `UNIQUE constraint failed: t.a` are all
564/// things a person needs to read, and they were all going into the field this
565/// module documents as staying inside the process - so a caller reading
566/// `message()`, which is the field it is *told* to read, got "bad parameter or
567/// other API misuse" for every one of them. `inillucent-cli::shell::reason` and
568/// `inillucent-compat`'s `readgate::why` had each independently worked around
569/// it with `detail().unwrap_or(message())`.
570///
571/// A refusal built this way must stay free of paths, bound values and page
572/// bytes, exactly as [`DbError::with_message`] requires - which a sentence about
573/// a statement's own tables, columns and constructs is. Anything naming a file
574/// belongs in [`misuse`].
575///
576/// The detail is set as well as the message so that every existing reader of
577/// `detail()` sees exactly what it saw before.
578///
579/// @param said - the sentence, safe for a caller to read
580pub fn refusal(said: impl Into<String>) -> DbError {
581    let said = said.into();
582    DbError::primary(PrimaryCode::Misuse)
583        .with_message(said.clone())
584        .with_detail(said)
585}
586
587/// Builds a `SQLITE_ERROR` refusal about the caller's own statement - the
588/// same shape as [`refusal`], but for the far more common case where SQLite's
589/// real answer is code 1 rather than `SQLITE_MISUSE`'s 21.
590///
591/// **Most engine-level statement refusals are `SQLITE_ERROR`, and `refusal`
592/// answers `SQLITE_MISUSE` unconditionally.** `bind.rs`'s own `refused`
593/// function found and fixed this for the parser and binder - `PrimaryCode::
594/// from `ParseError::code`, not from `refusal`'s hardcoded `Misuse` - because
595/// a parse or bind refusal is `SQLITE_ERROR` in SQLite, measured through
596/// `dml_differential.rs`. Refusals raised directly by the engine's execution
597/// code (`CREATE VIRTUAL TABLE` naming no such module, `VACUUM` from inside a
598/// transaction) go through `refusal` directly rather than through a
599/// `ParseError`, so they did not get that fix and still answer 21 where the
600/// pinned reference answers 1. This is the same fix, for that path: use it in
601/// place of `refusal` once the reference has been checked and answers 1 - do
602/// not switch a call over on the strength of this doc comment alone.
603///
604/// @param said - the sentence, safe for a caller to read
605pub fn statement_refusal(said: impl Into<String>) -> DbError {
606    let said = said.into();
607    DbError::primary(PrimaryCode::Error)
608        .with_message(said.clone())
609        .with_detail(said)
610}
611
612/// Builds a `SQLITE_BUSY` refusal about another process holding the file.
613///
614/// **The sentence is the message as well as the detail, because the caller can
615/// act on it.** What a contended open used to answer was the VFS's own
616/// "a writer holds PENDING", which names an internal lock level, is wrong
617/// whenever the holder is a reader, and says nothing about how long the wait
618/// was or what would change the outcome (task-1979, C6). A sentence built here
619/// names who holds the file, what they hold it for, and the pragma that governs
620/// the wait.
621///
622/// It must stay free of paths and bound values, exactly as
623/// [`DbError::with_message`] requires; the path is added by the caller that
624/// knows it, in the detail.
625///
626/// @param said - the sentence, safe for a caller to read
627pub fn busy(said: impl Into<String>) -> DbError {
628    let said = said.into();
629    DbError::primary(PrimaryCode::Busy)
630        .with_message(said.clone())
631        .with_detail(said)
632}
633
634/// Builds a refusal about a component this installation has not got, which the
635/// caller can install.
636///
637/// **The third member of the family, and the one `embed(TEXT)` needed.**
638/// [`misuse`] hides its sentence in the detail, [`refusal`] shows it, and both
639/// reach a driver as the status `syntax` - which is the right reading of a
640/// statement that is wrong and the wrong reading of a statement that is fine on
641/// a machine that is not ready. This one shows the sentence *and* marks why,
642/// so `inillucent-driver` answers `invalid_state` instead.
643///
644/// `what` is the component, in the words the sentence already uses, and `said`
645/// is the sentence. Both are shown to the caller, so both must stay free of
646/// paths and bound values exactly as [`DbError::with_message`] requires. A
647/// sentence that has to name the directory it looked in puts that in
648/// `with_detail`.
649///
650/// @param what - the missing component, such as "an embedding model"
651/// @param said - the sentence, naming what installs it
652pub fn unmet_requirement(what: impl Into<String>, said: impl Into<String>) -> DbError {
653    let said = said.into();
654    DbError::primary(PrimaryCode::Misuse)
655        .with_message(said.clone())
656        .with_detail(said)
657        .with_requirement(what)
658}
659
660#[cfg(test)]
661mod tests {
662    use super::*;
663
664    /// Every generated row must round-trip through its numeric value, which is
665    /// what the C surface and the oracle protocol both depend on.
666    #[test]
667    fn primary_codes_round_trip_through_their_numeric_value() {
668        for row in PRIMARY_ROWS.iter() {
669            assert_eq!(PrimaryCode::from_code(row.value), Some(row.code));
670            assert_eq!(row.code.value(), row.value);
671            assert_eq!(row.code.c_name(), row.c_name);
672        }
673    }
674
675    /// An extended code resolves to the primary code the manifest names, and
676    /// its low byte agrees with that primary code, which is the rule SQLite
677    /// documents for callers that mask the value themselves.
678    #[test]
679    fn extended_codes_agree_with_their_primary_code() {
680        for row in EXTENDED_ROWS.iter() {
681            let extended = ExtendedCode(row.value);
682            assert_eq!(extended.primary(), row.primary);
683            assert_eq!(row.value & 0xff, row.primary.value());
684        }
685    }
686
687    /// An extended code the manifest has never seen still resolves to a usable
688    /// primary code instead of failing, because extensions may invent codes.
689    #[test]
690    fn unknown_extended_codes_fall_back_to_their_low_byte() {
691        let invented = ExtendedCode((99 << 8) | PrimaryCode::Constraint.value());
692        assert!(invented.row().is_none());
693        assert_eq!(invented.primary(), PrimaryCode::Constraint);
694        assert_eq!(invented.message(), PrimaryCode::Constraint.message());
695    }
696
697    /// The manifest must not contain two rows with the same numeric value; a
698    /// duplicate would make the C surface ambiguous.
699    #[test]
700    fn numeric_values_are_unique() {
701        let mut values: Vec<i32> = PRIMARY_ROWS.iter().map(|row| row.value).collect();
702        values.extend(EXTENDED_ROWS.iter().map(|row| row.value));
703        let count = values.len();
704        values.sort_unstable();
705        values.dedup();
706        assert_eq!(
707            values.len(),
708            count,
709            "duplicate result code in compat/errors.toml"
710        );
711    }
712
713    /// Display must never carry the internal detail, because callers log it.
714    #[test]
715    fn display_hides_internal_detail() {
716        let error = DbError::primary(PrimaryCode::CantOpen)
717            .with_detail("C:/secret/path/app.db")
718            .with_sql_offset(12);
719        let rendered = error.to_string();
720        assert!(!rendered.contains("secret"), "{rendered}");
721        assert!(rendered.contains("at SQL byte 12"), "{rendered}");
722        assert_eq!(error.detail(), Some("C:/secret/path/app.db"));
723    }
724
725    /// The three `SQLITE_MISUSE` builders differ in exactly one thing - whether
726    /// the sentence they are given is the one a caller reads - and that is the
727    /// difference `embed(TEXT)` was on the wrong side of.
728    ///
729    /// `misuse` is the one that hides it, and the test says so rather than
730    /// leaving a reader to infer it from the doc comment: a caller reading
731    /// `message()` gets the primary code's manifest text and has to open the
732    /// diagnostic detail to find out anything at all.
733    #[test]
734    fn only_two_of_the_three_misuse_builders_show_their_sentence() {
735        let said = "embed: no embedding model is installed";
736        assert_eq!(misuse(said).message(), PrimaryCode::Misuse.message());
737        assert_eq!(misuse(said).detail(), Some(said));
738        assert_eq!(refusal(said).message(), said);
739        assert_eq!(
740            unmet_requirement("an embedding model", said).message(),
741            said
742        );
743    }
744
745    /// A refusal about a missing component says which component, and says it
746    /// through a field rather than through its wording.
747    ///
748    /// The marker is what `inillucent-driver` reads to answer `invalid_state`
749    /// instead of `syntax`. Deriving it from the sentence instead would work
750    /// until somebody improved the sentence, which is the argument the
751    /// `unsupported` marker beside it was added on.
752    #[test]
753    fn a_missing_component_is_named_by_a_marker_and_is_not_an_unimplemented_one() {
754        let error = unmet_requirement(
755            "an embedding model",
756            "embed: no embedding model is installed. Run `inillucent setup-embeddings`",
757        );
758        assert_eq!(error.requirement(), Some("an embedding model"));
759        assert_eq!(
760            error.unsupported(),
761            None,
762            "it is built, it is not installed"
763        );
764        assert_eq!(error.code(), PrimaryCode::Misuse);
765        assert!(error.to_string().contains("setup-embeddings"), "{error}");
766        assert_eq!(
767            refusal("no such table: peple").requirement(),
768            None,
769            "an ordinary statement refusal is missing nothing"
770        );
771    }
772
773    /// The recovery contract is what callers branch on, so spot-check the rows
774    /// where it differs from the default.
775    #[test]
776    fn recovery_contract_matches_the_manifest() {
777        assert!(!DbError::primary(PrimaryCode::Misuse).connection_usable());
778        assert!(DbError::primary(PrimaryCode::Busy).connection_usable());
779        assert!(DbError::new(ExtendedCode::ABORT_ROLLBACK).transaction_rolled_back());
780        assert!(!DbError::new(ExtendedCode::CONSTRAINT_UNIQUE).transaction_rolled_back());
781    }
782}