1use std::fmt::Write as _;
4use std::fs::File;
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}
70
71impl std::fmt::Debug for Shell {
72 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73 f.debug_struct("Shell")
74 .field("settings", &self.settings)
75 .field("failed", &self.failed)
76 .finish_non_exhaustive()
77 }
78}
79
80impl Shell {
81 pub fn new(
83 options: &Options,
84 database: Database,
85 out: Box<dyn Write>,
86 err: Box<dyn Write>,
87 ) -> Self {
88 Self {
89 connection: database.connect(),
90 database,
91 settings: options.settings.clone(),
92 out: Sink::Given(out),
93 err,
94 filename: options.database.clone(),
95 given: None,
96 timer: false,
97 echo: options.echo,
98 bail: options.bail,
99 failed: false,
100 }
101 }
102
103 pub fn failed(&self) -> bool {
105 self.failed
106 }
107
108 pub fn run_commands(&mut self, commands: &[Command]) -> Stop {
110 for command in commands {
111 let stop = match command {
112 Command::Sql(sql) => self.run_input(sql),
113 Command::File(path) => self.run_file(path),
114 };
115 if stop == Stop::Failed {
116 return Stop::Failed;
117 }
118 }
119 Stop::Done
120 }
121
122 pub fn run_file(&mut self, path: &Path) -> Stop {
124 match std::fs::read_to_string(path) {
125 Ok(text) => self.run_input(&text),
126 Err(problem) => {
127 let why = format!("Cannot open file \"{}\": {problem}", path.display());
128 self.report(&Error::io(why), "");
129 self.after_error()
130 }
131 }
132 }
133
134 pub fn greet(&mut self) {
136 let _ = writeln!(self.out, "rudb {}", crate::VERSION);
137 let _ = writeln!(self.out, "Enter \".help\" for usage hints.");
138 let _ = self.out.flush();
139 }
140
141 pub fn prompt(&mut self, stdin: &io::Stdin) -> Stop {
146 let mut pending = String::new();
147 loop {
148 let marker = if pending.is_empty() { "D " } else { "ยท " };
149 let _ = write!(self.out, "{marker}");
150 let _ = self.out.flush();
151 let mut line = String::new();
152 match stdin.read_line(&mut line) {
153 Ok(0) => {
154 let _ = writeln!(self.out);
155 return Stop::Done;
156 }
157 Ok(_) => {}
158 Err(_) => return Stop::Done,
159 }
160 let line = line.trim_end_matches(['\n', '\r']);
161 if pending.is_empty() && line.trim_start().starts_with('.') {
162 if self.run_dot(line.trim()) == Stop::Failed {
163 return Stop::Done;
164 }
165 continue;
166 }
167 if !pending.is_empty() {
168 pending.push('\n');
169 }
170 pending.push_str(line);
171 if rudb::is_complete(&pending) {
172 let statement = std::mem::take(&mut pending);
173 if self.run_sql(&statement) == Stop::Failed {
174 return Stop::Done;
175 }
176 }
177 }
178 }
179
180 pub fn run_input(&mut self, text: &str) -> Stop {
186 let mut pending = String::new();
187 for line in text.lines() {
188 if pending.trim().is_empty() && line.trim_start().starts_with('.') {
189 pending.clear();
190 if self.run_dot(line.trim()) == Stop::Failed {
191 return Stop::Failed;
192 }
193 continue;
194 }
195 if !pending.is_empty() {
196 pending.push('\n');
197 }
198 pending.push_str(line);
199 if rudb::is_complete(&pending) {
200 let statement = std::mem::take(&mut pending);
201 if self.run_sql(&statement) == Stop::Failed {
202 return Stop::Failed;
203 }
204 }
205 }
206 if pending.trim().is_empty() {
207 return Stop::Done;
208 }
209 self.run_sql(&pending)
210 }
211
212 fn run_sql(&mut self, text: &str) -> Stop {
214 let found = match rudb::statements(text) {
215 Ok(found) => found,
216 Err(problem) => {
217 self.report(&problem, text);
218 return self.after_error();
219 }
220 };
221 for statement in found {
222 if self.echo {
223 let _ = writeln!(self.out, "{}", statement.sql());
224 }
225 let started = Instant::now();
226 match self.connection.execute(statement.sql()) {
227 Ok(result) => {
228 self.print(&result);
229 if self.timer {
230 let _ = writeln!(
231 self.err,
232 "Run Time (s): real {:.3}",
233 started.elapsed().as_secs_f64()
234 );
235 }
236 }
237 Err(problem) => {
238 self.report(&problem, statement.sql());
239 return self.after_error();
240 }
241 }
242 }
243 Stop::Done
244 }
245
246 fn print(&mut self, result: &QueryResult) {
248 let text = render(result, &self.settings);
249 if !text.is_empty() {
250 let _ = write!(self.out, "{text}");
251 let _ = self.out.flush();
252 }
253 }
254
255 fn after_error(&mut self) -> Stop {
257 self.failed = true;
258 if self.bail { Stop::Failed } else { Stop::Done }
259 }
260
261 fn report(&mut self, problem: &Error, sql: &str) {
264 let _ = writeln!(self.err, "{problem}");
265 if let Some(span) = problem.span() {
266 if let Some(text) = pointer(sql, span) {
267 let _ = writeln!(self.err);
268 let _ = write!(self.err, "{text}");
269 }
270 }
271 let _ = self.err.flush();
272 }
273
274 fn run_dot(&mut self, line: &str) -> Stop {
276 let mut words = split(line);
277 if words.is_empty() {
278 return Stop::Done;
279 }
280 let name = words.remove(0);
281 let argument = |at: usize| words.get(at).cloned().unwrap_or_default();
282 match name.as_str() {
283 ".quit" | ".exit" => return Stop::Failed,
284 ".help" => {
285 let _ = write!(self.out, "{}", crate::help::DOT_COMMANDS);
286 }
287 ".mode" => {
288 if words.is_empty() {
289 let _ =
290 writeln!(self.out, "current output mode: {}", self.settings.format.name());
291 } else if let Some(format) = Format::from_name(&argument(0)) {
292 self.settings.set_format(format);
293 if let Some(table) = words.get(1) {
294 self.settings.table = table.clone();
295 }
296 } else {
297 return self.complain(&format!(
298 "Error: mode should be one of: {}",
299 crate::help::MODES
300 ));
301 }
302 }
303 ".headers" | ".header" => self.settings.header = on(&argument(0)),
304 ".separator" => {
305 self.settings.separator = argument(0);
306 if let Some(newline) = words.get(1) {
307 self.settings.newline = newline.clone();
308 }
309 }
310 ".nullvalue" | ".nullValue" => self.settings.nullvalue = argument(0),
311 ".timer" => self.timer = on(&argument(0)),
312 ".echo" => self.echo = on(&argument(0)),
313 ".bail" => self.bail = on(&argument(0)),
314 ".print" => {
315 let _ = writeln!(self.out, "{}", words.join(" "));
316 }
317 ".read" => return self.run_file(Path::new(&argument(0))),
318 ".output" => return self.redirect(words.first().map(String::as_str)),
319 ".tables" => self.tables(words.first().map(String::as_str)),
320 ".schema" => self.schema(words.first().map(String::as_str)),
321 ".databases" => {
322 let _ = writeln!(self.out, "memory:");
323 }
324 ".show" => self.show(),
325 ".open" => {
326 match Database::open(&argument(0)) {
330 Ok(database) => {
331 self.connection = database.connect();
332 self.database = database;
333 }
334 Err(problem) => return self.complain(&format!("Error: {}", problem.message())),
335 }
336 }
337 other => {
338 return self
339 .complain(&format!("Error: unknown command or invalid arguments: \"{}\". Enter \".help\" for help", other.trim_start_matches('.')));
340 }
341 }
342 let _ = self.out.flush();
343 Stop::Done
344 }
345
346 fn complain(&mut self, message: &str) -> Stop {
348 let _ = writeln!(self.err, "{message}");
349 let _ = self.err.flush();
350 self.after_error()
351 }
352
353 fn redirect(&mut self, path: Option<&str>) -> Stop {
355 let _ = self.out.flush();
356 match path {
357 None | Some("stdout") => {
358 if let Some(given) = self.given.take() {
359 self.out = Sink::Given(given);
360 }
361 }
362 Some(path) => {
363 let path = PathBuf::from(path);
364 match File::create(&path) {
365 Ok(file) => {
366 let opened = Sink::File(BufWriter::new(file), path);
367 if let Sink::Given(given) = std::mem::replace(&mut self.out, opened) {
368 self.given = Some(given);
369 }
370 }
371 Err(problem) => {
372 return self.complain(&format!(
373 "Error: cannot open \"{}\": {problem}",
374 path.display()
375 ));
376 }
377 }
378 }
379 }
380 Stop::Done
381 }
382
383 fn tables(&mut self, pattern: Option<&str>) {
385 let mut names = self.database.table_names();
386 names.sort();
387 for name in names {
388 if pattern.is_none_or(|pattern| matches(&name, pattern)) {
389 let _ = writeln!(self.out, "{name}");
390 }
391 }
392 }
393
394 fn schema(&mut self, wanted: Option<&str>) {
396 let mut names = self.database.table_names();
397 names.sort();
398 for name in names {
399 if wanted.is_some_and(|wanted| !matches(&name, wanted)) {
400 continue;
401 }
402 if let Ok(sql) = self.database.table_sql(&name) {
403 let _ = writeln!(self.out, "{sql}");
404 }
405 }
406 }
407
408 fn show(&mut self) {
413 let mut out = String::new();
414 let _ = writeln!(out, " echo: {}", off_on(self.echo));
415 let _ = writeln!(out, " headers: {}", off_on(self.settings.header));
416 let _ = writeln!(out, " mode: {}", self.settings.format.name());
417 let _ = writeln!(out, " nullvalue: \"{}\"", self.settings.nullvalue);
418 let _ = writeln!(out, " output: {}", self.output_name());
419 let _ = writeln!(out, "colseparator: \"{}\"", escaped(&self.settings.separator));
420 let _ = writeln!(out, "rowseparator: \"{}\"", escaped(&self.settings.newline));
421 let _ = writeln!(out, " width: ");
422 let _ = writeln!(out, " filename: {}", self.filename);
423 let _ = write!(self.out, "{out}");
424 }
425
426 fn output_name(&self) -> String {
428 match &self.out {
429 Sink::Given(_) => "stdout".to_string(),
430 Sink::File(_, path) => path.display().to_string(),
431 }
432 }
433}
434
435fn matches(name: &str, pattern: &str) -> bool {
437 let pattern = pattern.trim_matches('\'');
438 if let Some(prefix) = pattern.strip_suffix('%') {
439 name.starts_with(prefix)
440 } else {
441 name.eq_ignore_ascii_case(pattern)
442 }
443}
444
445fn split(line: &str) -> Vec<String> {
447 let mut words = Vec::new();
448 let mut current = String::new();
449 let mut quote = None;
450 let mut started = false;
451 for character in line.chars() {
452 match quote {
453 Some(open) if character == open => quote = None,
454 Some(_) => current.push(character),
455 None if character == '\'' || character == '"' => {
456 quote = Some(character);
457 started = true;
458 }
459 None if character.is_whitespace() => {
460 if started || !current.is_empty() {
461 words.push(std::mem::take(&mut current));
462 started = false;
463 }
464 }
465 None => current.push(character),
466 }
467 }
468 if started || !current.is_empty() {
469 words.push(current);
470 }
471 words
472}
473
474fn on(word: &str) -> bool {
476 !matches!(word, "off" | "0" | "false" | "no")
477}
478
479fn off_on(flag: bool) -> &'static str {
481 if flag { "on" } else { "off" }
482}
483
484fn pointer(sql: &str, span: Span) -> Option<String> {
490 let start = span.start as usize;
491 if start > sql.len() || !sql.is_char_boundary(start) {
492 return None;
493 }
494 let before = &sql[..start];
495 let number = before.matches('\n').count() + 1;
496 let line_start = before.rfind('\n').map_or(0, |at| at + 1);
497 let line_end = sql[line_start..].find('\n').map_or(sql.len(), |at| line_start + at);
498 let line = &sql[line_start..line_end];
499 let prefix = format!("LINE {number}: ");
500 let column = sql[line_start..start].chars().count();
501 Some(format!("{prefix}{line}\n{}^\n", " ".repeat(prefix.chars().count() + column)))
502}
503
504#[cfg(test)]
505mod tests {
506 use super::{matches, on, pointer, split};
507 use rudb::Span;
508
509 #[test]
510 fn a_dot_command_splits_on_whitespace() {
511 assert_eq!(split(".mode csv"), vec![".mode", "csv"]);
512 assert_eq!(split(" .timer on "), vec![".timer", "on"]);
513 }
514
515 #[test]
516 fn a_quoted_argument_keeps_its_spaces() {
517 assert_eq!(split(".separator ' | '"), vec![".separator", " | "]);
518 assert_eq!(split(".nullvalue \"\""), vec![".nullvalue", ""]);
519 }
520
521 #[test]
522 fn off_is_the_only_way_to_turn_something_off() {
523 assert!(on("on"));
524 assert!(on(""));
525 assert!(!on("off"));
526 assert!(!on("0"));
527 }
528
529 #[test]
530 fn a_pattern_ending_in_a_percent_is_a_prefix() {
531 assert!(matches("orders", "orders"));
532 assert!(matches("orders", "ORDERS"));
533 assert!(matches("orders", "ord%"));
534 assert!(!matches("orders", "lineitem"));
535 }
536
537 #[test]
538 fn the_caret_lands_under_the_span() {
539 let sql = "SELECT nosuch";
540 let text = pointer(sql, Span::new(7, 13)).expect("a pointer");
541 assert_eq!(text, "LINE 1: SELECT nosuch\n ^\n");
542 }
543
544 #[test]
545 fn the_caret_counts_lines() {
546 let sql = "SELECT\n nosuch";
547 let text = pointer(sql, Span::new(9, 15)).expect("a pointer");
548 assert_eq!(text, "LINE 2: nosuch\n ^\n");
549 }
550
551 #[test]
552 fn a_span_past_the_end_gets_no_pointer() {
553 assert!(pointer("SELECT 1", Span::new(100, 101)).is_none());
554 }
555}