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