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