rudb_cli/lib.rs
1//! The command line shell.
2//!
3//! Rank 15 in the layer rule. See `xtask/layers.toml` and `spec/18-package-layout.md`.
4//!
5//! The binary is a few lines over this, so that the shell can be driven from a test without
6//! spawning a process and so that `rudb-compat` can drive it as a target the same way it drives the
7//! library. Everything the shell does goes through [`rudb::Database`], which means the shell has no
8//! way to reach anything the embedding API cannot, which is the point: if the prompt can do it, a
9//! program can do it. The manifest says the same thing in the form the compiler checks, which is
10//! that `rudb` is the only dependency.
11//!
12//! Statements run on a [`rudb::Connection`] rather than on the database directly, which is the
13//! thing an interrupt has to reach. What is not here is the signal handler that would call
14//! [`rudb::Connection::interrupt`], because installing one needs `libc` and the dependency budget
15//! in `spec/18-package-layout.md` is a decision to make on purpose rather than in passing. That is
16//! the same decision line editing is waiting behind. The library half is done and tested, so the
17//! shell side is a handler and a clone of the connection the day the dependency is settled.
18//!
19//! The command line and the dot commands are DuckDB's, down to the single dash long options it
20//! inherits from SQLite. The output modes are DuckDB's too, byte for byte, because the reason
21//! anybody pipes a shell into another program is that they already know what comes out.
22
23#![forbid(unsafe_code)]
24
25pub mod args;
26pub mod format;
27pub mod help;
28pub mod shell;
29
30use std::io::{IsTerminal, Read, Write};
31use std::process::ExitCode;
32
33use rudb::{Config, Database};
34
35pub use args::{Action, Command, Options, parse};
36pub use format::{Format, Settings};
37pub use shell::{Shell, Stop};
38
39/// The version, which `-version` prints and which the greeting carries.
40pub const VERSION: &str = env!("CARGO_PKG_VERSION");
41
42/// The whole program, minus the process it runs in.
43///
44/// `out` and `err` are handed in rather than taken from the process so that a test can read what
45/// came out of each without a pipe and without a temporary file.
46pub fn run(arguments: &[String], out: Box<dyn Write>, err: Box<dyn Write>) -> ExitCode {
47 let mut err = err;
48 match parse(arguments) {
49 Action::Version => {
50 let mut out = out;
51 let _ = writeln!(out, "rudb {VERSION}");
52 ExitCode::SUCCESS
53 }
54 Action::Help => {
55 let mut out = out;
56 let _ = write!(out, "{}", help::USAGE);
57 ExitCode::SUCCESS
58 }
59 Action::Config => {
60 let mut out = out;
61 print_config(&mut out);
62 ExitCode::SUCCESS
63 }
64 Action::Wrong(why) => {
65 let _ = writeln!(err, "rudb: {why}");
66 let _ = writeln!(err, "rudb: try `rudb -help`");
67 ExitCode::FAILURE
68 }
69 Action::Run(options) => {
70 // The library decides what a database name means, here and behind `.open`, so there is
71 // one rule about it rather than a copy of the rule in the shell.
72 let database = match Database::open(&options.database) {
73 Ok(database) => database,
74 Err(problem) => {
75 let _ = writeln!(err, "rudb: {}", problem.message());
76 return ExitCode::FAILURE;
77 }
78 };
79 let mut shell = Shell::new(&options, database, out, err);
80 let mut stop = shell.run_commands(&options.commands);
81 if stop == Stop::Done && !options.stop_after_commands {
82 stop = read_input(&mut shell, &options);
83 }
84 let _ = stop;
85 if shell.failed() { ExitCode::FAILURE } else { ExitCode::SUCCESS }
86 }
87 }
88}
89
90/// Reads whatever is on standard input, with a prompt if that is a terminal.
91///
92/// There is no line editing, so no history, no arrow keys and no completion. That wants a
93/// dependency and the dependency budget in `spec/18-package-layout.md` is a decision to make on
94/// purpose rather than in passing, so it is a separate change. Everything else about the prompt
95/// works, including multi line statements.
96fn read_input(shell: &mut Shell, options: &Options) -> Stop {
97 let stdin = std::io::stdin();
98 let interactive = options.interactive.unwrap_or_else(|| stdin.is_terminal());
99 if !interactive {
100 let mut text = String::new();
101 if stdin.lock().read_to_string(&mut text).is_err() {
102 return Stop::Done;
103 }
104 return shell.run_input(&text);
105 }
106 shell.greet();
107 shell.prompt(&stdin)
108}
109
110/// The settled decisions from `spec/00-README.md` that a reader would otherwise have to take on
111/// trust. Printing them is cheap and it makes a bug report say which build it came from.
112///
113/// The first three lines are the ones a run can change, and they come from [`rudb::Config`] rather
114/// than from a literal here, so that what this prints is what the engine was actually opened with.
115/// A benchmark result that does not say how many threads it used is not a result, and one that says
116/// eight while the engine used one is worse than one that says nothing.
117fn print_config(out: &mut dyn Write) {
118 let _ = writeln!(out, "version: {VERSION}");
119 for (name, value) in Config::default().settings() {
120 let _ = writeln!(out, "{name}: {value}");
121 }
122 let _ = writeln!(out, "vector-size: 1024");
123 let _ = writeln!(out, "row-group-size: 122880");
124 let _ = writeln!(out, "storage-format: native (rudb v1), DuckDB import and export");
125 let _ = writeln!(out, "execution-tiers: interpreted");
126 let _ = writeln!(out, "duckdb-compat-level: 0 (nothing is implemented yet)");
127 let _ = writeln!(out, "target: {}", std::env::consts::ARCH);
128 let _ = writeln!(out, "os: {}", std::env::consts::OS);
129}