From 7a9fe8ac883078f3b608a7edebdf3441b3b41bbe Mon Sep 17 00:00:00 2001 From: PThorpe92 Date: Fri, 13 Dec 2024 11:44:39 -0500 Subject: [PATCH] Add cli command to set null value --- cli/app.rs | 120 +++++++++++++++++++++++++++++++---------------------- 1 file changed, 71 insertions(+), 49 deletions(-) diff --git a/cli/app.rs b/cli/app.rs index 67970ce54..4a41d9149 100644 --- a/cli/app.rs +++ b/cli/app.rs @@ -53,7 +53,7 @@ pub enum Command { Help, /// Display schema for a table Schema, - /// Set output file (stdout or file) + /// Set output file (or stdout if empty) SetOutput, /// Set output display mode OutputMode, @@ -63,13 +63,20 @@ pub enum Command { Cwd, /// Display information about settings ShowInfo, + /// Set the value of NULL to be printedin 'raw' mode + NullValue, } impl Command { fn min_args(&self) -> usize { (match self { - Self::Quit | Self::Help | Self::Opcodes | Self::ShowInfo | Self::SetOutput => 0, - Self::Open | Self::Schema | Self::OutputMode | Self::Cwd => 1, + Self::Quit + | Self::Schema + | Self::Help + | Self::Opcodes + | Self::ShowInfo + | Self::SetOutput => 0, + Self::Open | Self::OutputMode | Self::Cwd | Self::NullValue => 1, } + 1) // argv0 } @@ -78,12 +85,13 @@ impl Command { Self::Quit => ".quit", Self::Open => ".open ", Self::Help => ".help", - Self::Schema => ".schema ", + Self::Schema => ".schema ?
?", Self::Opcodes => ".opcodes", Self::OutputMode => ".mode ", - Self::SetOutput => ".output ", + Self::SetOutput => ".output ?file?", Self::Cwd => ".cd ", Self::ShowInfo => ".show", + Self::NullValue => ".nullvalue ", } } } @@ -101,6 +109,7 @@ impl FromStr for Command { ".output" => Ok(Self::SetOutput), ".cd" => Ok(Self::Cwd), ".show" => Ok(Self::ShowInfo), + ".nullvalue" => Ok(Self::NullValue), _ => Err("Unknown command".to_string()), } } @@ -118,6 +127,7 @@ pub struct Limbo { output_mode: OutputMode, is_stdout: bool, input_buff: String, + null_value: String, } impl Limbo { @@ -139,19 +149,38 @@ impl Limbo { } else { Box::new(io::stdout()) }; + let output = if opts.output.is_empty() { + "STDOUT" + } else { + &opts.output + }; Ok(Self { prompt: PROMPT.to_string(), io, writer, - output_filename: opts.output.clone(), + output_filename: output.to_string(), conn, db_file, output_mode: opts.output_mode, is_stdout: true, input_buff: String::new(), + null_value: String::new(), }) } + fn set_multiline_prompt(&mut self) { + self.prompt = match self.input_buff.chars().fold(0, |acc, c| match c { + '(' => acc + 1, + ')' => acc - 1, + _ => acc, + }) { + n if n < 0 => String::from(")x!...>"), + 0 => String::from(" ...> "), + n if n < 10 => format!("(x{}...> ", n), + _ => String::from("(.....> "), + }; + } + fn show_info(&mut self) -> std::io::Result<()> { self.writeln("------------------------------\nCurrent settings:")?; self.writeln(format!("Output mode: {}", self.output_mode))?; @@ -164,6 +193,12 @@ impl Limbo { "CWD: {}", std::env::current_dir().unwrap().display() ))?; + let null_value = if self.null_value.is_empty() { + "\'\'".to_string() + } else { + self.null_value.clone() + }; + self.writeln(format!("Null value: {}", null_value))?; self.writer.flush() } @@ -177,22 +212,27 @@ impl Limbo { } fn open_db(&mut self, path: &str) -> anyhow::Result<()> { - let db = Database::open_file(self.io.clone(), path)?; if self.conn.is_some() { + // close existing connection if open self.conn.as_mut().unwrap().close()?; } + let db = Database::open_file(self.io.clone(), path)?; self.conn = Some(db.connect()); self.db_file = Some(path.to_string()); Ok(()) } - fn set_output_file(&mut self, path: &str) -> io::Result<()> { - if let Ok(file) = std::fs::File::create(path) { - self.writer = Box::new(file); - self.is_stdout = false; - return Ok(()); + fn set_output_file(&mut self, path: &str) -> Result<(), String> { + match std::fs::File::create(path) { + Ok(file) => { + self.writer = Box::new(file); + self.is_stdout = false; + return Ok(()); + } + Err(e) => { + return Err(e.to_string()); + } } - Err(io::Error::new(io::ErrorKind::NotFound, "File not found")) } fn set_output_stdout(&mut self) { @@ -205,19 +245,11 @@ impl Limbo { self.output_mode = mode; } - fn write>(&mut self, data: D) -> io::Result<()> { - self.writer.write_all(data.as_ref()) - } - fn writeln>(&mut self, data: D) -> io::Result<()> { self.writer.write_all(data.as_ref())?; self.writer.write_all(b"\n") } - fn is_first_input(&self) -> bool { - self.input_buff.is_empty() - } - fn buffer_input(&mut self, line: &str) { self.input_buff.push_str(line); self.input_buff.push(' '); @@ -229,7 +261,7 @@ impl Limbo { interrupt_count: &Arc, rl: &mut rustyline::DefaultEditor, ) -> anyhow::Result<()> { - if self.is_first_input() { + if self.input_buff.is_empty() { if line.is_empty() { return Ok(()); } @@ -248,18 +280,13 @@ impl Limbo { .filter(|s| !s.is_empty()) .for_each(|stmt| { if let Err(e) = self.query(stmt, interrupt_count) { - eprintln!("{}", e); + let _ = self.writeln(e.to_string()); } }); self.reset_input(); } else { self.buffer_input(line); - self.prompt = match calc_parens_offset(&self.input_buff) { - n if n < 0 => String::from(")x!...>"), - 0 => String::from(" ...> "), - n if n < 10 => format!("(x{}...> ", n), - _ => String::from("(.....> "), - }; + self.set_multiline_prompt(); } rl.add_history_entry(line.to_owned())?; interrupt_count.store(0, Ordering::SeqCst); @@ -294,7 +321,7 @@ impl Limbo { } let table_name = args.get(1).copied(); if let Err(e) = self.display_schema(table_name) { - let _ = self.writeln(format!("{}", e)); + let _ = self.writeln(e.to_string()); } } Command::Opcodes => { @@ -310,6 +337,9 @@ impl Limbo { } } } + Command::NullValue => { + self.null_value = args[1].to_string(); + } Command::OutputMode => match OutputMode::from_str(args[1], true) { Ok(mode) => { self.set_mode(mode); @@ -328,10 +358,6 @@ impl Limbo { } } Command::Cwd => { - if args.len() < 2 { - println!("USAGE: .cd "); - return; - } let _ = std::env::set_current_dir(args[1]); } Command::ShowInfo => { @@ -369,9 +395,9 @@ impl Limbo { if i > 0 { let _ = self.writer.write(b"|"); } - self.write( + let _ = self.writer.write( match value { - Value::Null => "".to_string(), + Value::Null => self.null_value.clone(), Value::Integer(i) => format!("{}", i), Value::Float(f) => format!("{:?}", f), Value::Text(s) => s.to_string(), @@ -391,7 +417,7 @@ impl Limbo { break; } Err(err) => { - eprintln!("{}", err); + let _ = self.writeln(err.to_string()); break; } } @@ -409,7 +435,7 @@ impl Limbo { row.values .iter() .map(|value| match value { - Value::Null => "".cell(), + Value::Null => self.null_value.clone().cell(), Value::Integer(i) => i.to_string().cell(), Value::Float(f) => f.to_string().cell(), Value::Text(s) => s.cell(), @@ -433,13 +459,13 @@ impl Limbo { if let Ok(table) = table_rows.table().display() { let _ = self.writeln(format!("{}", table)); } else { - eprintln!("Error displaying table."); + let _ = self.writeln("Error displaying table."); } } }, Ok(None) => {} Err(err) => { - self.writeln(format!("{}", err)); + let _ = self.writeln(format!("{}", err)); } } // for now let's cache flush always @@ -484,7 +510,7 @@ impl Limbo { } } Ok(None) => { - println!("No results returned from the query."); + let _ = self.writeln("No results returned from the query."); } Err(err) => { if err.to_string().contains("no such table: sqlite_schema") { @@ -515,6 +541,7 @@ Special Commands: .schema Show the schema of the specified table. .opcodes Display all the opcodes defined by the virtual machine .cd Change the current working directory. +.nullvalue Set the value to be displayed for null values. .help Display this help message. Usage Examples: @@ -543,17 +570,12 @@ Usage Examples: 8. Show the current values of settings: .show +9. Set the value 'NULL' to be displayed for null values instead of empty string: + .nullvalue "NULL" + Note: ----- - All SQL commands must end with a semicolon (;). - Special commands do not require a semicolon. "#; - -fn calc_parens_offset(input: &str) -> i32 { - input.chars().fold(0, |acc, c| match c { - '(' => acc + 1, - ')' => acc - 1, - _ => acc, - }) -}