Skip to main content

inillucent_cli/
shell.rs

1//! The shell's state, and the loop that reads a line and decides what it is.
2//!
3//! Invariant: the shell is an adapter. It parses dot commands, formats output
4//! and manages files; every statement it runs goes through the public `inillucent`
5//! facade, and it never reaches past it. That is the rule that keeps the shell
6//! from becoming a second, slightly different database - which is exactly what
7//! happens to a shell that starts "just reading the schema directly".
8//!
9//! Input is accumulated until it is a complete statement. That is the one piece
10//! of real logic here and it is not a nicety: a `CREATE TRIGGER` spans many
11//! lines and holds semicolons inside its body, so "ends with a semicolon" is
12//! wrong and the engine's own parser has to be the one that says when a
13//! statement is finished.
14
15use std::io::Write;
16
17use inillucent_engine::connect::{Connection, Database};
18use inillucent_tree::datum::{owned_row_values, OwnedDatum};
19use inillucent_value::Value;
20
21use crate::render::{render, Layout, Mode};
22
23/// Everything the shell remembers between lines.
24/// One open database and the session statements on it belong to.
25pub struct Opened {
26    /// The database.
27    ///
28    /// A connection is a borrow of it rather than a thing of its own, so one is
29    /// made where it is used instead of being stored - storing it beside the
30    /// database it borrows would be a self-referential struct for no gain.
31    database: Database,
32    /// The session every one of those borrows is a continuation of.
33    ///
34    /// **Because a shell is one connection, not one per statement.** Temporary
35    /// objects belong to a session: `CREATE TEMP TABLE t(a)` puts `t` in the
36    /// session's own database, and a `SELECT` on a *different* session cannot
37    /// see it. Calling `Database::connect` per statement opened a new session
38    /// each time, so the shell reported success on the `CREATE` and then
39    /// `no such table: t` on the very next line.
40    ///
41    /// The engine was fixed to add `connect_as` for callers that hand out a
42    /// connection per call over one logical connection; the shell is one of
43    /// those and was not converted.
44    session: u64,
45    /// Where the database came from, for `.databases` and the prompt.
46    path: String,
47}
48
49/// How many databases `.connection` can hold open at once.
50///
51/// Five, which is the reference's own array size. A slot that has never been
52/// switched to is closed, and switching to one opens an in-memory database
53/// there - which is what makes `.connection 1` a working command on a shell
54/// that was started with one file.
55pub const CONNECTIONS: usize = 5;
56
57/// One shell session: the databases it can reach, and every setting a dot
58/// command can change.
59pub struct Shell {
60    /// The flag that stops the statement this shell is running.
61    ///
62    /// **The shell does not go through `command::run`, so it arms its own
63    /// budget (task-1932, H11).** Every statement a person types runs inside
64    /// this, and `interrupt::stop_on_ctrl_c` is what a front end registers it
65    /// with - so Ctrl+C ends the query rather than the program, and a second
66    /// press still ends the program because the operating system's default
67    /// handler comes back once ours has fired.
68    cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
69    /// The databases `.connection` switches between; slot 0 is the one the
70    /// shell was started on.
71    connections: Vec<Option<Opened>>,
72    /// Which slot statements run on.
73    active: usize,
74    /// How results are laid out.
75    pub layout: Layout,
76    /// What `.headers` last set, or `None` when it has not been used.
77    ///
78    /// SQLite's shell keeps the choice apart from the mode: until `.headers`
79    /// is used, `.mode` turns headers on for the tabular modes and off for
80    /// the others, and once it is used the choice survives every later
81    /// `.mode`. A single flag that `.mode box` set to on left headers on in
82    /// `list` and `insert` mode afterwards.
83    pub headers_chosen: Option<bool>,
84    /// Whether a result with no rows still prints its header line.
85    ///
86    /// Off for everything a person or a script types, because `sqlite3`
87    /// prints nothing for an empty result. `inillucent export` turns it on
88    /// for its one statement, so an exported file always names its columns.
89    pub header_when_empty: bool,
90    /// Where output goes, when it is not standard output.
91    output: Option<std::fs::File>,
92    /// The name of that file, for `.show` to report.
93    ///
94    /// **A second field rather than asking the `File`**, because a `File` does
95    /// not carry the path it was opened with on any platform this builds for.
96    /// Before it existed `.show` printed `output: stdout` while a `.output`
97    /// redirect was open, which is the one line of that report a person reads
98    /// when they cannot find where their rows went.
99    output_name: Option<String>,
100    /// Whether `.once` set that file for one statement only.
101    output_is_once: bool,
102    /// Whether a failing statement stops the script.
103    pub bail: bool,
104    /// Whether each statement is echoed before it runs.
105    pub echo: bool,
106    /// Whether to print how long each statement took.
107    pub timer: bool,
108    /// Whether the page cache's counters are printed after each statement.
109    pub stats: bool,
110    /// Whether to print the change count after each statement.
111    pub show_changes: bool,
112    /// Whether an `EXPLAIN QUERY PLAN` is printed before each statement.
113    pub explain_plan: bool,
114    /// Whether output lines end with a carriage return, which `.crlf` sets.
115    pub crlf: bool,
116    /// The prompt an interactive session prints for a new statement.
117    pub prompt_main: String,
118    /// The prompt it prints for a statement that is not finished.
119    pub prompt_continue: String,
120    /// When an `EXPLAIN` listing is laid out as a table.
121    pub explain_mode: crate::commands::ExplainMode,
122    /// The token `.nonce` set, which suspends safe mode for one command.
123    pub nonce: Option<String>,
124    /// The name of the `.testcase` that is capturing output, if one is.
125    pub testcase: Option<String>,
126    /// What has been printed since that `.testcase`.
127    pub captured: String,
128    /// How many `.check`s have run.
129    pub tests_run: usize,
130    /// How many of them failed.
131    pub tests_failed: usize,
132    /// Where a `.excel` or `.www` file is being written, if one is.
133    pub viewer: Option<std::path::PathBuf>,
134    /// Whether the authorizer's decisions are printed, which `.auth` sets.
135    pub auth: bool,
136    /// The decisions it has recorded since the last statement.
137    pub authorized: std::rc::Rc<std::cell::RefCell<Vec<String>>>,
138    /// Where `.trace` sends each statement, when it sends it anywhere.
139    pub trace: Option<String>,
140    /// What `.scanstats` was set to.
141    pub scanstats: String,
142    /// Whether `SQLITE_DBCONFIG_DEFENSIVE` is in force.
143    ///
144    /// **On, because the reference's shell turns it on.** It is the flag that
145    /// makes `PRAGMA journal_mode = OFF` and `PRAGMA writable_schema = ON`
146    /// refuse rather than take effect, and a shell that left it off answered
147    /// those two differently from the reference on a fresh database.
148    pub defensive: bool,
149    /// Whether the shell should stop.
150    pub done: bool,
151    /// Whether anything has failed, which decides the exit code.
152    pub failed: bool,
153    /// Whether a chunk of input is being run, so that the error of a statement that
154    /// fails while running is held until the chunk ends. See `run_chunk`.
155    pub holding: bool,
156    /// The lines of the last runtime error of the chunk being run.
157    pub held_error: Option<Vec<String>>,
158    /// Whether a statement of the chunk failed to compile, which ends the chunk.
159    pub chunk_stop: bool,
160    /// The text of the chunk from the start of the statement being run.
161    ///
162    /// The reference draws the excerpt under an error from the rest of the line, not from
163    /// the statement alone, so the statements that follow a failing one appear in it.
164    pub excerpt: Option<String>,
165    /// The engine's error for the first statement that failed since `failed`
166    /// was last cleared, when the failure came from the engine.
167    ///
168    /// **Kept so a command can report the right status (task-2120).** The
169    /// printed text says what went wrong; only the `DbError` says which class
170    /// of failure it was. `inillucent run` used to report every failure in a
171    /// script as `syntax` with exit code 1, so a statement the engine has not
172    /// built - `exec` reports it as `unsupported` with exit code 3 - told the
173    /// caller to look for a mistake in SQL that had none.
174    pub first_error: Option<inillucent_base::DbError>,
175    /// The line the statement being run started on.
176    pub line: usize,
177    /// Where `.log` was pointed, when it was pointed anywhere.
178    ///
179    /// Recorded and never written to: this engine emits no log messages, so the
180    /// destination is a place nothing arrives. `.show` reports it, which is the
181    /// only thing that reads it.
182    pub log_to: Option<String>,
183    /// How often `.progress` was asked to run a handler, in opcodes.
184    ///
185    /// Recorded and reported by `.show`, and never acted on: the reference's
186    /// handler prints nothing unless `--limit` is given, and this engine's VM
187    /// has no per-opcode callback to hang one on. Keeping the state means a
188    /// script written for the reference sets it and runs on rather than
189    /// stopping at "unknown command".
190    pub progress_interval: u64,
191    /// The `--limit` `.progress` was given.
192    pub progress_limit: u64,
193    /// Whether `.progress --once` was asked for.
194    pub progress_once: bool,
195    /// Whether `.progress --quiet` was asked for.
196    pub progress_quiet: bool,
197    /// Whether the next number belongs to a `--limit` that has just been read.
198    pub progress_pending_limit: bool,
199    /// Whether a statement that changes something is refused.
200    ///
201    /// `-readonly` on the command line, and `--readonly` on `inillucent` and
202    /// `inillucent-mcp`. **The binder decides what writes, not a scan of the
203    /// text**: `EXPLAIN QUERY PLAN` over the statement fails with "not a
204    /// read-only statement" for anything that does, which cannot be talked past
205    /// with whitespace, a comment or an unusual capitalisation. It is the same
206    /// mechanism `inillucent-driver` uses and it is deliberately the same one -
207    /// two classifiers would eventually disagree, and the one that let a write
208    /// through would be the one nobody was watching.
209    ///
210    /// The *file* is still open for writing. The capability table says
211    /// `readonly_open` is `partial` and says exactly this, which is why the row
212    /// is worth reading before an application decides what it means by "open
213    /// this read only".
214    pub readonly: bool,
215    /// Whether the commands that reach outside the database are refused.
216    ///
217    /// `-safe` on the command line. The set is the reference's: running a
218    /// program (`.shell`, `.system`), loading a shared library (`.load`),
219    /// changing the working directory (`.cd`), handing a file to whatever the
220    /// system opens it with (`.excel`, `.www`), and writing output through a
221    /// pipe (`.output |cmd`, `.once |cmd`). Every one of them is a way for a
222    /// script that was only supposed to query a database to run code.
223    ///
224    /// `.nonce` lifts it for one command, which is the reference's own escape
225    /// hatch and is why the token is a secret the script's author chose.
226    pub safe: bool,
227    /// Where output goes when a caller is collecting it rather than printing.
228    ///
229    /// **Not the same as `captured`, and deliberately outside it.** `captured`
230    /// belongs to `.testcase`/`.check`, which compare one command's output
231    /// against an expected digest; this belongs to a caller running the shell
232    /// as a subroutine - the `run` command and the MCP server behind it
233    /// - and has to still be collecting while a `.testcase` inside the script it
234    /// was given is doing its own thing. So `say` checks the testcase first,
235    /// and a script that uses both nests the way it reads.
236    ///
237    /// **A `.once` or `.output` redirect is checked before this** and takes the
238    /// rows, which is what makes `export --out` write its file - see the
239    /// comment in `say`. So a collected script that redirects hands its caller
240    /// whatever was not redirected, which for an export is nothing.
241    ///
242    /// `complain` writes here whatever a redirect is doing, because a caller
243    /// collecting output wants the error in the same stream a person would have
244    /// seen it in rather than appended to the rows in the file. It still sets
245    /// `failed`.
246    pub sink: Option<String>,
247    /// How many result rows have been rendered since output was last sent
248    /// somewhere with `redirect`.
249    ///
250    /// **So a command that redirects can say what it wrote.** `export --out`
251    /// sends its rows to a file, which leaves it nothing to report from the
252    /// text it collected; counting the lines back out of the file would have
253    /// to know which of the eight formats writes a header, a separator rule or
254    /// several lines to the row. The number the renderer was handed is the
255    /// answer, and it costs one addition.
256    pub rows_since_redirect: usize,
257    /// The values `.parameter set` bound, by the name they were given.
258    ///
259    /// **The shell's own table, not the engine's.** SQLite keeps them in a
260    /// `temp.sqlite_parameters` table and binds from it before each step; the
261    /// visible behaviour is the same and this needs no reserved table name.
262    /// Ordered by key, which is the order `.parameter list` prints and the
263    /// order the reference prints.
264    pub parameters: std::collections::BTreeMap<String, Value<'static>>,
265}
266
267/// Returns an error as a sentence, with its detail when it carries one.
268///
269/// @param error - the failure
270fn described(error: inillucent_base::DbError) -> String {
271    match error.detail() {
272        Some(detail) => format!("{}: {detail}", error.message()),
273        None => error.message().to_string(),
274    }
275}
276
277/// Why a statement did not produce rows.
278pub struct Failure {
279    /// What went wrong.
280    pub message: String,
281    /// Where in the statement, when the failure knows.
282    pub offset: Option<u32>,
283    /// Whether it failed to compile rather than while running.
284    pub compiling: bool,
285    /// The engine's own error, kept so a caller can classify it.
286    ///
287    /// **The message is not the classification.** The command layer
288    /// has to tell a caller whether a statement was refused because the engine
289    /// has not built the construct - the driver's `unsupported` - or because it
290    /// was mistyped, and `drivers/README.md` argues at length for why folding
291    /// those two together throws the design away. Only the `DbError` knows:
292    /// `unsupported()` is a field on it, and the primary code separates a
293    /// constraint from a busy file from corruption. Rendering it to a sentence
294    /// here and matching on the sentence there would be a second, worse
295    /// classifier beside the driver's.
296    pub error: Option<inillucent_base::DbError>,
297    /// The column names and rows a query produced before it failed, which the
298    /// reference shell prints ahead of the error.
299    pub partial: Option<(Vec<String>, Vec<Vec<Value<'static>>>)>,
300}
301
302impl Shell {
303    /// Opens one database, with the modules and the flags a shell gives it.
304    ///
305    /// @param path - the file, or an in-memory name
306    pub fn open_one(path: &str) -> Result<Opened, String> {
307        Shell::open_one_as(path, false)
308    }
309
310    /// Opens one database, read only when the surface asked for it.
311    ///
312    /// @param path - the file, or an in-memory name
313    /// @param read_only - whether this connection may write the file
314    pub fn open_one_as(path: &str, read_only: bool) -> Result<Opened, String> {
315        Shell::open_one_reporting(path, read_only).map_err(described)
316    }
317
318    /// [`Shell::open_one_as`], handing back the engine's own error.
319    ///
320    /// @param path - the file, or an in-memory name
321    /// @param read_only - whether this connection may write the file
322    pub fn open_one_reporting(
323        path: &str,
324        read_only: bool,
325    ) -> Result<Opened, inillucent_base::DbError> {
326        // **The detail, not only the code.** An open that fails with "bad
327        // parameter or other API misuse" and nothing else is an error nobody
328        // can act on; the detail says which part of the file could not be read.
329        // **With the process's key, when it has one.** See `crate::keys`: the
330        // key comes from `--key-file` or the environment, and every database
331        // this shell opens is opened with it.
332        let database = Database::open_keyed(
333            path,
334            inillucent_driver::PAGE_SIZE,
335            inillucent_driver::DEFAULT_FRAMES,
336            read_only,
337            crate::keys::for_opening(),
338        )?;
339        // **The shell adds `fsdir`, and the library does not.** A table-valued
340        // function over the file system belongs to a program that asked for
341        // one; the reference draws the same line, with `fsdir` in `shell.c`.
342        for module in [
343            std::sync::Arc::new(inillucent_driver::vtab::fsdir::FsDirModule)
344                as std::sync::Arc<dyn inillucent_driver::vtab::Module>,
345            std::sync::Arc::new(inillucent_driver::vtab::zipfile::ZipFileModule),
346        ] {
347            database.register_module(module)?;
348        }
349        let session = database.session().session();
350        // **The reference's shell turns this on and this one has to as well.**
351        // It is a connection flag rather than a shell one, so setting the field
352        // below is not enough: the engine has to be told, or
353        // `PRAGMA journal_mode = OFF` is honoured here and refused there.
354        let _ = database.session_as(session).set_defensive(true);
355        // **And turns this one off, for the same reason (task-1972).** The
356        // library's default is on, which is SQLite's, and the reference's shell
357        // turns it off at startup - so `.dbconfig` on the reference prints
358        // `trusted_schema off` on a connection whose library default was on.
359        // A shell is a program that opens files it did not write, which is the
360        // case the flag exists for.
361        let _ = database
362            .session_as(session)
363            .execute_batch("PRAGMA trusted_schema = OFF;");
364        Ok(Opened {
365            database,
366            session,
367            path: path.to_string(),
368        })
369    }
370
371    /// Opens a shell on a database file, or on an in-memory one.
372    pub fn open(path: &str) -> Result<Shell, String> {
373        Shell::open_as(path, false)
374    }
375
376    /// Opens a shell on a database file, read only when the surface asked.
377    ///
378    /// @param path - the file, or an in-memory name
379    /// @param read_only - whether this connection may write the file
380    pub fn open_as(path: &str, read_only: bool) -> Result<Shell, String> {
381        Shell::open_reporting(path, read_only).map_err(described)
382    }
383
384    /// Opens a shell, handing back the engine's own error.
385    ///
386    /// **So a caller can report the status the engine gave (task-1979, C6).**
387    /// `open_as` folds the failure into a sentence, and the command surface
388    /// then reported every open failure as `io` - including a file another
389    /// process holds, which is `busy` and is the one an agent or a script can
390    /// act on by retrying.
391    ///
392    /// @param path - the file, or an in-memory name
393    /// @param read_only - whether this connection may write the file
394    pub fn open_reporting(path: &str, read_only: bool) -> Result<Shell, inillucent_base::DbError> {
395        let mut connections: Vec<Option<Opened>> = (0..CONNECTIONS).map(|_| None).collect();
396        if let Some(first) = connections.first_mut() {
397            *first = Some(Shell::open_one_reporting(path, read_only)?);
398        }
399        Ok(Shell {
400            cancel: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
401            connections,
402            active: 0,
403            layout: Layout::default(),
404            headers_chosen: None,
405            header_when_empty: false,
406            output: None,
407            output_name: None,
408            output_is_once: false,
409            bail: false,
410            echo: false,
411            timer: false,
412            stats: false,
413            show_changes: false,
414            explain_plan: false,
415            crlf: false,
416            prompt_main: "sqlite> ".to_string(),
417            prompt_continue: "   ...> ".to_string(),
418            explain_mode: crate::commands::ExplainMode::Auto,
419            nonce: None,
420            testcase: None,
421            captured: String::new(),
422            tests_run: 0,
423            tests_failed: 0,
424            viewer: None,
425            auth: false,
426            authorized: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
427            trace: None,
428            scanstats: "off".to_string(),
429            defensive: true,
430            done: false,
431            failed: false,
432            holding: false,
433            held_error: None,
434            chunk_stop: false,
435            excerpt: None,
436            first_error: None,
437            log_to: None,
438            progress_interval: 0,
439            progress_limit: 0,
440            progress_once: false,
441            progress_quiet: false,
442            progress_pending_limit: false,
443            parameters: std::collections::BTreeMap::new(),
444            readonly: false,
445            safe: false,
446            sink: None,
447            rows_since_redirect: 0,
448            line: 1,
449        })
450    }
451
452    /// Returns the connection statements run on.
453    ///
454    /// Always the same session, so a temporary object made by one statement is
455    /// there for the next one.
456    pub fn connection(&self) -> Connection<'_> {
457        let held = self.open_slot();
458        held.database.session_as(held.session)
459    }
460
461    /// Returns what one run-time limit is set to on the open database.
462    ///
463    /// @param limit - which limit
464    pub fn limit(&self, limit: inillucent_base::limits::Limit) -> i64 {
465        self.open_slot().database.limit(limit)
466    }
467
468    /// Sets one run-time limit on the open database, returning its old value.
469    ///
470    /// @param limit - which limit
471    /// @param requested - the value asked for
472    pub fn set_limit(&mut self, limit: inillucent_base::limits::Limit, requested: i64) -> i64 {
473        self.open_slot().database.set_limit(limit, requested)
474    }
475
476    /// Returns whether a boolean pragma reads on.
477    ///
478    /// @param name - the pragma's name
479    pub fn boolean_pragma(&self, name: &str) -> bool {
480        self.column(&format!("PRAGMA {name};"))
481            .first()
482            .is_some_and(|value| value == "1")
483    }
484
485    /// Sets a boolean pragma, reporting whether the engine took it.
486    ///
487    /// @param name - the pragma's name
488    /// @param value - what to set it to
489    pub fn set_boolean_pragma(&mut self, name: &str, value: bool) -> bool {
490        let word = if value { "on" } else { "off" };
491        self.collect(&format!("PRAGMA {name} = {word};")).is_ok()
492            && self.boolean_pragma(name) == value
493    }
494
495    /// Installs or removes the authorizer that `.auth on` prints through.
496    ///
497    /// @param on - whether the decisions are watched
498    pub fn set_authorizer(&mut self, on: bool) {
499        let installed: Option<std::rc::Rc<dyn inillucent_driver::Authorizer>> = on.then(|| {
500            std::rc::Rc::new(crate::commands::Watching {
501                seen: std::rc::Rc::clone(&self.authorized),
502            }) as std::rc::Rc<dyn inillucent_driver::Authorizer>
503        });
504        let _ = self.connection().set_authorizer(installed);
505    }
506
507    /// Prints and clears whatever the authorizer recorded.
508    fn report_authorized(&mut self) {
509        let lines: Vec<String> = self.authorized.borrow_mut().drain(..).collect();
510        for line in lines {
511            self.say(&line);
512        }
513    }
514
515    /// Puts the connection into or out of defensive mode.
516    ///
517    /// @param on - whether the flag is in force
518    pub fn set_defensive(&mut self, on: bool) -> bool {
519        let _ = self.connection().set_defensive(on);
520        true
521    }
522
523    /// Returns what the page cache has been asked to do.
524    pub fn cache_stats(&self) -> inillucent_driver::CacheStats {
525        self.open_slot().database.cache_stats()
526    }
527
528    /// Returns how many bytes the page cache is holding.
529    pub fn pool_bytes(&self) -> usize {
530        self.open_slot().database.pool_bytes()
531    }
532
533    /// Copies the open database into a file and checks the copy.
534    ///
535    /// @param path - where the copy goes
536    pub fn backup_to(&self, path: &str) -> Result<(), String> {
537        self.open_slot()
538            .database
539            .backup_to(path)
540            .map_err(|error| error.message().to_string())
541    }
542
543    /// Writes a copy of the database, encrypted with `key` or in plaintext.
544    ///
545    /// @param path - where the copy goes
546    /// @param key - the key the copy is encrypted with, if any
547    pub fn export_to(
548        &self,
549        path: &str,
550        key: Option<inillucent_driver::EncryptionKey>,
551    ) -> Result<(), inillucent_base::DbError> {
552        self.open_slot().database.export_to(path, key)
553    }
554
555    /// Reports whether the database is encrypted.
556    pub fn is_encrypted(&self) -> bool {
557        self.open_slot().database.is_encrypted()
558    }
559
560    /// Changes the key the database is encrypted with.
561    ///
562    /// @param key - the new key
563    pub fn rekey(
564        &self,
565        key: inillucent_driver::EncryptionKey,
566    ) -> Result<(), inillucent_base::DbError> {
567        self.open_slot().database.rekey(key)
568    }
569
570    /// Returns where the database was opened from.
571    pub fn path(&self) -> &str {
572        &self.open_slot().path
573    }
574
575    /// Returns the database statements currently run on.
576    ///
577    /// The active slot is never closed: `.connection close` on it moves the
578    /// shell back to slot zero, and slot zero is opened before the shell is.
579    ///
580    /// **The `expect` is the invariant, and the invariant is enforced twice.**
581    /// `Shell::open` fills slot zero before the shell exists, and
582    /// `.connection close` on the active slot moves back to slot zero rather
583    /// than closing it. The crate denies `expect_used` because a shell that
584    /// panics on a caller's SQL is unusable; this is not that - reaching it
585    /// would mean the shell had been constructed without a database, which no
586    /// path does.
587    #[allow(clippy::expect_used)]
588    fn open_slot(&self) -> &Opened {
589        self.connections
590            .get(self.active)
591            .and_then(|held| held.as_ref())
592            .or_else(|| self.connections.first().and_then(|held| held.as_ref()))
593            .expect("the shell always holds one open database")
594    }
595
596    /// Returns what opening the active database did to it.
597    ///
598    /// See `inillucent_driver::Recovery`; the caller decides whether to report
599    /// it, which for the command surface is "only when it says something
600    /// happened".
601    pub fn recovery(&self) -> inillucent_driver::Recovery {
602        let report = self.open_slot().database.recovery_report();
603        inillucent_driver::Recovery {
604            recovered: report.recovered,
605            scanned: report.scanned,
606            applied: report.applied,
607            dropped: report.dropped,
608            committed: report.committed,
609            losers: report.losers,
610            last_sequence: report.last_sequence,
611            last_lsn: report.last_lsn,
612        }
613    }
614
615    /// Returns which segment of its log the active database is writing.
616    pub fn log_sequence(&self) -> u64 {
617        self.open_slot().database.log_sequence()
618    }
619
620    /// Returns which slot statements run on.
621    pub fn active(&self) -> usize {
622        self.active
623    }
624
625    /// Returns each slot and what it holds, for `.connection`.
626    pub fn slots(&self) -> Vec<Option<String>> {
627        self.connections
628            .iter()
629            .map(|held| held.as_ref().map(|open| open.path.clone()))
630            .collect()
631    }
632
633    /// Switches to one slot, opening an in-memory database if it is closed.
634    ///
635    /// Out of range is ignored, which is what the reference does with it.
636    ///
637    /// @param slot - which connection to run statements on
638    pub fn use_slot(&mut self, slot: usize) -> Result<(), String> {
639        if slot >= CONNECTIONS {
640            return Ok(());
641        }
642        if self.connections.get(slot).is_some_and(Option::is_none) {
643            let opened = Shell::open_one(":memory:")?;
644            if let Some(place) = self.connections.get_mut(slot) {
645                *place = Some(opened);
646            }
647        }
648        self.active = slot;
649        Ok(())
650    }
651
652    /// Closes one slot, moving back to slot zero if it was the active one.
653    ///
654    /// Slot zero is never closed: it is the database the shell was started on,
655    /// and a shell with nothing open has nothing to run a statement against.
656    ///
657    /// @param slot - which connection to close
658    pub fn close_slot(&mut self, slot: usize) {
659        if slot == 0 || slot >= CONNECTIONS {
660            return;
661        }
662        if let Some(place) = self.connections.get_mut(slot) {
663            *place = None;
664        }
665        if self.active == slot {
666            self.active = 0;
667        }
668    }
669
670    /// Closes the current database and opens another.
671    pub fn reopen(&mut self, path: &str) -> Result<(), String> {
672        let replacement = Shell::open_one(path)?;
673        let active = self.active;
674        if let Some(place) = self.connections.get_mut(active) {
675            *place = Some(replacement);
676        }
677        Ok(())
678    }
679
680    /// Returns the layout to render with, told where its lines are going.
681    ///
682    /// The only caller is `run`, and it is a method rather than two lines
683    /// there because the three destinations `say` chooses between are the
684    /// three this has to agree with. They disagreeing is how `csv` came to
685    /// write a carriage return too many into a file.
686    fn rendering_layout(&self) -> crate::render::Layout {
687        let mut layout = self.layout.clone();
688        layout.to_stdout = self.output.is_none() && self.sink.is_none() && self.testcase.is_none();
689        layout
690    }
691
692    /// Prints one line to wherever output is currently going.
693    pub fn say(&mut self, line: &str) {
694        // **A `.testcase` captures instead of printing.** `.check` compares the
695        // output of the commands between the two, and a passing case prints
696        // nothing at all - which is what makes a test script's output the list
697        // of the cases that failed.
698        if self.testcase.is_some() {
699            self.captured.push_str(line);
700            self.captured.push('\n');
701            return;
702        }
703        // **A redirect outranks a collecting caller, and used to lose to one
704        // (task-2044).** `.once` and `.output` open their file and every line
705        // then went into the sink instead, so the file existed and was zero
706        // bytes while the rows came back in the caller's report. It was not a
707        // corner: `export --out` built a `.once` and ran it through
708        // `collect_output`, so the shipped command reported `"ok": true` with
709        // `"wrote": "<path>"` over an empty file in all eight formats, and a
710        // `.once` inside a script handed to `run` did the same. `export` asks
711        // for its redirect directly now, but `run` still hands the shell a
712        // script somebody else wrote, so this order is what makes that work.
713        //
714        // This order is what the two mean. A redirect is the caller of the
715        // shell saying where output goes; a sink is a caller collecting what
716        // was not redirected. `complain` is deliberately the other way round -
717        // an error goes to the sink even while a redirect is open, because an
718        // error belongs in the report rather than in the middle of the rows.
719        let ending = if self.crlf { "\r\n" } else { "\n" };
720        if let Some(file) = self.output.as_mut() {
721            let _ = write!(file, "{line}{ending}");
722            return;
723        }
724        if let Some(sink) = self.sink.as_mut() {
725            sink.push_str(line);
726            sink.push('\n');
727            return;
728        }
729        let mut out = std::io::stdout();
730        let _ = write!(out, "{line}{ending}");
731    }
732
733    /// Prints an error, which always goes to standard error.
734    ///
735    /// Unless a caller is collecting output, in which case it goes there: a
736    /// command run through the MCP server has no standard error anybody will
737    /// ever read, and an error that vanished would be worse than one printed
738    /// among the rows.
739    pub fn complain(&mut self, message: &str) {
740        match self.sink.as_mut() {
741            Some(sink) => {
742                sink.push_str(message);
743                sink.push('\n');
744            }
745            None => eprintln!("{message}"),
746        }
747        self.failed = true;
748    }
749
750    /// Refuses a command that safe mode does not allow, and says which it was.
751    ///
752    /// Returns whether the caller may go on. A matching `.nonce` has already
753    /// cleared safe mode for this command by the time this is asked, because
754    /// that is what `.nonce` does.
755    ///
756    /// @param command - the dot command being attempted, leading dot included
757    pub fn unsafe_refused(&mut self, command: &str) -> bool {
758        if !self.safe {
759            return false;
760        }
761        self.complain(&format!("Error: {command} is prohibited in safe mode"));
762        true
763    }
764
765    /// Sends output to a file, or back to standard output when `path` is none.
766    ///
767    /// @param path - the file to write, or none to go back to standard output
768    /// @param once - whether the redirect ends after the next SQL statement
769    pub fn redirect(&mut self, path: Option<&str>, once: bool) -> Result<(), String> {
770        self.rows_since_redirect = 0;
771        let Some(path) = path else {
772            self.output = None;
773            self.output_name = None;
774            self.output_is_once = false;
775            return Ok(());
776        };
777        let file = std::fs::File::create(path).map_err(|error| error.to_string())?;
778        self.output = Some(file);
779        self.output_name = Some(path.to_string());
780        self.output_is_once = once;
781        Ok(())
782    }
783
784    /// Where output is going, as `.show` names it.
785    ///
786    /// `stdout` when nothing is redirecting, and the file name when `.output`
787    /// or `.once` is.
788    pub fn output_target(&self) -> &str {
789        self.output_name.as_deref().unwrap_or("stdout")
790    }
791
792    /// Returns output to the terminal after a `.once`.
793    fn finish_once(&mut self) {
794        if self.output_is_once {
795            self.output = None;
796            self.output_name = None;
797            self.output_is_once = false;
798        }
799        // `.excel` and `.www` hand the file to whatever the system opens that
800        // kind with, and only once it is closed and complete.
801        if let Some(path) = self.viewer.take() {
802            crate::commands::open_viewer(&path);
803        }
804    }
805
806    /// Returns a handle to the flag that stops the running statement.
807    ///
808    /// A front end registers it with `interrupt::stop_on_ctrl_c`; a program
809    /// embedding the shell can set it from any thread.
810    pub fn cancel_flag(&self) -> std::sync::Arc<std::sync::atomic::AtomicBool> {
811        std::sync::Arc::clone(&self.cancel)
812    }
813
814    /// Runs one complete statement and prints whatever it produced.
815    pub fn run(&mut self, sql: &str) {
816        if self.readonly && self.writes(sql) {
817            self.complain("Error: attempt to write a readonly database");
818            return;
819        }
820        if self.echo {
821            let text = sql.to_string();
822            self.say(&text);
823        }
824        let started = std::time::Instant::now();
825        if self.explain_plan {
826            self.print_plan(sql);
827        }
828        // Armed for this statement and dropped after it, so a Ctrl+C that
829        // arrives between two statements belongs to the one that finished and
830        // is cleared rather than applied to the one that has not started.
831        let armed = inillucent_driver::arm(
832            inillucent_driver::StatementLimits::unbounded(),
833            std::sync::Arc::clone(&self.cancel),
834        );
835        let outcome = self.collect(sql);
836        drop(armed);
837        // `.auth on` prints what the binder asked about, before the rows the
838        // statement produced - which is the order the reference prints them in.
839        if self.auth {
840            self.report_authorized();
841        }
842        match outcome {
843            Err(failure) => {
844                if let Some((columns, rows)) = &failure.partial {
845                    let layout = self.rendering_layout();
846                    for line in render(&layout, columns, rows) {
847                        self.say(&line);
848                    }
849                }
850                self.report(sql, &failure);
851            }
852            Ok((columns, rows)) => {
853                // Counted here rather than beside the one `render` call below,
854                // because the two branches that follow print rows and return
855                // without reaching it - and a count that is right for six of
856                // the eight formats and silently zero for a plan is the kind
857                // of number a caller stops checking.
858                self.rows_since_redirect = self.rows_since_redirect.saturating_add(rows.len());
859                // **`EXPLAIN QUERY PLAN` is drawn, not listed.** Its four
860                // columns are a tree, and the reference's shell renders them as
861                // one; printing `0|0|0|SCAN t` is the raw result of a statement
862                // nobody writes for the raw result.
863                if is_query_plan(sql) {
864                    for line in plan_tree(&rows) {
865                        self.say(&line);
866                    }
867                    self.finish_once();
868                    return;
869                }
870                // **And the bytecode form is a table with fixed columns.** The
871                // reference's shell switches to its own `MODE_Explain` for an
872                // `EXPLAIN` whatever `.mode` says, because eight columns of
873                // opcode printed as `0|Init|0|1|0||0|Start at 1` is unreadable.
874                // The widths are the reference's own.
875                let as_table = match self.explain_mode {
876                    crate::commands::ExplainMode::Auto => is_bytecode_explain(sql),
877                    crate::commands::ExplainMode::On => true,
878                    crate::commands::ExplainMode::Off => false,
879                };
880                if as_table && columns.len() == EXPLAIN_WIDTHS.len() {
881                    for line in explain_table(&columns, &rows) {
882                        self.say(&line);
883                    }
884                    self.finish_once();
885                    return;
886                }
887                let layout = self.rendering_layout();
888                // A statement that returned no rows prints nothing, headers
889                // included: SQLite's shell prints the header from its per-row
890                // callback. Printing it here put an empty line after every
891                // `CREATE` and `INSERT` once `.headers on` was set. `export`
892                // renders through the same function and keeps its header.
893                let lines = if rows.is_empty() && !self.header_when_empty {
894                    Vec::new()
895                } else {
896                    render(&layout, &columns, &rows)
897                };
898                for line in lines {
899                    self.say(&line);
900                }
901                if self.show_changes {
902                    let changes = self.connection().changes().unwrap_or_default();
903                    // The reference prints both counters, aligned with three
904                    // spaces between them.
905                    let total = self.connection().total_changes().unwrap_or_default();
906                    self.say(&format!("changes: {changes}   total_changes: {total}"));
907                }
908            }
909        }
910        if self.stats {
911            for line in crate::diagnose::statistics(self) {
912                self.say(&line);
913            }
914        }
915        if self.timer {
916            let elapsed = started.elapsed();
917            self.say(&format!("Run Time: real {:.3}", elapsed.as_secs_f64()));
918        }
919        self.finish_once();
920    }
921
922    /// Prints a failure the way the reference prints it.
923    ///
924    /// The caret block only appears when the failure knows where it happened,
925    /// which is the same rule the reference follows: `no such table` has no
926    /// position and `no such column` does.
927    fn report(&mut self, sql: &str, failure: &Failure) {
928        if self.first_error.is_none() {
929            self.first_error = failure.error.clone();
930        }
931        let line = self.line;
932        let heading = if failure.compiling {
933            format!("Parse error near line {line}: {}", failure.message)
934        } else {
935            format!("Error near line {line}: {}", failure.message)
936        };
937        let mut lines = vec![heading];
938        if let Some(offset) = failure.offset {
939            let shown = self.excerpt.as_deref().unwrap_or(sql);
940            lines.extend(error_context(shown.as_bytes(), offset as usize));
941        }
942        self.print_or_hold(lines, failure.compiling);
943    }
944
945    /// Prints the lines of an error, or holds them until the chunk of input ends.
946    ///
947    /// **A statement that fails while running does not stop its chunk, and only the last
948    /// of those errors is printed.** The reference runs every statement of a line, keeps
949    /// the message of each failure in one slot that the next failure overwrites, and prints
950    /// the slot when the line is done. A statement that fails to compile ends the line, and
951    /// its message replaces whatever was held. So
952    /// `INSERT ...(duplicate); INSERT ...(fine); SELECT ...;` on one line runs all three and
953    /// prints one error, and a typo in the second statement hides the first's error and
954    /// skips the third statement.
955    ///
956    /// @param lines - the heading and the statement excerpt lines
957    /// @param compiling - whether the statement failed to compile
958    fn print_or_hold(&mut self, lines: Vec<String>, compiling: bool) {
959        if self.holding && !compiling {
960            self.held_error = Some(lines);
961            self.failed = true;
962            return;
963        }
964        if self.holding {
965            self.held_error = None;
966            self.chunk_stop = true;
967        }
968        for line in lines {
969            self.complain(&line);
970        }
971    }
972
973    /// Runs a statement and collects its column names and rows.
974    pub fn collect(&self, sql: &str) -> Result<(Vec<String>, Vec<Vec<Value<'static>>>), Failure> {
975        self.collect_bound(sql, &[])
976    }
977
978    /// Runs a statement with values bound by position, and collects its rows.
979    ///
980    /// **By position, because a caller that is a program has no names.** The
981    /// shell's own `.parameter` table binds `:name` and `@name` markers, which
982    /// is what a person typing a script wants; a command arriving over MCP or
983    /// off a command line carries an ordered array and means `?1`, `?2`, ... .
984    /// Going through the named table for those was the first thing tried,
985    /// and it bound nothing at all: the engine reports a numbered marker
986    /// under a name that is not the text `?1`, so every lookup missed and every
987    /// value silently arrived as NULL. Binding by the index the parser assigned
988    /// cannot miss.
989    ///
990    /// Both mechanisms apply: positional values are bound first and the named
991    /// table after, so a script that sets `:limit` once and passes `?1` per
992    /// call gets both.
993    ///
994    /// @param sql - the statement
995    /// @param bound - the values for `?1`, `?2`, ... in order
996    pub fn collect_bound(
997        &self,
998        sql: &str,
999        bound: &[OwnedDatum],
1000    ) -> Result<(Vec<String>, Vec<Vec<Value<'static>>>), Failure> {
1001        let connection = self.connection();
1002        let mut statement = connection.prepare(sql).map_err(|error| Failure {
1003            message: reason(&error),
1004            offset: error.sql_offset(),
1005            compiling: true,
1006            error: Some(error),
1007            partial: None,
1008        })?;
1009        for (nth, value) in bound.iter().enumerate() {
1010            // The parser numbers markers from one, and a caller that passed
1011            // more values than the statement has markers is told so rather than
1012            // having the extras dropped: a query that silently ignored an
1013            // argument is a query answering a different question.
1014            statement
1015                .bind(nth as u32 + 1, value.clone())
1016                .map_err(|error| Failure {
1017                    message: reason(&error),
1018                    offset: None,
1019                    compiling: true,
1020                    error: Some(error),
1021                    partial: None,
1022                })?;
1023        }
1024        // **What `.parameter set` bound, applied by name.** A statement that
1025        // names none of them binds nothing; a name the statement does not use
1026        // is not an error, which is what makes a set of parameters reusable
1027        // across a script.
1028        if !self.parameters.is_empty() {
1029            let names = connection.parameter_names(sql).unwrap_or_default();
1030            // A parameter number has one name, the first the statement wrote for it. In
1031            // `SELECT :a, ?1` both are number 1 and the name is `:a`, so a value set for `?1`
1032            // is never looked up, as in the reference shell.
1033            let mut named: Vec<u32> = Vec::new();
1034            for (name, index) in names {
1035                if named.contains(&index) {
1036                    continue;
1037                }
1038                named.push(index);
1039                let key = String::from_utf8_lossy(&name).into_owned();
1040                let Some(value) = self.parameters.get(&key) else {
1041                    continue;
1042                };
1043                let _ = statement.bind(index, OwnedDatum::from(value));
1044            }
1045        }
1046        let mut rows = Vec::new();
1047        loop {
1048            match statement.step() {
1049                Err(error) => {
1050                    // The rows that came before the failure are printed ahead
1051                    // of the error, as the reference shell does.
1052                    let partial = (!rows.is_empty()).then(|| (statement.columns().to_vec(), rows));
1053                    return Err(Failure {
1054                        message: reason(&error),
1055                        offset: None,
1056                        compiling: false,
1057                        error: Some(error),
1058                        partial,
1059                    });
1060                }
1061                Ok(false) => break,
1062                Ok(true) => match owned_row_values(statement.row()) {
1063                    Ok(row) => rows.push(row),
1064                    Err(error) => {
1065                        return Err(Failure {
1066                            message: reason(&error),
1067                            offset: None,
1068                            compiling: false,
1069                            error: Some(error),
1070                            partial: None,
1071                        })
1072                    }
1073                },
1074            }
1075        }
1076        // Read *after* stepping. The engine's statement materialises on its
1077        // first step, so it does not know its column names until it has run -
1078        // where `sqlite3_column_name` answers straight after a prepare. Asking
1079        // first returned an empty list, and `.headers on` printed nothing.
1080        let columns: Vec<String> = statement.columns().to_vec();
1081        Ok((columns, rows))
1082    }
1083
1084    /// Prints the query plan for a statement, for `.eqp on`.
1085    fn print_plan(&mut self, sql: &str) {
1086        let plan = format!("EXPLAIN QUERY PLAN {sql}");
1087        let Ok((_, rows)) = self.collect(&plan) else {
1088            return;
1089        };
1090        for line in plan_tree(&rows) {
1091            self.say(&line);
1092        }
1093    }
1094
1095    /// Returns the text left over after the first statement, when it holds another one.
1096    ///
1097    /// **The parser's own count, not a scan for semicolons.** A trigger body contains a semicolon,
1098    /// and a string literal can contain anything, so counting them is how a correct script gets
1099    /// refused and an incorrect one gets accepted. `prepare_with_tail` reports how many bytes the
1100    /// first statement used, and `leading_trivia` reports how much of what is left is not a
1101    /// statement at all - which is what makes a trailing semicolon and a trailing comment not count
1102    /// as a second statement. Both are the engine's own, so there is no second scanner here to
1103    /// disagree with the parser.
1104    ///
1105    /// A script that will not compile answers `None`: it is a syntax error, and it should be
1106    /// reported as the syntax error it is rather than as a script with too many statements in it.
1107    ///
1108    /// @param sql - the text a caller passed as one statement
1109    pub fn trailing_statement(&self, sql: &str) -> Option<String> {
1110        let connection = self.connection();
1111        let consumed = connection.prepare_with_tail(sql).ok()?.consumed;
1112        let left = sql.get(consumed..)?;
1113        let rest = left.get(inillucent_driver::leading_trivia(left)..)?.trim();
1114        if rest.is_empty() {
1115            return None;
1116        }
1117        Some(rest.chars().take(60).collect())
1118    }
1119
1120    /// Runs a statement for its effect, reporting only a failure.
1121    ///
1122    /// @param sql - the statements, separated by semicolons
1123    pub fn execute(&mut self, sql: &str) -> Result<(), String> {
1124        self.connection()
1125            .execute_batch(sql)
1126            .map_err(|error| reason(&error))
1127    }
1128
1129    /// Returns whether a statement changes something, by its class.
1130    ///
1131    /// **From `inillucent_driver::readonly`, the same answer the command
1132    /// surface and the driver use (task-1979, section 5.2).** It used to ask
1133    /// the engine to plan the statement and read the text of the failure, and
1134    /// `explain` answers `Ok` for an `INSERT`, a write pragma, an `ATTACH` and
1135    /// a `VACUUM INTO` - so this reported that none of them writes.
1136    ///
1137    /// @param sql - the statement
1138    pub fn writes(&self, sql: &str) -> bool {
1139        !inillucent_driver::readonly::admits(sql)
1140    }
1141
1142    /// Returns one column of one row, as text.
1143    pub fn scalar(&self, sql: &str) -> Option<String> {
1144        let (_, rows) = self.collect(sql).ok()?;
1145        let value = rows.first().and_then(|row| row.first())?;
1146        Some(match value {
1147            Value::Null => String::new(),
1148            Value::Text(text) => String::from_utf8_lossy(text.raw()).into_owned(),
1149            other => crate::render::literal(other),
1150        })
1151    }
1152
1153    /// Returns the first column of every row, as text.
1154    pub fn column(&self, sql: &str) -> Vec<String> {
1155        let Ok((_, rows)) = self.collect(sql) else {
1156            return Vec::new();
1157        };
1158        rows.iter()
1159            .filter_map(|row| row.first())
1160            .map(|value| match value {
1161                Value::Null => String::new(),
1162                Value::Text(text) => String::from_utf8_lossy(text.raw()).into_owned(),
1163                other => crate::render::literal(other),
1164            })
1165            .collect()
1166    }
1167}
1168
1169/// Reads input line by line, running statements as they become complete.
1170///
1171/// A line beginning with a dot is a command, but only when nothing is
1172/// half-typed: `.` inside a `CREATE TRIGGER` body is part of the statement, and
1173/// treating it as a command there is the bug every naive shell has.
1174pub fn drive(shell: &mut Shell, input: impl Iterator<Item = String>) {
1175    let mut pending = String::new();
1176    let mut number = 0usize;
1177    let mut started = 1usize;
1178    for line in input {
1179        number += 1;
1180        // The reference swallows a line that holds only whitespace or comments when no statement
1181        // is half read, so such a line neither starts a statement nor moves the line number an
1182        // error reports. `--what` on line 1 and a bad statement on line 2 is an error on line 2.
1183        // With `.echo on` the line is still passed through, which keeps the echoed text unchanged.
1184        if !shell.echo && pending.trim().is_empty() && only_whitespace_and_comments(&line) {
1185            continue;
1186        }
1187        if pending.trim().is_empty() {
1188            started = number;
1189        }
1190        shell.line = started;
1191        if pending.trim().is_empty() && line.trim_start().starts_with('.') {
1192            if shell.echo {
1193                let text = line.trim().to_string();
1194                shell.say(&text);
1195            }
1196            crate::dot::run(shell, line.trim());
1197            if shell.done || (shell.failed && shell.bail) {
1198                return;
1199            }
1200            continue;
1201        }
1202        pending.push_str(&line);
1203        pending.push('\n');
1204        if ends_in_complete_statement(shell, &pending) {
1205            run_chunk(shell, &std::mem::take(&mut pending));
1206            if shell.done || (shell.failed && shell.bail) {
1207                return;
1208            }
1209        }
1210    }
1211    if !pending.trim().is_empty() {
1212        // Whatever is left was never terminated. Running it is what SQLite's
1213        // shell does at end of input, and it is what makes `echo "SELECT 1" |
1214        // inillucent-shell` work without a semicolon.
1215        run_chunk(shell, &pending);
1216    }
1217}
1218
1219/// Reports whether the text read so far ends at the end of a statement.
1220///
1221/// This is `sqlite3_complete` applied to everything the shell has accumulated, which is
1222/// what decides when the reference's shell runs its input: when a line makes the whole
1223/// text end in a `;` that is not inside a string, a comment or a trigger body.
1224///
1225/// @param shell - the shell
1226/// @param text - the lines read since the last chunk ran
1227fn ends_in_complete_statement(shell: &Shell, text: &str) -> bool {
1228    let mut rest = text;
1229    while let Some(consumed) = complete_statement(shell, rest) {
1230        rest = rest.get(consumed..).unwrap_or_default();
1231        if only_whitespace_and_comments(rest) {
1232            return true;
1233        }
1234    }
1235    false
1236}
1237
1238/// Reports whether text holds nothing but whitespace and comments.
1239///
1240/// @param text - the text after a statement's semicolon
1241fn only_whitespace_and_comments(text: &str) -> bool {
1242    let bytes = text.as_bytes();
1243    let mut at = 0usize;
1244    while at < bytes.len() {
1245        match bytes.get(at).copied() {
1246            Some(byte) if byte.is_ascii_whitespace() => at += 1,
1247            Some(b'-') if bytes.get(at + 1) == Some(&b'-') => at = skip_line_comment(bytes, at),
1248            Some(b'/') if bytes.get(at + 1) == Some(&b'*') => match skip_block_comment(bytes, at) {
1249                Some(end) => at = end,
1250                None => return false,
1251            },
1252            _ => return false,
1253        }
1254    }
1255    true
1256}
1257
1258/// Runs the statements of one chunk of input the way the reference runs a line.
1259///
1260/// **The unit is the chunk, not the statement.** The reference's shell hands everything
1261/// it has accumulated to one call that prepares and runs statement after statement.
1262/// A statement that fails to compile ends the call and its error is printed. A statement
1263/// that fails while running does not end it: the next statement runs, and only the last
1264/// runtime error is printed, after the line. See [`Shell::print_or_hold`].
1265///
1266/// @param shell - the shell
1267/// @param chunk - the text, which may hold several statements
1268fn run_chunk(shell: &mut Shell, chunk: &str) {
1269    shell.holding = true;
1270    shell.chunk_stop = false;
1271    shell.held_error = None;
1272    let mut rest = chunk.to_string();
1273    loop {
1274        let (statement, remainder) = match complete_statement(shell, &rest) {
1275            Some(consumed) => {
1276                let remainder = rest.split_off(consumed);
1277                (std::mem::take(&mut rest), remainder)
1278            }
1279            None => (std::mem::take(&mut rest), String::new()),
1280        };
1281        if !statement.trim().is_empty() {
1282            // The excerpt under an error runs on into the statements after this one.
1283            let tail = format!("{}{remainder}", statement.trim_start());
1284            shell.excerpt = Some(tail.trim_end().to_string());
1285            shell.run(statement.trim());
1286        }
1287        rest = remainder;
1288        if shell.done || shell.chunk_stop || (shell.failed && shell.bail) || rest.trim().is_empty()
1289        {
1290            break;
1291        }
1292    }
1293    shell.holding = false;
1294    shell.excerpt = None;
1295    if let Some(lines) = shell.held_error.take() {
1296        for line in lines {
1297            shell.complain(&line);
1298        }
1299    }
1300}
1301
1302/// Returns how many bytes of `text` form one complete statement, if any.
1303///
1304/// This is `sqlite3_complete`, and it is lexical on purpose. Asking the parser
1305/// cannot work: a `CREATE TRIGGER` does not parse until its `END`, and a parse
1306/// failure does not distinguish "still typing" from "misspelt". What a shell
1307/// needs to know is narrower and decidable - has a semicolon been reached that
1308/// is not inside a trigger body - so that is what is computed.
1309fn complete_statement(_shell: &Shell, text: &str) -> Option<usize> {
1310    let mut state = State::Start;
1311    let bytes = text.as_bytes();
1312    let mut at = 0usize;
1313    while at < bytes.len() {
1314        let Some(byte) = bytes.get(at).copied() else {
1315            break;
1316        };
1317        match byte {
1318            b'-' if bytes.get(at + 1) == Some(&b'-') => {
1319                at = skip_line_comment(bytes, at);
1320            }
1321            b'/' if bytes.get(at + 1) == Some(&b'*') => {
1322                // `?` rather than a `let ... else`: an unterminated block
1323                // comment is more input to come, which is what `None` means all
1324                // the way up this function.
1325                at = skip_block_comment(bytes, at)?;
1326            }
1327            b'\'' | b'"' | b'`' => {
1328                at = skip_quoted(bytes, at, byte)?;
1329            }
1330            b'[' => {
1331                at = skip_quoted(bytes, at, b']')?;
1332            }
1333            b';' => {
1334                at += 1;
1335                if state.ends_here() {
1336                    return Some(at);
1337                }
1338                state = state.after_semicolon();
1339            }
1340            _ if byte.is_ascii_alphabetic() || byte == b'_' => {
1341                let end = word_end(bytes, at);
1342                let word = bytes.get(at..end).unwrap_or(&[]).to_ascii_uppercase();
1343                state = state.after_word(&word);
1344                at = end;
1345            }
1346            _ if byte.is_ascii_whitespace() => at += 1,
1347            _ => {
1348                state = state.after_other();
1349                at += 1;
1350            }
1351        }
1352    }
1353    None
1354}
1355
1356/// Where the scan is, in terms of what a semicolon would mean.
1357#[derive(Clone, Copy, PartialEq, Eq)]
1358enum State {
1359    /// Nothing has been read yet, or the last statement finished.
1360    Start,
1361    /// A statement is under way and a semicolon ends it.
1362    Plain,
1363    /// `CREATE` has been read, and the next words decide.
1364    Create,
1365    /// `CREATE ... TRIGGER` has been read; the body has not started.
1366    Trigger,
1367    /// Inside a trigger body, where a semicolon ends a nested statement.
1368    Body,
1369    /// A semicolon inside a body has just been read, so an `END` now would
1370    /// close the trigger.
1371    ///
1372    /// **Only an `END` straight after a semicolon closes a trigger**, which is
1373    /// `sqlite3_complete`'s table. Any `END` in the body used to count, so the
1374    /// `END` of a `CASE` did: `BEGIN SELECT CASE WHEN NEW.n < 0 THEN
1375    /// RAISE(ABORT, 'negative') END; END;` was cut after the first `END;` and
1376    /// sent to the parser as a trigger with no end, which is the usual way to
1377    /// write a guard trigger.
1378    Semi,
1379    /// `END` has been read after a semicolon inside a body, so a semicolon
1380    /// ends the whole thing.
1381    End,
1382}
1383
1384impl State {
1385    /// Returns whether a semicolon here finishes the statement.
1386    fn ends_here(self) -> bool {
1387        !matches!(self, State::Trigger | State::Body | State::Semi)
1388    }
1389
1390    /// Returns the state after a semicolon that did not finish anything.
1391    fn after_semicolon(self) -> State {
1392        match self {
1393            State::Trigger | State::Body | State::Semi => State::Semi,
1394            _ => State::Start,
1395        }
1396    }
1397
1398    /// Returns the state after a word.
1399    fn after_word(self, word: &[u8]) -> State {
1400        match self {
1401            State::Start if word == b"CREATE" => State::Create,
1402            State::Start if word == b"EXPLAIN" => State::Start,
1403            State::Start => State::Plain,
1404            // `TEMP`, `TEMPORARY` and `IF NOT EXISTS` all sit between `CREATE`
1405            // and the thing being created, so they leave the state alone.
1406            State::Create
1407                if matches!(
1408                    word,
1409                    b"TEMP" | b"TEMPORARY" | b"IF" | b"NOT" | b"EXISTS" | b"OR" | b"REPLACE"
1410                ) =>
1411            {
1412                State::Create
1413            }
1414            State::Create if word == b"TRIGGER" => State::Trigger,
1415            State::Create => State::Plain,
1416            State::Trigger if word == b"BEGIN" => State::Body,
1417            State::Semi if word == b"END" => State::End,
1418            State::Semi | State::End => State::Body,
1419            other => other,
1420        }
1421    }
1422
1423    /// Returns the state after anything that is not a word or a semicolon.
1424    fn after_other(self) -> State {
1425        match self {
1426            State::Start => State::Plain,
1427            State::Semi | State::End => State::Body,
1428            other => other,
1429        }
1430    }
1431}
1432
1433/// Returns the offset just past a `--` comment.
1434fn skip_line_comment(bytes: &[u8], at: usize) -> usize {
1435    let mut scan = at + 2;
1436    while scan < bytes.len() {
1437        if bytes.get(scan) == Some(&b'\n') {
1438            return scan + 1;
1439        }
1440        scan += 1;
1441    }
1442    scan
1443}
1444
1445/// Returns the offset just past a block comment, or `None` when it is open.
1446fn skip_block_comment(bytes: &[u8], at: usize) -> Option<usize> {
1447    let mut scan = at + 2;
1448    while scan + 1 < bytes.len() {
1449        if bytes.get(scan) == Some(&b'*') && bytes.get(scan + 1) == Some(&b'/') {
1450            return Some(scan + 2);
1451        }
1452        scan += 1;
1453    }
1454    None
1455}
1456
1457/// Returns the offset just past a quoted run, or `None` when it is open.
1458///
1459/// A doubled quote inside a quoted run is one character and does not close it,
1460/// which is the case a naive scan gets wrong on `'it''s'`.
1461fn skip_quoted(bytes: &[u8], at: usize, close: u8) -> Option<usize> {
1462    let open = bytes.get(at).copied()?;
1463    let mut scan = at + 1;
1464    while scan < bytes.len() {
1465        let byte = bytes.get(scan).copied()?;
1466        if byte == close {
1467            if close == open && bytes.get(scan + 1) == Some(&close) {
1468                scan += 2;
1469                continue;
1470            }
1471            return Some(scan + 1);
1472        }
1473        scan += 1;
1474    }
1475    None
1476}
1477
1478/// Returns the offset just past a word.
1479fn word_end(bytes: &[u8], at: usize) -> usize {
1480    let mut scan = at;
1481    while scan < bytes.len() {
1482        match bytes.get(scan) {
1483            Some(byte) if byte.is_ascii_alphanumeric() || *byte == b'_' => scan += 1,
1484            _ => break,
1485        }
1486    }
1487    scan
1488}
1489
1490/// Returns the mode a `.mode` argument selects, or a message.
1491pub fn mode_named(name: &str) -> Result<Mode, String> {
1492    Mode::from_name(name).ok_or_else(|| format!("Error: mode should be one of: {}", MODE_NAMES))
1493}
1494
1495/// Every mode name, for the message above and for `.help`.
1496pub const MODE_NAMES: &str = "box column csv html insert json line list markdown quote table tabs";
1497
1498/// Reports whether a statement is an `EXPLAIN QUERY PLAN`.
1499///
1500/// The words rather than the bound statement, because the shell decides how to
1501/// *print* before it knows what the engine made of it - and the two spellings
1502/// SQLite accepts are `EXPLAIN QUERY PLAN` and nothing else.
1503///
1504/// @param sql - the statement as typed
1505fn is_query_plan(sql: &str) -> bool {
1506    let mut words = sql.split_whitespace();
1507    words
1508        .next()
1509        .is_some_and(|word| word.eq_ignore_ascii_case("explain"))
1510        && words
1511            .next()
1512            .is_some_and(|word| word.eq_ignore_ascii_case("query"))
1513        && words
1514            .next()
1515            .is_some_and(|word| word.eq_ignore_ascii_case("plan"))
1516}
1517
1518/// The column widths the reference prints an `EXPLAIN` listing in.
1519///
1520/// `addr`, `opcode`, `p1`, `p2`, `p3`, `p4`, `p5`, `comment` - the same numbers
1521/// its shell carries, so a listing lines up under the same headings.
1522const EXPLAIN_WIDTHS: [usize; 8] = [4, 13, 4, 4, 4, 13, 2, 13];
1523
1524/// Reports whether a statement is an `EXPLAIN` in its bytecode form.
1525///
1526/// The word `EXPLAIN` not followed by `QUERY`, which is the only other thing it
1527/// can be followed by.
1528///
1529/// @param sql - the statement as typed
1530fn is_bytecode_explain(sql: &str) -> bool {
1531    let mut words = sql.split_whitespace();
1532    words
1533        .next()
1534        .is_some_and(|word| word.eq_ignore_ascii_case("explain"))
1535        && !words
1536            .next()
1537            .is_some_and(|word| word.eq_ignore_ascii_case("query"))
1538}
1539
1540/// Renders an `EXPLAIN` listing as the reference's fixed-width table.
1541///
1542/// A header, a rule of dashes, then one line per instruction, each column
1543/// left-aligned in its own width and separated by two spaces. A value wider
1544/// than its column is not truncated - the reference does not truncate either,
1545/// and a clipped opcode name would be worse than a ragged line.
1546///
1547/// @param columns - the column names, which are the reference's headings
1548/// @param rows - the instructions
1549fn explain_table(columns: &[String], rows: &[Vec<Value<'static>>]) -> Vec<String> {
1550    /// What separates two columns.
1551    const GAP: &str = "  ";
1552
1553    let mut lines = Vec::with_capacity(rows.len().saturating_add(2));
1554    lines.push(
1555        columns
1556            .iter()
1557            .enumerate()
1558            .map(|(at, name)| pad(name, EXPLAIN_WIDTHS.get(at).copied().unwrap_or(0)))
1559            .collect::<Vec<String>>()
1560            .join(GAP),
1561    );
1562    lines.push(
1563        EXPLAIN_WIDTHS
1564            .iter()
1565            .map(|width| "-".repeat(*width))
1566            .collect::<Vec<String>>()
1567            .join(GAP),
1568    );
1569    let last = EXPLAIN_WIDTHS.len().saturating_sub(1);
1570    for row in rows {
1571        let cells: Vec<String> = (0..EXPLAIN_WIDTHS.len())
1572            .map(|at| {
1573                let text = match row.get(at) {
1574                    Some(Value::Text(text)) => String::from_utf8_lossy(text.raw()).into_owned(),
1575                    Some(Value::Null) | None => String::new(),
1576                    Some(other) => crate::render::literal(other),
1577                };
1578                // **The last column of a row is written as it is.** The heading
1579                // is padded and the instruction's comment is not, which is what
1580                // leaves a `Halt` line ending in the separator rather than in
1581                // thirteen spaces. It is a small thing and it is two bytes of
1582                // difference per line against the reference.
1583                if at == last {
1584                    text
1585                } else {
1586                    pad(&text, EXPLAIN_WIDTHS.get(at).copied().unwrap_or(0))
1587                }
1588            })
1589            .collect();
1590        lines.push(cells.join(GAP));
1591    }
1592    lines
1593}
1594
1595/// Left-aligns one cell in its column.
1596///
1597/// @param text - the cell
1598/// @param width - the column's width
1599fn pad(text: &str, width: usize) -> String {
1600    let mut out = text.to_string();
1601    while out.chars().count() < width {
1602        out.push(' ');
1603    }
1604    out
1605}
1606
1607/// Renders `EXPLAIN QUERY PLAN`'s four columns as the tree the reference draws.
1608///
1609/// **The rows are a tree and were being printed as rows.** Each carries an id
1610/// and its parent's id, and the reference draws them under a `QUERY PLAN`
1611/// heading, with `|--` for a node that has a sibling after it and a backtick
1612/// arm for the last, indented three characters per level - which is how a
1613/// subquery under a step is told from a step beside it. Printing the raw four
1614/// columns left the shape for the reader to work out.
1615///
1616/// @param rows - the plan's rows: id, parent, notused, detail
1617pub fn plan_tree(rows: &[Vec<Value<'static>>]) -> Vec<String> {
1618    if rows.is_empty() {
1619        return Vec::new();
1620    }
1621    let mut lines = vec!["QUERY PLAN".to_string()];
1622    plan_children(rows, 0, "", 0, &mut lines);
1623    lines
1624}
1625
1626/// The arm the reference draws under the last child of a node.
1627const LAST_ARM: &str = "`--";
1628
1629/// How deep a plan tree may be drawn before the walk gives up.
1630///
1631/// A plan that named itself as its own parent would otherwise not terminate,
1632/// and a malformed plan is not a reason for a shell to hang. The cap rather
1633/// than an id check, because this engine numbers its top-level rows from zero
1634/// and the root is asked for by parent zero - so a row whose id and parent are
1635/// both zero is the ordinary first line of every plan.
1636const PLAN_DEPTH: usize = 64;
1637
1638/// Emits one parent's children, and theirs.
1639///
1640/// @param rows - every row of the plan
1641/// @param parent - the id whose children to emit
1642/// @param prefix - the indent the ancestors give
1643/// @param depth - how deep this call is
1644/// @param lines - where the rendered lines go
1645fn plan_children(
1646    rows: &[Vec<Value<'static>>],
1647    parent: i64,
1648    prefix: &str,
1649    depth: usize,
1650    lines: &mut Vec<String>,
1651) {
1652    if depth >= PLAN_DEPTH {
1653        return;
1654    }
1655    let field = |row: &Vec<Value<'static>>, at: usize| -> i64 {
1656        row.get(at).and_then(Value::as_integer).unwrap_or(0)
1657    };
1658    let children: Vec<&Vec<Value<'static>>> =
1659        rows.iter().filter(|row| field(row, 1) == parent).collect();
1660    for (at, row) in children.iter().enumerate() {
1661        let last = at.saturating_add(1) == children.len();
1662        let detail = row
1663            .last()
1664            .and_then(Value::as_text)
1665            .map(|text| String::from_utf8_lossy(text.raw()).into_owned())
1666            .unwrap_or_default();
1667        let arm = if last { LAST_ARM } else { "|--" };
1668        lines.push(format!("{prefix}{arm}{detail}"));
1669        // A node that still has siblings below it keeps a vertical bar in its
1670        // children's indent; the last one leaves a space.
1671        let carried = format!("{prefix}{}", if last { "   " } else { "|  " });
1672        // A row that names its own parent's id is its own child, which is what
1673        // a top-level row looks like on an engine that numbers from zero: it
1674        // has id 0 and parent 0. It is selected as a child of the root and must
1675        // not then be expanded as its own parent.
1676        if field(row, 0) != parent {
1677            plan_children(
1678                rows,
1679                field(row, 0),
1680                &carried,
1681                depth.saturating_add(1),
1682                lines,
1683            );
1684        }
1685    }
1686}
1687
1688/// Returns the two lines that point at where a statement went wrong.
1689///
1690/// A port of the reference shell's `shell_error_context`, down to the two
1691/// arrangements of the marker and the number that chooses between them, because
1692/// this is one of the places a transcript is compared rather than read. The
1693/// reference slides a window along the statement so the offending token is never
1694/// off the left of the line, truncates at 78 bytes, flattens every space
1695/// character to a plain space so a tab cannot shift the marker, and then draws
1696/// the caret to the left of the token while it still fits and to the right of a
1697/// trailing rule once it does not.
1698///
1699/// Returns nothing when the position is not inside the statement, which is the
1700/// reference's answer for `no such table` and for everything that fails while
1701/// stepping rather than while parsing.
1702///
1703/// @param sql - the whole statement, as the shell was given it
1704/// @param offset - the byte the engine says the error is at
1705fn error_context(sql: &[u8], offset: usize) -> Vec<String> {
1706    if offset >= sql.len() {
1707        return Vec::new();
1708    }
1709    // Slide the window right until the marker is within 50 bytes of the start,
1710    // never stopping inside a UTF-8 sequence.
1711    let mut start = 0usize;
1712    let mut column = offset;
1713    while column > 50 {
1714        start += 1;
1715        column -= 1;
1716        while sql.get(start).is_some_and(|byte| byte & 0xc0 == 0x80) {
1717            start += 1;
1718            column -= 1;
1719        }
1720    }
1721    let window = sql.get(start..).unwrap_or_default();
1722    let mut length = window.len().min(78);
1723    while length > 0 && window.get(length).is_some_and(|byte| byte & 0xc0 == 0x80) {
1724        length -= 1;
1725    }
1726    let shown = String::from_utf8_lossy(window.get(..length).unwrap_or_default())
1727        .chars()
1728        .map(|character| {
1729            if character.is_ascii_whitespace() {
1730                ' '
1731            } else {
1732                character
1733            }
1734        })
1735        .collect::<String>();
1736    let marker = if column < 25 {
1737        format!("  {}^--- error here", " ".repeat(column))
1738    } else {
1739        format!("  {}error here ---^", " ".repeat(column - 14))
1740    };
1741    vec![format!("  {shown}"), marker]
1742}
1743
1744/// Returns what a failure should say to a person.
1745///
1746/// **The detail, when there is one, and the code's text otherwise.** A
1747/// `DbError`'s `message` is the text of its primary code - "bad parameter or
1748/// other API misuse" for everything the engine refuses - and the sentence a
1749/// person can act on is in `detail`: "no such table: nope". Printing the code's
1750/// text made every refusal look like the same failure, which is the opposite of
1751/// what a shell is for.
1752///
1753/// @param error - what went wrong
1754fn reason(error: &inillucent_base::DbError) -> String {
1755    error
1756        .detail()
1757        .unwrap_or_else(|| error.message())
1758        .to_string()
1759}
1760
1761#[cfg(test)]
1762mod trailing_statement_tests {
1763    use super::Shell;
1764
1765    /// Opens a scratch shell over a database that reaches no file.
1766    fn shell() -> Shell {
1767        Shell::open(":memory:").expect("a memory database opens")
1768    }
1769
1770    /// One statement is one statement, however it is punctuated.
1771    ///
1772    /// `exec` and `query` are documented as taking one, and used to run the first of several and
1773    /// report success - which is how `inillucent exec "<twenty CREATE TABLEs>"` produced a database
1774    /// with one table in it and printed `ok. 0 rows changed.` These are the cases the
1775    /// refusal must not fire on, and the one it must.
1776    #[test]
1777    fn a_second_statement_is_recognised_and_punctuation_is_not() {
1778        let held = shell();
1779        for one in [
1780            "CREATE TABLE a (id INTEGER PRIMARY KEY)",
1781            "CREATE TABLE a (id INTEGER PRIMARY KEY);",
1782            "CREATE TABLE a (id INTEGER PRIMARY KEY);   ",
1783            "CREATE TABLE a (id INTEGER PRIMARY KEY); -- and that is all",
1784            "CREATE TABLE a (id INTEGER PRIMARY KEY); /* and that is all */",
1785            "CREATE TABLE a (id INTEGER PRIMARY KEY);;;",
1786            // A trigger body holds semicolons, which is why counting them is the wrong test.
1787            "CREATE TRIGGER t AFTER INSERT ON a FOR EACH ROW BEGIN UPDATE a SET id = id; END",
1788            // And the `END` of a `CASE` inside one is not the trigger's.
1789            "CREATE TRIGGER t AFTER INSERT ON a BEGIN SELECT CASE WHEN NEW.id < 0 THEN RAISE(ABORT, 'no') END; END",
1790        ] {
1791            assert_eq!(
1792                held.trailing_statement(one),
1793                None,
1794                "{one:?} is one statement"
1795            );
1796        }
1797
1798        let two = held
1799            .trailing_statement(
1800                "CREATE TABLE a (id INTEGER PRIMARY KEY); CREATE TABLE b (id INTEGER PRIMARY KEY)",
1801            )
1802            .expect("two statements are two statements");
1803        assert!(
1804            two.starts_with("CREATE TABLE b"),
1805            "the refusal names what comes next, and said {two:?}"
1806        );
1807
1808        // A comment between them does not hide the second one.
1809        let commented = held
1810            .trailing_statement(
1811                "CREATE TABLE a (id INTEGER PRIMARY KEY); -- next
1812CREATE TABLE b (id INTEGER PRIMARY KEY)",
1813            )
1814            .expect("a comment does not hide a statement");
1815        assert!(
1816            commented.starts_with("CREATE TABLE b"),
1817            "said {commented:?}"
1818        );
1819    }
1820
1821    /// The shell cuts its input where `sqlite3_complete` would, and a `CASE` inside a trigger
1822    /// body does not end the trigger.
1823    ///
1824    /// Each case is the text the shell has read so far and how much of it is one complete
1825    /// statement. The last three are the guard triggers the coffee shop example writes, which
1826    /// were cut after the `CASE`'s `END;` and sent to the parser as a trigger with no end.
1827    #[test]
1828    fn a_statement_ends_where_sqlite3_complete_says() {
1829        let held = shell();
1830        let guard = "CREATE TRIGGER g BEFORE UPDATE ON o BEGIN SELECT CASE WHEN NEW.n < 0 THEN RAISE(ABORT, 'negative ' || NEW.n) END; END;";
1831        for (text, wanted) in [
1832            ("SELECT 1; SELECT 2;", Some(9)),
1833            ("CREATE TRIGGER t AFTER INSERT ON a BEGIN UPDATE a SET id = id; END; SELECT 1;", Some(67)),
1834            ("CREATE TRIGGER t AFTER INSERT ON a BEGIN UPDATE a SET id = id;", None),
1835            (guard, Some(guard.len())),
1836            ("CREATE TRIGGER g BEFORE UPDATE ON o BEGIN SELECT CASE WHEN 1 THEN 2 END; SELECT 3; END;", Some(87)),
1837            ("CREATE TRIGGER g BEFORE UPDATE ON o BEGIN SELECT CASE WHEN 1 THEN 2 END;", None),
1838        ] {
1839            assert_eq!(
1840                super::complete_statement(&held, text),
1841                wanted,
1842                "how much of {text:?} is one statement"
1843            );
1844        }
1845    }
1846
1847    /// Text that will not compile is a syntax error, not a script with too many statements in it.
1848    #[test]
1849    fn text_that_does_not_compile_is_left_to_the_parser() {
1850        let held = shell();
1851        assert_eq!(held.trailing_statement("SELEKT 1"), None);
1852        assert_eq!(held.trailing_statement(""), None);
1853        assert_eq!(held.trailing_statement("-- only a comment"), None);
1854    }
1855}