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 print_fallbacks(&mut self) {
123 let _ = writeln!(self.err, "{}", rudb::fallback::report());
124 }
125
126 pub fn run_commands(&mut self, commands: &[Command]) -> Stop {
128 for command in commands {
129 let stop = match command {
130 Command::Sql(sql) => self.run_input(sql),
131 Command::File(path) => self.run_file(path),
132 };
133 if stop == Stop::Failed {
134 return Stop::Failed;
135 }
136 }
137 Stop::Done
138 }
139
140 pub fn run_file(&mut self, path: &Path) -> Stop {
142 match std::fs::read_to_string(path) {
143 Ok(text) => self.run_input(&text),
144 Err(problem) => {
145 let why = format!("Cannot open file \"{}\": {problem}", path.display());
146 self.report(&Error::io(why), "");
147 self.after_error()
148 }
149 }
150 }
151
152 pub fn greet(&mut self) {
154 let _ = writeln!(self.out, "rudb {}", crate::VERSION);
155 let _ = writeln!(self.out, "Enter \".help\" for usage hints.");
156 let _ = self.out.flush();
157 }
158
159 pub fn prompt(&mut self, stdin: &io::Stdin) -> Stop {
164 let mut pending = String::new();
165 loop {
166 let marker = if pending.is_empty() { "D " } else { "ยท " };
167 let _ = write!(self.out, "{marker}");
168 let _ = self.out.flush();
169 let mut line = String::new();
170 match stdin.read_line(&mut line) {
171 Ok(0) => {
172 let _ = writeln!(self.out);
173 return Stop::Done;
174 }
175 Ok(_) => {}
176 Err(_) => return Stop::Done,
177 }
178 let line = line.trim_end_matches(['\n', '\r']);
179 if pending.is_empty() && line.trim_start().starts_with('.') {
180 if self.run_dot(line.trim()) == Stop::Failed {
181 return Stop::Done;
182 }
183 continue;
184 }
185 if !pending.is_empty() {
186 pending.push('\n');
187 }
188 pending.push_str(line);
189 if rudb::is_complete(&pending) {
190 let statement = std::mem::take(&mut pending);
191 if self.run_sql(&statement) == Stop::Failed {
192 return Stop::Done;
193 }
194 }
195 }
196 }
197
198 pub fn run_input(&mut self, text: &str) -> Stop {
204 let mut pending = String::new();
205 for line in text.lines() {
206 if pending.trim().is_empty() && line.trim_start().starts_with('.') {
207 pending.clear();
208 if self.run_dot(line.trim()) == Stop::Failed {
209 return Stop::Failed;
210 }
211 continue;
212 }
213 if !pending.is_empty() {
214 pending.push('\n');
215 }
216 pending.push_str(line);
217 if rudb::is_complete(&pending) {
218 let statement = std::mem::take(&mut pending);
219 if self.run_sql(&statement) == Stop::Failed {
220 return Stop::Failed;
221 }
222 }
223 }
224 if pending.trim().is_empty() {
225 return Stop::Done;
226 }
227 self.run_sql(&pending)
228 }
229
230 fn run_sql(&mut self, text: &str) -> Stop {
232 let found = match rudb::statements(text) {
233 Ok(found) => found,
234 Err(problem) => {
235 self.report(&problem, text);
236 return self.after_error();
237 }
238 };
239 for statement in found {
240 if self.echo {
241 let _ = writeln!(self.out, "{}", statement.sql());
242 }
243 let started = Instant::now();
244 match self.connection.execute(statement.sql()) {
245 Ok(result) => {
246 self.record(&result);
247 self.print(&result);
248 if self.timer {
249 let _ = writeln!(
250 self.err,
251 "Run Time (s): real {:.9}",
252 started.elapsed().as_secs_f64()
253 );
254 }
255 }
256 Err(problem) => {
257 self.report(&problem, statement.sql());
258 return self.after_error();
259 }
260 }
261 }
262 Stop::Done
263 }
264
265 fn record(&mut self, result: &QueryResult) {
275 let (Some(path), Some(document)) = (self.metrics.clone(), result.metrics()) else {
276 return;
277 };
278 let opened = if self.started {
279 OpenOptions::new().append(true).open(&path)
280 } else {
281 File::create(&path)
282 };
283 let written = opened.and_then(|mut file| writeln!(file, "{}", document.one_line()));
284 match written {
285 Ok(()) => self.started = true,
286 Err(problem) => {
287 self.metrics = None;
288 let _ = writeln!(
289 self.err,
290 "Error: cannot write metrics to \"{}\": {problem}",
291 path.display()
292 );
293 let _ = self.err.flush();
294 self.failed = true;
295 }
296 }
297 }
298
299 fn print(&mut self, result: &QueryResult) {
301 let text = render(result, &self.settings);
302 if !text.is_empty() {
303 let _ = write!(self.out, "{text}");
304 let _ = self.out.flush();
305 }
306 }
307
308 fn after_error(&mut self) -> Stop {
310 self.failed = true;
311 if self.bail { Stop::Failed } else { Stop::Done }
312 }
313
314 fn report(&mut self, problem: &Error, sql: &str) {
317 let _ = writeln!(self.err, "{problem}");
318 if let Some(span) = problem.span() {
319 if let Some(text) = pointer(sql, span) {
320 let _ = writeln!(self.err);
321 let _ = write!(self.err, "{text}");
322 }
323 }
324 let _ = self.err.flush();
325 }
326
327 fn run_dot(&mut self, line: &str) -> Stop {
329 let mut words = split(line);
330 if words.is_empty() {
331 return Stop::Done;
332 }
333 let name = words.remove(0);
334 let argument = |at: usize| words.get(at).cloned().unwrap_or_default();
335 match name.as_str() {
336 ".quit" | ".exit" => return Stop::Failed,
337 ".help" => {
338 let _ = write!(self.out, "{}", crate::help::DOT_COMMANDS);
339 }
340 ".mode" => {
341 if words.is_empty() {
342 let _ =
343 writeln!(self.out, "current output mode: {}", self.settings.format.name());
344 } else if let Some(format) = Format::from_name(&argument(0)) {
345 self.settings.set_format(format);
346 if let Some(table) = words.get(1) {
347 self.settings.table = table.clone();
348 }
349 } else {
350 return self.complain(&format!(
351 "Error: mode should be one of: {}",
352 crate::help::MODES
353 ));
354 }
355 }
356 ".headers" | ".header" => self.settings.header = on(&argument(0)),
357 ".separator" => {
358 self.settings.separator = argument(0);
359 if let Some(newline) = words.get(1) {
360 self.settings.newline = newline.clone();
361 }
362 }
363 ".nullvalue" | ".nullValue" => self.settings.nullvalue = argument(0),
364 ".timer" => self.timer = on(&argument(0)),
365 ".echo" => self.echo = on(&argument(0)),
366 ".bail" => self.bail = on(&argument(0)),
367 ".print" => {
368 let _ = writeln!(self.out, "{}", words.join(" "));
369 }
370 ".read" => return self.run_file(Path::new(&argument(0))),
371 ".output" => return self.redirect(words.first().map(String::as_str)),
372 ".tables" => self.tables(words.first().map(String::as_str)),
373 ".schema" => self.schema(words.first().map(String::as_str)),
374 ".databases" => {
375 let _ = writeln!(self.out, "memory:");
376 }
377 ".show" => self.show(),
378 ".open" => {
379 match Database::open(&argument(0)) {
383 Ok(database) => {
384 self.connection = database.connect();
385 self.database = database;
386 }
387 Err(problem) => return self.complain(&format!("Error: {}", problem.message())),
388 }
389 }
390 other => {
391 return self
392 .complain(&format!("Error: unknown command or invalid arguments: \"{}\". Enter \".help\" for help", other.trim_start_matches('.')));
393 }
394 }
395 let _ = self.out.flush();
396 Stop::Done
397 }
398
399 fn complain(&mut self, message: &str) -> Stop {
401 let _ = writeln!(self.err, "{message}");
402 let _ = self.err.flush();
403 self.after_error()
404 }
405
406 fn redirect(&mut self, path: Option<&str>) -> Stop {
408 let _ = self.out.flush();
409 match path {
410 None | Some("stdout") => {
411 if let Some(given) = self.given.take() {
412 self.out = Sink::Given(given);
413 }
414 }
415 Some(path) => {
416 let path = PathBuf::from(path);
417 match File::create(&path) {
418 Ok(file) => {
419 let opened = Sink::File(BufWriter::new(file), path);
420 if let Sink::Given(given) = std::mem::replace(&mut self.out, opened) {
421 self.given = Some(given);
422 }
423 }
424 Err(problem) => {
425 return self.complain(&format!(
426 "Error: cannot open \"{}\": {problem}",
427 path.display()
428 ));
429 }
430 }
431 }
432 }
433 Stop::Done
434 }
435
436 fn tables(&mut self, pattern: Option<&str>) {
438 let mut names = self.database.table_names();
439 names.sort();
440 for name in names {
441 if pattern.is_none_or(|pattern| matches(&name, pattern)) {
442 let _ = writeln!(self.out, "{name}");
443 }
444 }
445 }
446
447 fn schema(&mut self, wanted: Option<&str>) {
449 let mut names = self.database.table_names();
450 names.sort();
451 for name in names {
452 if wanted.is_some_and(|wanted| !matches(&name, wanted)) {
453 continue;
454 }
455 if let Ok(sql) = self.database.table_sql(&name) {
456 let _ = writeln!(self.out, "{sql}");
457 }
458 }
459 }
460
461 fn show(&mut self) {
466 let mut out = String::new();
467 let _ = writeln!(out, " echo: {}", off_on(self.echo));
468 let _ = writeln!(out, " headers: {}", off_on(self.settings.header));
469 let _ = writeln!(out, " mode: {}", self.settings.format.name());
470 let _ = writeln!(out, " nullvalue: \"{}\"", self.settings.nullvalue);
471 let _ = writeln!(out, " output: {}", self.output_name());
472 let _ = writeln!(out, "colseparator: \"{}\"", escaped(&self.settings.separator));
473 let _ = writeln!(out, "rowseparator: \"{}\"", escaped(&self.settings.newline));
474 let _ = writeln!(out, " width: ");
475 let _ = writeln!(out, " filename: {}", self.filename);
476 let _ = write!(self.out, "{out}");
477 }
478
479 fn output_name(&self) -> String {
481 match &self.out {
482 Sink::Given(_) => "stdout".to_string(),
483 Sink::File(_, path) => path.display().to_string(),
484 }
485 }
486}
487
488fn matches(name: &str, pattern: &str) -> bool {
490 let pattern = pattern.trim_matches('\'');
491 if let Some(prefix) = pattern.strip_suffix('%') {
492 name.starts_with(prefix)
493 } else {
494 name.eq_ignore_ascii_case(pattern)
495 }
496}
497
498fn split(line: &str) -> Vec<String> {
500 let mut words = Vec::new();
501 let mut current = String::new();
502 let mut quote = None;
503 let mut started = false;
504 for character in line.chars() {
505 match quote {
506 Some(open) if character == open => quote = None,
507 Some(_) => current.push(character),
508 None if character == '\'' || character == '"' => {
509 quote = Some(character);
510 started = true;
511 }
512 None if character.is_whitespace() => {
513 if started || !current.is_empty() {
514 words.push(std::mem::take(&mut current));
515 started = false;
516 }
517 }
518 None => current.push(character),
519 }
520 }
521 if started || !current.is_empty() {
522 words.push(current);
523 }
524 words
525}
526
527fn on(word: &str) -> bool {
529 !matches!(word, "off" | "0" | "false" | "no")
530}
531
532fn off_on(flag: bool) -> &'static str {
534 if flag { "on" } else { "off" }
535}
536
537fn pointer(sql: &str, span: Span) -> Option<String> {
543 let start = span.start as usize;
544 if start > sql.len() || !sql.is_char_boundary(start) {
545 return None;
546 }
547 let before = &sql[..start];
548 let number = before.matches('\n').count() + 1;
549 let line_start = before.rfind('\n').map_or(0, |at| at + 1);
550 let line_end = sql[line_start..].find('\n').map_or(sql.len(), |at| line_start + at);
551 let line = &sql[line_start..line_end];
552 let prefix = format!("LINE {number}: ");
553 let column = sql[line_start..start].chars().count();
554 Some(format!("{prefix}{line}\n{}^\n", " ".repeat(prefix.chars().count() + column)))
555}
556
557#[cfg(test)]
558mod tests {
559 use super::{matches, on, pointer, split};
560 use rudb::Span;
561
562 #[test]
563 fn a_dot_command_splits_on_whitespace() {
564 assert_eq!(split(".mode csv"), vec![".mode", "csv"]);
565 assert_eq!(split(" .timer on "), vec![".timer", "on"]);
566 }
567
568 #[test]
569 fn a_quoted_argument_keeps_its_spaces() {
570 assert_eq!(split(".separator ' | '"), vec![".separator", " | "]);
571 assert_eq!(split(".nullvalue \"\""), vec![".nullvalue", ""]);
572 }
573
574 #[test]
575 fn off_is_the_only_way_to_turn_something_off() {
576 assert!(on("on"));
577 assert!(on(""));
578 assert!(!on("off"));
579 assert!(!on("0"));
580 }
581
582 #[test]
583 fn a_pattern_ending_in_a_percent_is_a_prefix() {
584 assert!(matches("orders", "orders"));
585 assert!(matches("orders", "ORDERS"));
586 assert!(matches("orders", "ord%"));
587 assert!(!matches("orders", "lineitem"));
588 }
589
590 #[test]
591 fn the_caret_lands_under_the_span() {
592 let sql = "SELECT nosuch";
593 let text = pointer(sql, Span::new(7, 13)).expect("a pointer");
594 assert_eq!(text, "LINE 1: SELECT nosuch\n ^\n");
595 }
596
597 #[test]
598 fn the_caret_counts_lines() {
599 let sql = "SELECT\n nosuch";
600 let text = pointer(sql, Span::new(9, 15)).expect("a pointer");
601 assert_eq!(text, "LINE 2: nosuch\n ^\n");
602 }
603
604 #[test]
605 fn a_span_past_the_end_gets_no_pointer() {
606 assert!(pointer("SELECT 1", Span::new(100, 101)).is_none());
607 }
608}