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}