1use std::fmt::Write as _;
4use std::fs::{File, OpenOptions};
5use std::io::{self, BufWriter, Write};
6use std::path::{Path, PathBuf};
7use std::time::Instant;
8
9use rudb::{Connection, Database, Error, QueryResult, Span};
10
11use crate::args::{Command, Options};
12use crate::format::{Format, Settings, escaped, render};
13
14enum Sink {
20 Given(Box<dyn Write>),
22 File(BufWriter<File>, PathBuf),
24}
25
26impl Write for Sink {
27 fn write(&mut self, buffer: &[u8]) -> io::Result<usize> {
28 match self {
29 Self::Given(out) => out.write(buffer),
30 Self::File(out, _) => out.write(buffer),
31 }
32 }
33
34 fn flush(&mut self) -> io::Result<()> {
35 match self {
36 Self::Given(out) => out.flush(),
37 Self::File(out, _) => out.flush(),
38 }
39 }
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum Stop {
45 Done,
47 Failed,
49}
50
51pub struct Shell {
53 database: Database,
54 connection: Connection,
60 settings: Settings,
61 out: Sink,
62 err: Box<dyn Write>,
63 filename: String,
64 given: Option<Box<dyn Write>>,
65 timer: bool,
66 echo: bool,
67 bail: bool,
68 failed: bool,
69 metrics: Option<PathBuf>,
74 started: bool,
75}
76
77impl std::fmt::Debug for Shell {
78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 f.debug_struct("Shell")
80 .field("settings", &self.settings)
81 .field("failed", &self.failed)
82 .finish_non_exhaustive()
83 }
84}
85
86impl Shell {
87 pub fn new(
89 options: &Options,
90 database: Database,
91 out: Box<dyn Write>,
92 err: Box<dyn Write>,
93 ) -> Self {
94 Self {
95 connection: database.connect(),
96 database,
97 settings: options.settings.clone(),
98 out: Sink::Given(out),
99 err,
100 filename: options.database.clone(),
101 given: None,
102 timer: false,
103 echo: options.echo,
104 bail: options.bail,
105 failed: false,
106 metrics: options.metrics.clone(),
107 started: false,
108 }
109 }
110
111 pub fn failed(&self) -> bool {
113 self.failed
114 }
115
116 pub fn run_commands(&mut self, commands: &[Command]) -> Stop {
118 for command in commands {
119 let stop = match command {
120 Command::Sql(sql) => self.run_input(sql),
121 Command::File(path) => self.run_file(path),
122 };
123 if stop == Stop::Failed {
124 return Stop::Failed;
125 }
126 }
127 Stop::Done
128 }
129
130 pub fn run_file(&mut self, path: &Path) -> Stop {
132 match std::fs::read_to_string(path) {
133 Ok(text) => self.run_input(&text),
134 Err(problem) => {
135 let why = format!("Cannot open file \"{}\": {problem}", path.display());
136 self.report(&Error::io(why), "");
137 self.after_error()
138 }
139 }
140 }
141
142 pub fn greet(&mut self) {
144 let _ = writeln!(self.out, "rudb {}", crate::VERSION);
145 let _ = writeln!(self.out, "Enter \".help\" for usage hints.");
146 let _ = self.out.flush();
147 }
148
149 pub fn prompt(&mut self, stdin: &io::Stdin) -> Stop {
154 let mut pending = String::new();
155 loop {
156 let marker = if pending.is_empty() { "D " } else { "ยท " };
157 let _ = write!(self.out, "{marker}");
158 let _ = self.out.flush();
159 let mut line = String::new();
160 match stdin.read_line(&mut line) {
161 Ok(0) => {
162 let _ = writeln!(self.out);
163 return Stop::Done;
164 }
165 Ok(_) => {}
166 Err(_) => return Stop::Done,
167 }
168 let line = line.trim_end_matches(['\n', '\r']);
169 if pending.is_empty() && line.trim_start().starts_with('.') {
170 if self.run_dot(line.trim()) == Stop::Failed {
171 return Stop::Done;
172 }
173 continue;
174 }
175 if !pending.is_empty() {
176 pending.push('\n');
177 }
178 pending.push_str(line);
179 if rudb::is_complete(&pending) {
180 let statement = std::mem::take(&mut pending);
181 if self.run_sql(&statement) == Stop::Failed {
182 return Stop::Done;
183 }
184 }
185 }
186 }
187
188 pub fn run_input(&mut self, text: &str) -> Stop {
194 let mut pending = String::new();
195 for line in text.lines() {
196 if pending.trim().is_empty() && line.trim_start().starts_with('.') {
197 pending.clear();
198 if self.run_dot(line.trim()) == Stop::Failed {
199 return Stop::Failed;
200 }
201 continue;
202 }
203 if !pending.is_empty() {
204 pending.push('\n');
205 }
206 pending.push_str(line);
207 if rudb::is_complete(&pending) {
208 let statement = std::mem::take(&mut pending);
209 if self.run_sql(&statement) == Stop::Failed {
210 return Stop::Failed;
211 }
212 }
213 }
214 if pending.trim().is_empty() {
215 return Stop::Done;
216 }
217 self.run_sql(&pending)
218 }
219
220 fn run_sql(&mut self, text: &str) -> Stop {
222 let found = match rudb::statements(text) {
223 Ok(found) => found,
224 Err(problem) => {
225 self.report(&problem, text);
226 return self.after_error();
227 }
228 };
229 for statement in found {
230 if self.echo {
231 let _ = writeln!(self.out, "{}", statement.sql());
232 }
233 let started = Instant::now();
234 match self.connection.execute(statement.sql()) {
235 Ok(result) => {
236 self.record(&result);
237 self.print(&result);
238 if self.timer {
239 let _ = writeln!(
240 self.err,
241 "Run Time (s): real {:.3}",
242 started.elapsed().as_secs_f64()
243 );
244 }
245 }
246 Err(problem) => {
247 self.report(&problem, statement.sql());
248 return self.after_error();
249 }
250 }
251 }
252 Stop::Done
253 }
254
255 fn record(&mut self, result: &QueryResult) {
265 let (Some(path), Some(document)) = (self.metrics.clone(), result.metrics()) else {
266 return;
267 };
268 let opened = if self.started {
269 OpenOptions::new().append(true).open(&path)
270 } else {
271 File::create(&path)
272 };
273 let written = opened.and_then(|mut file| writeln!(file, "{}", document.one_line()));
274 match written {
275 Ok(()) => self.started = true,
276 Err(problem) => {
277 self.metrics = None;
278 let _ = writeln!(
279 self.err,
280 "Error: cannot write metrics to \"{}\": {problem}",
281 path.display()
282 );
283 let _ = self.err.flush();
284 self.failed = true;
285 }
286 }
287 }
288
289 fn print(&mut self, result: &QueryResult) {
291 let text = render(result, &self.settings);
292 if !text.is_empty() {
293 let _ = write!(self.out, "{text}");
294 let _ = self.out.flush();
295 }
296 }
297
298 fn after_error(&mut self) -> Stop {
300 self.failed = true;
301 if self.bail { Stop::Failed } else { Stop::Done }
302 }
303
304 fn report(&mut self, problem: &Error, sql: &str) {
307 let _ = writeln!(self.err, "{problem}");
308 if let Some(span) = problem.span() {
309 if let Some(text) = pointer(sql, span) {
310 let _ = writeln!(self.err);
311 let _ = write!(self.err, "{text}");
312 }
313 }
314 let _ = self.err.flush();
315 }
316
317 fn run_dot(&mut self, line: &str) -> Stop {
319 let mut words = split(line);
320 if words.is_empty() {
321 return Stop::Done;
322 }
323 let name = words.remove(0);
324 let argument = |at: usize| words.get(at).cloned().unwrap_or_default();
325 match name.as_str() {
326 ".quit" | ".exit" => return Stop::Failed,
327 ".help" => {
328 let _ = write!(self.out, "{}", crate::help::DOT_COMMANDS);
329 }
330 ".mode" => {
331 if words.is_empty() {
332 let _ =
333 writeln!(self.out, "current output mode: {}", self.settings.format.name());
334 } else if let Some(format) = Format::from_name(&argument(0)) {
335 self.settings.set_format(format);
336 if let Some(table) = words.get(1) {
337 self.settings.table = table.clone();
338 }
339 } else {
340 return self.complain(&format!(
341 "Error: mode should be one of: {}",
342 crate::help::MODES
343 ));
344 }
345 }
346 ".headers" | ".header" => self.settings.header = on(&argument(0)),
347 ".separator" => {
348 self.settings.separator = argument(0);
349 if let Some(newline) = words.get(1) {
350 self.settings.newline = newline.clone();
351 }
352 }
353 ".nullvalue" | ".nullValue" => self.settings.nullvalue = argument(0),
354 ".timer" => self.timer = on(&argument(0)),
355 ".echo" => self.echo = on(&argument(0)),
356 ".bail" => self.bail = on(&argument(0)),
357 ".print" => {
358 let _ = writeln!(self.out, "{}", words.join(" "));
359 }
360 ".read" => return self.run_file(Path::new(&argument(0))),
361 ".output" => return self.redirect(words.first().map(String::as_str)),
362 ".tables" => self.tables(words.first().map(String::as_str)),
363 ".schema" => self.schema(words.first().map(String::as_str)),
364 ".databases" => {
365 let _ = writeln!(self.out, "memory:");
366 }
367 ".show" => self.show(),
368 ".open" => {
369 match Database::open(&argument(0)) {
373 Ok(database) => {
374 self.connection = database.connect();
375 self.database = database;
376 }
377 Err(problem) => return self.complain(&format!("Error: {}", problem.message())),
378 }
379 }
380 other => {
381 return self
382 .complain(&format!("Error: unknown command or invalid arguments: \"{}\". Enter \".help\" for help", other.trim_start_matches('.')));
383 }
384 }
385 let _ = self.out.flush();
386 Stop::Done
387 }
388
389 fn complain(&mut self, message: &str) -> Stop {
391 let _ = writeln!(self.err, "{message}");
392 let _ = self.err.flush();
393 self.after_error()
394 }
395
396 fn redirect(&mut self, path: Option<&str>) -> Stop {
398 let _ = self.out.flush();
399 match path {
400 None | Some("stdout") => {
401 if let Some(given) = self.given.take() {
402 self.out = Sink::Given(given);
403 }
404 }
405 Some(path) => {
406 let path = PathBuf::from(path);
407 match File::create(&path) {
408 Ok(file) => {
409 let opened = Sink::File(BufWriter::new(file), path);
410 if let Sink::Given(given) = std::mem::replace(&mut self.out, opened) {
411 self.given = Some(given);
412 }
413 }
414 Err(problem) => {
415 return self.complain(&format!(
416 "Error: cannot open \"{}\": {problem}",
417 path.display()
418 ));
419 }
420 }
421 }
422 }
423 Stop::Done
424 }
425
426 fn tables(&mut self, pattern: Option<&str>) {
428 let mut names = self.database.table_names();
429 names.sort();
430 for name in names {
431 if pattern.is_none_or(|pattern| matches(&name, pattern)) {
432 let _ = writeln!(self.out, "{name}");
433 }
434 }
435 }
436
437 fn schema(&mut self, wanted: Option<&str>) {
439 let mut names = self.database.table_names();
440 names.sort();
441 for name in names {
442 if wanted.is_some_and(|wanted| !matches(&name, wanted)) {
443 continue;
444 }
445 if let Ok(sql) = self.database.table_sql(&name) {
446 let _ = writeln!(self.out, "{sql}");
447 }
448 }
449 }
450
451 fn show(&mut self) {
456 let mut out = String::new();
457 let _ = writeln!(out, " echo: {}", off_on(self.echo));
458 let _ = writeln!(out, " headers: {}", off_on(self.settings.header));
459 let _ = writeln!(out, " mode: {}", self.settings.format.name());
460 let _ = writeln!(out, " nullvalue: \"{}\"", self.settings.nullvalue);
461 let _ = writeln!(out, " output: {}", self.output_name());
462 let _ = writeln!(out, "colseparator: \"{}\"", escaped(&self.settings.separator));
463 let _ = writeln!(out, "rowseparator: \"{}\"", escaped(&self.settings.newline));
464 let _ = writeln!(out, " width: ");
465 let _ = writeln!(out, " filename: {}", self.filename);
466 let _ = write!(self.out, "{out}");
467 }
468
469 fn output_name(&self) -> String {
471 match &self.out {
472 Sink::Given(_) => "stdout".to_string(),
473 Sink::File(_, path) => path.display().to_string(),
474 }
475 }
476}
477
478fn matches(name: &str, pattern: &str) -> bool {
480 let pattern = pattern.trim_matches('\'');
481 if let Some(prefix) = pattern.strip_suffix('%') {
482 name.starts_with(prefix)
483 } else {
484 name.eq_ignore_ascii_case(pattern)
485 }
486}
487
488fn split(line: &str) -> Vec<String> {
490 let mut words = Vec::new();
491 let mut current = String::new();
492 let mut quote = None;
493 let mut started = false;
494 for character in line.chars() {
495 match quote {
496 Some(open) if character == open => quote = None,
497 Some(_) => current.push(character),
498 None if character == '\'' || character == '"' => {
499 quote = Some(character);
500 started = true;
501 }
502 None if character.is_whitespace() => {
503 if started || !current.is_empty() {
504 words.push(std::mem::take(&mut current));
505 started = false;
506 }
507 }
508 None => current.push(character),
509 }
510 }
511 if started || !current.is_empty() {
512 words.push(current);
513 }
514 words
515}
516
517fn on(word: &str) -> bool {
519 !matches!(word, "off" | "0" | "false" | "no")
520}
521
522fn off_on(flag: bool) -> &'static str {
524 if flag { "on" } else { "off" }
525}
526
527fn pointer(sql: &str, span: Span) -> Option<String> {
533 let start = span.start as usize;
534 if start > sql.len() || !sql.is_char_boundary(start) {
535 return None;
536 }
537 let before = &sql[..start];
538 let number = before.matches('\n').count() + 1;
539 let line_start = before.rfind('\n').map_or(0, |at| at + 1);
540 let line_end = sql[line_start..].find('\n').map_or(sql.len(), |at| line_start + at);
541 let line = &sql[line_start..line_end];
542 let prefix = format!("LINE {number}: ");
543 let column = sql[line_start..start].chars().count();
544 Some(format!("{prefix}{line}\n{}^\n", " ".repeat(prefix.chars().count() + column)))
545}
546
547#[cfg(test)]
548mod tests {
549 use super::{matches, on, pointer, split};
550 use rudb::Span;
551
552 #[test]
553 fn a_dot_command_splits_on_whitespace() {
554 assert_eq!(split(".mode csv"), vec![".mode", "csv"]);
555 assert_eq!(split(" .timer on "), vec![".timer", "on"]);
556 }
557
558 #[test]
559 fn a_quoted_argument_keeps_its_spaces() {
560 assert_eq!(split(".separator ' | '"), vec![".separator", " | "]);
561 assert_eq!(split(".nullvalue \"\""), vec![".nullvalue", ""]);
562 }
563
564 #[test]
565 fn off_is_the_only_way_to_turn_something_off() {
566 assert!(on("on"));
567 assert!(on(""));
568 assert!(!on("off"));
569 assert!(!on("0"));
570 }
571
572 #[test]
573 fn a_pattern_ending_in_a_percent_is_a_prefix() {
574 assert!(matches("orders", "orders"));
575 assert!(matches("orders", "ORDERS"));
576 assert!(matches("orders", "ord%"));
577 assert!(!matches("orders", "lineitem"));
578 }
579
580 #[test]
581 fn the_caret_lands_under_the_span() {
582 let sql = "SELECT nosuch";
583 let text = pointer(sql, Span::new(7, 13)).expect("a pointer");
584 assert_eq!(text, "LINE 1: SELECT nosuch\n ^\n");
585 }
586
587 #[test]
588 fn the_caret_counts_lines() {
589 let sql = "SELECT\n nosuch";
590 let text = pointer(sql, Span::new(9, 15)).expect("a pointer");
591 assert_eq!(text, "LINE 2: nosuch\n ^\n");
592 }
593
594 #[test]
595 fn a_span_past_the_end_gets_no_pointer() {
596 assert!(pointer("SELECT 1", Span::new(100, 101)).is_none());
597 }
598}