From f6473ea40d6f7e12a61cc6eb41690b55d1268d5c Mon Sep 17 00:00:00 2001 From: PThorpe92 Date: Thu, 12 Dec 2024 23:01:32 -0500 Subject: [PATCH 1/3] Add several cli commands, re-structure shell --- cli/app.rs | 471 ++++++++++++++++++++++++++++++++++++++++++++++++++++ cli/main.rs | 308 ++-------------------------------- 2 files changed, 489 insertions(+), 290 deletions(-) create mode 100644 cli/app.rs diff --git a/cli/app.rs b/cli/app.rs new file mode 100644 index 000000000..7a9ea51a4 --- /dev/null +++ b/cli/app.rs @@ -0,0 +1,471 @@ +use crate::opcodes_dictionary::OPCODE_DESCRIPTIONS; +use cli_table::{Cell, Table}; +use limbo_core::{Database, RowResult, Value, IO}; + +use clap::{Parser, ValueEnum}; +use std::{ + io::{self, LineWriter, Write}, + path::PathBuf, + rc::Rc, + str::FromStr, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }, +}; + +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +pub struct Opts { + pub database: Option, + pub sql: Option, + #[clap(short, long, default_value_t = OutputMode::Raw)] + pub output_mode: OutputMode, +} + +#[derive(ValueEnum, Copy, Clone, Debug, PartialEq, Eq)] +pub enum OutputMode { + Raw, + Pretty, +} + +impl std::fmt::Display for OutputMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.to_possible_value() + .expect("no values are skipped") + .get_name() + .fmt(f) + } +} + +#[derive(Debug, Clone)] +pub enum Command { + /// Quit the shell + Quit, + /// Open a database file + Open, + /// Display help message + Help, + /// Display schema for a table + Schema, + /// Set output file (stdout or file) + SetOutput, + /// Set output display mode + OutputMode, + /// Show vdbe opcodes + Opcodes, + /// Change the current working directory + Cwd, + /// Display information about settings + ShowInfo, +} + +impl FromStr for Command { + type Err = String; + fn from_str(s: &str) -> Result { + match s { + ".quit" => Ok(Self::Quit), + ".open" => Ok(Self::Open), + ".help" => Ok(Self::Help), + ".schema" => Ok(Self::Schema), + ".opcodes" => Ok(Self::Opcodes), + ".mode" => Ok(Self::OutputMode), + ".output" => Ok(Self::SetOutput), + ".cd" => Ok(Self::Cwd), + ".show" => Ok(Self::ShowInfo), + _ => Err("Unknown command".to_string()), + } + } +} + +pub struct Limbo { + io: Arc, + writer: Box, + conn: Option>, + filename: Option, + db_file: Option, + pub interrupt_count: Arc, + pub output_mode: OutputMode, + pub is_stdout: bool, +} + +impl Limbo { + #[allow(clippy::arc_with_non_send_sync)] + pub fn new(opts: &Opts) -> anyhow::Result { + println!("Limbo v{}", env!("CARGO_PKG_VERSION")); + println!("Enter \".help\" for usage hints."); + let io = Arc::new(limbo_core::PlatformIO::new()?); + let mut db_file = None; + let conn = if let Some(path) = &opts.database { + let path = path.to_str().unwrap(); + db_file = Some(path.to_string()); + let db = Database::open_file(io.clone(), path)?; + Some(db.connect()) + } else { + println!("No database file specified: Use .open to open a database."); + None + }; + Ok(Self { + io: Arc::new(limbo_core::PlatformIO::new()?), + writer: Box::new(LineWriter::new(std::io::stdout())), + filename: None, + conn, + db_file, + interrupt_count: AtomicUsize::new(0).into(), + output_mode: opts.output_mode, + is_stdout: true, + }) + } + + fn show_info(&mut self) { + self.writeln("------------------------------\nCurrent settings:"); + self.writeln(format!("Output mode: {}", self.output_mode)); + let output = self + .filename + .as_ref() + .unwrap_or(&"STDOUT".to_string()) + .clone(); + self.writeln(format!("Output mode: {output}")); + self.writeln(format!( + "DB filename: {}", + self.db_file.clone().unwrap_or(":none:".to_string()) + )); + self.writeln(format!( + "CWD: {}", + std::env::current_dir().unwrap().display() + )); + let _ = self.writer.flush(); + } + + pub fn close(&mut self) { + self.conn.as_mut().map(|c| c.close()); + } + + pub fn open_db(&mut self, path: &str) -> anyhow::Result<()> { + let db = Database::open_file(self.io.clone(), path)?; + self.conn = Some(db.connect()); + self.db_file = Some(path.to_string()); + Ok(()) + } + + pub 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(()); + } + Err(io::Error::new(io::ErrorKind::NotFound, "File not found")) + } + + fn set_output_stdout(&mut self) { + self.writer = Box::new(io::stdout()); + self.is_stdout = true; + } + + pub fn set_mode(&mut self, mode: OutputMode) { + self.output_mode = mode; + } + + pub fn write>(&mut self, data: D) -> io::Result<()> { + self.writer.write_all(data.as_ref()) + } + + pub fn writeln>(&mut self, data: D) { + self.writer.write_all(data.as_ref()).unwrap(); + self.writer.write_all(b"\n").unwrap(); + let _ = self.writer.flush(); + } + + pub fn reset_interrupt_count(&self) { + self.interrupt_count + .store(0, std::sync::atomic::Ordering::SeqCst); + } + + pub fn display_help_message(&mut self) { + let _ = self.writer.write_all(HELP_MSG.as_ref()); + } + + pub fn incr_inturrupt_count(&mut self) -> usize { + self.interrupt_count + .fetch_add(1, std::sync::atomic::Ordering::SeqCst) + } + + pub fn handle_dot_command(&mut self, line: &str) { + let args: Vec<&str> = line.split_whitespace().collect(); + if args.is_empty() { + return; + } + + match Command::from_str(args[0]) { + Ok(Command::Quit) => { + println!("Exiting Limbo SQL Shell."); + self.close(); + std::process::exit(0) + } + Ok(Command::Open) => { + if args.len() < 2 { + println!("Error: No database file specified."); + } else if self.open_db(args[1]).is_err() { + println!("Error: Unable to open database file."); + } + } + Ok(Command::Schema) => { + if self.conn.is_none() { + println!("Error: no database currently open"); + return; + } + let table_name = args.get(1).copied(); + let _ = self.display_schema(table_name); + } + Ok(Command::Opcodes) => { + if args.len() > 1 { + for op in &OPCODE_DESCRIPTIONS { + if op.name.eq_ignore_ascii_case(args.get(1).unwrap()) { + self.writeln(format!("{}", op)); + } + } + } else { + for op in &OPCODE_DESCRIPTIONS { + println!("{}\n", op); + } + } + } + Ok(Command::OutputMode) => { + if args.len() < 2 { + println!("Error: No output mode specified."); + return; + } + match OutputMode::from_str(args[1], true) { + Ok(mode) => { + self.set_mode(mode); + } + Err(e) => { + println!("{e}"); + } + } + } + Ok(Command::SetOutput) => { + if args.len() == 2 { + if let Err(e) = self.set_output_file(args[1]) { + println!("Error: {}", e); + } + } else { + self.set_output_stdout(); + } + } + Ok(Command::Cwd) => { + if args.len() < 2 { + println!("USAGE: .cd "); + return; + } + let _ = std::env::set_current_dir(args[1]); + } + Ok(Command::ShowInfo) => { + self.show_info(); + } + Ok(Command::Help) => { + self.display_help_message(); + } + _ => { + println!("Unknown command: {}", args[0]); + println!("enter: .help for all available commands"); + } + } + } + + pub fn query(&mut self, sql: &str) -> anyhow::Result<()> { + if self.conn.is_none() { + println!("Error: No database file specified."); + return Ok(()); + } + let conn = self.conn.as_ref().unwrap().clone(); + match conn.query(sql) { + Ok(Some(ref mut rows)) => match self.output_mode { + OutputMode::Raw => loop { + if self.interrupt_count.load(Ordering::SeqCst) > 0 { + println!("Query interrupted."); + return Ok(()); + } + + match rows.next_row() { + Ok(RowResult::Row(row)) => { + for (i, value) in row.values.iter().enumerate() { + if i > 0 { + let _ = self.write(b"|"); + } + self.write( + match value { + Value::Null => "".to_string(), + Value::Integer(i) => format!("{}", i), + Value::Float(f) => format!("{:?}", f), + Value::Text(s) => s.to_string(), + Value::Blob(b) => { + format!("{}", String::from_utf8_lossy(b)) + } + } + .as_bytes(), + )?; + } + self.writeln(""); + } + Ok(RowResult::IO) => { + self.io.run_once()?; + } + Ok(RowResult::Done) => { + break; + } + Err(err) => { + eprintln!("{}", err); + break; + } + } + }, + OutputMode::Pretty => { + if self.interrupt_count.load(Ordering::SeqCst) > 0 { + println!("Query interrupted."); + return Ok(()); + } + let mut table_rows: Vec> = vec![]; + loop { + match rows.next_row() { + Ok(RowResult::Row(row)) => { + table_rows.push( + row.values + .iter() + .map(|value| match value { + Value::Null => "".cell(), + Value::Integer(i) => i.to_string().cell(), + Value::Float(f) => f.to_string().cell(), + Value::Text(s) => s.cell(), + Value::Blob(b) => { + format!("{}", String::from_utf8_lossy(b)).cell() + } + }) + .collect(), + ); + } + Ok(RowResult::IO) => { + self.io.run_once()?; + } + Ok(RowResult::Done) => break, + Err(err) => { + eprintln!("{}", err); + break; + } + } + } + let table = table_rows.table(); + cli_table::print_stdout(table).unwrap(); + } + }, + Ok(None) => {} + Err(err) => { + eprintln!("{}", err); + } + } + // for now let's cache flush always + conn.cacheflush()?; + Ok(()) + } + + fn display_schema(&mut self, table: Option<&str>) -> anyhow::Result<()> { + let sql = match table { + Some(table_name) => format!( + "SELECT sql FROM sqlite_schema WHERE type IN ('table', 'index') AND tbl_name = '{}' AND name NOT LIKE 'sqlite_%'", + table_name + ), + None => String::from( + "SELECT sql FROM sqlite_schema WHERE type IN ('table', 'index') AND name NOT LIKE 'sqlite_%'" + ), + }; + + match self.conn.as_ref().unwrap().query(&sql) { + Ok(Some(ref mut rows)) => { + let mut found = false; + loop { + match rows.next_row()? { + RowResult::Row(row) => { + if let Some(Value::Text(schema)) = row.values.first() { + self.writeln(format!("{};", schema)); + found = true; + } + } + RowResult::IO => { + self.io.run_once()?; + } + RowResult::Done => break, + } + } + if !found { + if let Some(table_name) = table { + self.writeln(format!("Error: Table '{}' not found.", table_name)); + } else { + self.writeln("No tables or indexes found in the database."); + } + } + } + Ok(None) => { + println!("No results returned from the query."); + } + Err(err) => { + if err.to_string().contains("no such table: sqlite_schema") { + return Err(anyhow::anyhow!("Unable to access database schema. The database may be using an older SQLite version or may not be properly initialized.")); + } else { + return Err(anyhow::anyhow!("Error querying schema: {}", err)); + } + } + } + + Ok(()) + } +} + +const HELP_MSG: &str = r#" +Limbo SQL Shell Help +============== + +Welcome to the Limbo SQL Shell! You can execute any standard SQL command here. +In addition to standard SQL commands, the following special commands are available: + +Special Commands: +----------------- +.quit Stop interpreting input stream and exit. +.open Open and connect to a database file. +.output Change the output mode. Available modes are 'raw' and 'pretty'. +.schema Show the schema of the specified table. +.opcodes Display all the opcodes defined by the virtual machine +.cd Change the current working directory. +.help Display this help message. + +Usage Examples: +--------------- +1. To quit the Limbo SQL Shell: + .quit + +2. To open a database file at path './employees.db': + .open employees.db + +3. To view the schema of a table named 'employees': + .schema employees + +4. To list all available SQL opcodes: + .opcodes + +5. To change the current output mode to 'pretty': + .mode pretty + +6. Send output to STDOUT if no file is specified: + .output + +7. To change the current working directory to '/tmp': + .cd /tmp + +8. Show the current values of settings: + .show + +Note: +----- +- All SQL commands must end with a semicolon (;). +- Special commands do not require a semicolon. + +"#; diff --git a/cli/main.rs b/cli/main.rs index 389fac710..dab6d856c 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -1,53 +1,19 @@ +mod app; mod opcodes_dictionary; -use clap::{Parser, ValueEnum}; -use cli_table::{Cell, Table}; -use limbo_core::{Database, RowResult, Value}; -use opcodes_dictionary::OPCODE_DESCRIPTIONS; +use clap::Parser; use rustyline::{error::ReadlineError, DefaultEditor}; -use std::path::PathBuf; -use std::rc::Rc; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; -#[derive(ValueEnum, Copy, Clone, Debug, PartialEq, Eq)] -enum OutputMode { - Raw, - Pretty, -} - -impl std::fmt::Display for OutputMode { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.to_possible_value() - .expect("no values are skipped") - .get_name() - .fmt(f) - } -} - -#[derive(Parser)] -#[command(author, version, about, long_about = None)] -struct Opts { - database: PathBuf, - sql: Option, - #[clap(short, long, default_value_t = OutputMode::Raw)] - output_mode: OutputMode, -} - #[allow(clippy::arc_with_non_send_sync)] fn main() -> anyhow::Result<()> { env_logger::init(); - let opts = Opts::parse(); - let path = opts.database.to_str().unwrap(); - let io = Arc::new(limbo_core::PlatformIO::new()?); - let db = Database::open_file(io.clone(), path)?; - let conn = db.connect(); - - let interrupt_count = Arc::new(AtomicUsize::new(0)); + let opts = app::Opts::parse(); + let mut app = app::Limbo::new(&opts)?; { - let interrupt_count = Arc::clone(&interrupt_count); - + let interrupt_count: Arc = app.interrupt_count.clone(); ctrlc::set_handler(move || { // Increment the interrupt count on Ctrl-C interrupt_count.fetch_add(1, Ordering::SeqCst); @@ -57,20 +23,18 @@ fn main() -> anyhow::Result<()> { if let Some(sql) = opts.sql { if sql.trim().starts_with('.') { - handle_dot_command(io.clone(), &conn, &sql)?; + app.handle_dot_command(&sql); } else { - query(io.clone(), &conn, &sql, &opts.output_mode, &interrupt_count)?; + app.query(&sql)?; } return Ok(()); } let mut rl = DefaultEditor::new()?; - let home = dirs::home_dir().unwrap(); + let home = dirs::home_dir().expect("Could not determine home directory"); let history_file = home.join(".limbo_history"); if history_file.exists() { rl.load_history(history_file.as_path())?; } - println!("Limbo v{}", env!("CARGO_PKG_VERSION")); - println!("Enter \".help\" for usage hints."); const PROMPT: &str = "limbo> "; let mut input_buff = String::new(); let mut prompt = PROMPT.to_string(); @@ -82,21 +46,18 @@ fn main() -> anyhow::Result<()> { if input_buff.is_empty() { if line.is_empty() { continue; - } else if line.starts_with('.') { - if let Err(e) = handle_dot_command(io.clone(), &conn, line) { - eprintln!("{}", e); - } + } + if line.starts_with('.') { + app.handle_dot_command(line); rl.add_history_entry(line.to_owned())?; - interrupt_count.store(0, Ordering::SeqCst); + app.reset_interrupt_count(); continue; } } if line.ends_with(';') { input_buff.push_str(line); input_buff.split(';').for_each(|stmt| { - if let Err(e) = - query(io.clone(), &conn, stmt, &opts.output_mode, &interrupt_count) - { + if let Err(e) = app.query(stmt) { eprintln!("{}", e); } }); @@ -113,13 +74,13 @@ fn main() -> anyhow::Result<()> { }; } rl.add_history_entry(line.to_owned())?; - interrupt_count.store(0, Ordering::SeqCst); + app.reset_interrupt_count(); } Err(ReadlineError::Interrupted) => { // At prompt, increment interrupt count - if interrupt_count.fetch_add(1, Ordering::SeqCst) >= 1 { + if app.incr_inturrupt_count() >= 1 { eprintln!("Interrupted. Exiting..."); - conn.close()?; + app.close(); break; } println!("Use .quit to exit or press Ctrl-C again to force quit."); @@ -127,11 +88,11 @@ fn main() -> anyhow::Result<()> { continue; } Err(ReadlineError::Eof) => { - conn.close()?; + app.close(); break; } Err(err) => { - conn.close()?; + app.close(); anyhow::bail!(err) } } @@ -147,236 +108,3 @@ fn calc_parens_offset(input: &str) -> i32 { _ => acc, }) } - -fn display_help_message() { - let help_message = r#" -Limbo SQL Shell Help -============== - -Welcome to the Limbo SQL Shell! You can execute any standard SQL command here. -In addition to standard SQL commands, the following special commands are available: - -Special Commands: ------------------ -.quit Stop interpreting input stream and exit. -.schema Show the schema of the specified table. -.opcodes Display all the opcodes defined by the virtual machine -.help Display this help message. - -Usage Examples: ---------------- -1. To quit the Limbo SQL Shell: - .quit - -2. To view the schema of a table named 'employees': - .schema employees - -3. To list all available SQL opcodes: - .opcodes - -Note: ------ -- All SQL commands must end with a semicolon (;). -- Special commands do not require a semicolon. - -"#; - - println!("{}", help_message); -} - -fn handle_dot_command( - io: Arc, - conn: &Rc, - line: &str, -) -> anyhow::Result<()> { - let args: Vec<&str> = line.split_whitespace().collect(); - - if args.is_empty() { - return Ok(()); - } - - match args[0] { - ".quit" => { - println!("Exiting Limbo SQL Shell."); - std::process::exit(0) - } - ".schema" => { - let table_name = args.get(1).copied(); - display_schema(io, conn, table_name)?; - } - ".opcodes" => { - if args.len() > 1 { - for op in &OPCODE_DESCRIPTIONS { - if op.name.eq_ignore_ascii_case(args.get(1).unwrap()) { - println!("{}", op); - } - } - } else { - for op in &OPCODE_DESCRIPTIONS { - println!("{}\n", op); - } - } - } - ".help" => { - display_help_message(); - } - _ => { - println!("Unknown command: {}", args[0]); - println!("Available commands:"); - println!(" .schema - Display the schema for a specific table"); - println!( - " .opcodes - Display all the opcodes defined by the virtual machine" - ); - } - } - - Ok(()) -} - -fn display_schema( - io: Arc, - conn: &Rc, - table: Option<&str>, -) -> anyhow::Result<()> { - let sql = match table { - Some(table_name) => format!( - "SELECT sql FROM sqlite_schema WHERE type IN ('table', 'index') AND tbl_name = '{}' AND name NOT LIKE 'sqlite_%'", - table_name - ), - None => String::from( - "SELECT sql FROM sqlite_schema WHERE type IN ('table', 'index') AND name NOT LIKE 'sqlite_%'" - ), - }; - - match conn.query(sql) { - Ok(Some(ref mut rows)) => { - let mut found = false; - loop { - match rows.next_row()? { - RowResult::Row(row) => { - if let Some(Value::Text(schema)) = row.values.first() { - println!("{};", schema); - found = true; - } - } - RowResult::IO => { - io.run_once()?; - } - RowResult::Done => break, - } - } - if !found { - if let Some(table_name) = table { - println!("Error: Table '{}' not found.", table_name); - } else { - println!("No tables or indexes found in the database."); - } - } - } - Ok(None) => { - println!("No results returned from the query."); - } - Err(err) => { - if err.to_string().contains("no such table: sqlite_schema") { - return Err(anyhow::anyhow!("Unable to access database schema. The database may be using an older SQLite version or may not be properly initialized.")); - } else { - return Err(anyhow::anyhow!("Error querying schema: {}", err)); - } - } - } - - Ok(()) -} - -fn query( - io: Arc, - conn: &Rc, - sql: &str, - output_mode: &OutputMode, - interrupt_count: &Arc, -) -> anyhow::Result<()> { - match conn.query(sql) { - Ok(Some(ref mut rows)) => match output_mode { - OutputMode::Raw => loop { - if interrupt_count.load(Ordering::SeqCst) > 0 { - println!("Query interrupted."); - return Ok(()); - } - - match rows.next_row() { - Ok(RowResult::Row(row)) => { - for (i, value) in row.values.iter().enumerate() { - if i > 0 { - print!("|"); - } - match value { - Value::Null => print!(""), - Value::Integer(i) => print!("{}", i), - Value::Float(f) => print!("{:?}", f), - Value::Text(s) => print!("{}", s), - Value::Blob(b) => { - print!("{}", String::from_utf8_lossy(b)) - } - } - } - println!(); - } - Ok(RowResult::IO) => { - io.run_once()?; - } - Ok(RowResult::Done) => { - break; - } - Err(err) => { - eprintln!("{}", err); - break; - } - } - }, - OutputMode::Pretty => { - if interrupt_count.load(Ordering::SeqCst) > 0 { - println!("Query interrupted."); - return Ok(()); - } - let mut table_rows: Vec> = vec![]; - loop { - match rows.next_row() { - Ok(RowResult::Row(row)) => { - table_rows.push( - row.values - .iter() - .map(|value| match value { - Value::Null => "".cell(), - Value::Integer(i) => i.to_string().cell(), - Value::Float(f) => f.to_string().cell(), - Value::Text(s) => s.cell(), - Value::Blob(b) => { - format!("{}", String::from_utf8_lossy(b)).cell() - } - }) - .collect(), - ); - } - Ok(RowResult::IO) => { - io.run_once()?; - } - Ok(RowResult::Done) => break, - Err(err) => { - eprintln!("{}", err); - break; - } - } - } - let table = table_rows.table(); - cli_table::print_stdout(table).unwrap(); - } - }, - Ok(None) => {} - Err(err) => { - eprintln!("{}", err); - } - } - // for now let's cache flush always - conn.cacheflush()?; - Ok(()) -} From 33c2d528f288e6b41faae691af594dfb192458c3 Mon Sep 17 00:00:00 2001 From: PThorpe92 Date: Thu, 12 Dec 2024 23:18:19 -0500 Subject: [PATCH 2/3] Refactor shell API for command extensibility --- cli/app.rs | 320 +++++++++++++++++++++++++++++++++------------------- cli/main.rs | 72 +++--------- 2 files changed, 221 insertions(+), 171 deletions(-) diff --git a/cli/app.rs b/cli/app.rs index 7a9ea51a4..67970ce54 100644 --- a/cli/app.rs +++ b/cli/app.rs @@ -1,10 +1,10 @@ use crate::opcodes_dictionary::OPCODE_DESCRIPTIONS; use cli_table::{Cell, Table}; -use limbo_core::{Database, RowResult, Value, IO}; +use limbo_core::{Database, RowResult, Value}; use clap::{Parser, ValueEnum}; use std::{ - io::{self, LineWriter, Write}, + io::{self, Write}, path::PathBuf, rc::Rc, str::FromStr, @@ -15,12 +15,17 @@ use std::{ }; #[derive(Parser)] +#[command(name = "limbo")] #[command(author, version, about, long_about = None)] pub struct Opts { + #[clap(index = 1)] pub database: Option, + #[clap(index = 2)] pub sql: Option, - #[clap(short, long, default_value_t = OutputMode::Raw)] + #[clap(short = 'm', long, default_value_t = OutputMode::Raw)] pub output_mode: OutputMode, + #[clap(short, long, default_value = "")] + pub output: String, } #[derive(ValueEnum, Copy, Clone, Debug, PartialEq, Eq)] @@ -60,6 +65,29 @@ pub enum Command { ShowInfo, } +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, + } + 1) // argv0 + } + + fn useage(&self) -> &str { + match self { + Self::Quit => ".quit", + Self::Open => ".open ", + Self::Help => ".help", + Self::Schema => ".schema ", + Self::Opcodes => ".opcodes", + Self::OutputMode => ".mode ", + Self::SetOutput => ".output ", + Self::Cwd => ".cd ", + Self::ShowInfo => ".show", + } + } +} + impl FromStr for Command { type Err = String; fn from_str(s: &str) -> Result { @@ -78,22 +106,23 @@ impl FromStr for Command { } } +const PROMPT: &str = "limbo> "; + pub struct Limbo { - io: Arc, + pub prompt: String, + io: Arc, writer: Box, conn: Option>, - filename: Option, + output_filename: String, db_file: Option, - pub interrupt_count: Arc, - pub output_mode: OutputMode, - pub is_stdout: bool, + output_mode: OutputMode, + is_stdout: bool, + input_buff: String, } impl Limbo { #[allow(clippy::arc_with_non_send_sync)] pub fn new(opts: &Opts) -> anyhow::Result { - println!("Limbo v{}", env!("CARGO_PKG_VERSION")); - println!("Enter \".help\" for usage hints."); let io = Arc::new(limbo_core::PlatformIO::new()?); let mut db_file = None; let conn = if let Some(path) = &opts.database { @@ -105,50 +134,59 @@ impl Limbo { println!("No database file specified: Use .open to open a database."); None }; + let writer: Box = if !opts.output.is_empty() { + Box::new(std::fs::File::create(&opts.output)?) + } else { + Box::new(io::stdout()) + }; Ok(Self { - io: Arc::new(limbo_core::PlatformIO::new()?), - writer: Box::new(LineWriter::new(std::io::stdout())), - filename: None, + prompt: PROMPT.to_string(), + io, + writer, + output_filename: opts.output.clone(), conn, db_file, - interrupt_count: AtomicUsize::new(0).into(), output_mode: opts.output_mode, is_stdout: true, + input_buff: String::new(), }) } - fn show_info(&mut self) { - self.writeln("------------------------------\nCurrent settings:"); - self.writeln(format!("Output mode: {}", self.output_mode)); - let output = self - .filename - .as_ref() - .unwrap_or(&"STDOUT".to_string()) - .clone(); - self.writeln(format!("Output mode: {output}")); + fn show_info(&mut self) -> std::io::Result<()> { + self.writeln("------------------------------\nCurrent settings:")?; + self.writeln(format!("Output mode: {}", self.output_mode))?; self.writeln(format!( "DB filename: {}", self.db_file.clone().unwrap_or(":none:".to_string()) - )); + ))?; + self.writeln(format!("Output: {}", self.output_filename))?; self.writeln(format!( "CWD: {}", std::env::current_dir().unwrap().display() - )); - let _ = self.writer.flush(); + ))?; + self.writer.flush() } - pub fn close(&mut self) { + pub fn reset_input(&mut self) { + self.prompt = PROMPT.to_string(); + self.input_buff.clear(); + } + + pub fn close_conn(&mut self) { self.conn.as_mut().map(|c| c.close()); } - pub fn open_db(&mut self, path: &str) -> anyhow::Result<()> { + fn open_db(&mut self, path: &str) -> anyhow::Result<()> { let db = Database::open_file(self.io.clone(), path)?; + if self.conn.is_some() { + self.conn.as_mut().unwrap().close()?; + } self.conn = Some(db.connect()); self.db_file = Some(path.to_string()); Ok(()) } - pub fn set_output_file(&mut self, path: &str) -> io::Result<()> { + 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; @@ -158,36 +196,74 @@ impl Limbo { } fn set_output_stdout(&mut self) { + let _ = self.writer.flush(); self.writer = Box::new(io::stdout()); self.is_stdout = true; } - pub fn set_mode(&mut self, mode: OutputMode) { + fn set_mode(&mut self, mode: OutputMode) { self.output_mode = mode; } - pub fn write>(&mut self, data: D) -> io::Result<()> { + fn write>(&mut self, data: D) -> io::Result<()> { self.writer.write_all(data.as_ref()) } - pub fn writeln>(&mut self, data: D) { - self.writer.write_all(data.as_ref()).unwrap(); - self.writer.write_all(b"\n").unwrap(); - let _ = self.writer.flush(); + fn writeln>(&mut self, data: D) -> io::Result<()> { + self.writer.write_all(data.as_ref())?; + self.writer.write_all(b"\n") } - pub fn reset_interrupt_count(&self) { - self.interrupt_count - .store(0, std::sync::atomic::Ordering::SeqCst); + fn is_first_input(&self) -> bool { + self.input_buff.is_empty() } - pub fn display_help_message(&mut self) { - let _ = self.writer.write_all(HELP_MSG.as_ref()); + fn buffer_input(&mut self, line: &str) { + self.input_buff.push_str(line); + self.input_buff.push(' '); } - pub fn incr_inturrupt_count(&mut self) -> usize { - self.interrupt_count - .fetch_add(1, std::sync::atomic::Ordering::SeqCst) + pub fn handle_input_line( + &mut self, + line: &str, + interrupt_count: &Arc, + rl: &mut rustyline::DefaultEditor, + ) -> anyhow::Result<()> { + if self.is_first_input() { + if line.is_empty() { + return Ok(()); + } + if line.starts_with('.') { + self.handle_dot_command(line); + rl.add_history_entry(line.to_owned())?; + interrupt_count.store(0, Ordering::SeqCst); + return Ok(()); + } + } + if line.ends_with(';') { + self.buffer_input(line); + let buff = self.input_buff.clone(); + buff.split(';') + .map(str::trim) + .filter(|s| !s.is_empty()) + .for_each(|stmt| { + if let Err(e) = self.query(stmt, interrupt_count) { + eprintln!("{}", e); + } + }); + 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("(.....> "), + }; + } + rl.add_history_entry(line.to_owned())?; + interrupt_count.store(0, Ordering::SeqCst); + Ok(()) } pub fn handle_dot_command(&mut self, line: &str) { @@ -195,94 +271,94 @@ impl Limbo { if args.is_empty() { return; } - - match Command::from_str(args[0]) { - Ok(Command::Quit) => { - println!("Exiting Limbo SQL Shell."); - self.close(); - std::process::exit(0) + if let Ok(ref cmd) = Command::from_str(args[0]) { + if args.len() < cmd.min_args() { + let _ = self.writeln(format!("Insufficient arguments: USAGE: {}", cmd.useage())); + return; } - Ok(Command::Open) => { - if args.len() < 2 { - println!("Error: No database file specified."); - } else if self.open_db(args[1]).is_err() { - println!("Error: Unable to open database file."); + match cmd { + Command::Quit => { + let _ = self.writeln("Exiting Limbo SQL Shell."); + self.close_conn(); + std::process::exit(0) } - } - Ok(Command::Schema) => { - if self.conn.is_none() { - println!("Error: no database currently open"); - return; + Command::Open => { + if self.open_db(args[1]).is_err() { + let _ = self.writeln("Error: Unable to open database file."); + } } - let table_name = args.get(1).copied(); - let _ = self.display_schema(table_name); - } - Ok(Command::Opcodes) => { - if args.len() > 1 { - for op in &OPCODE_DESCRIPTIONS { - if op.name.eq_ignore_ascii_case(args.get(1).unwrap()) { - self.writeln(format!("{}", op)); + Command::Schema => { + if self.conn.is_none() { + let _ = self.writeln("Error: no database currently open"); + return; + } + let table_name = args.get(1).copied(); + if let Err(e) = self.display_schema(table_name) { + let _ = self.writeln(format!("{}", e)); + } + } + Command::Opcodes => { + if args.len() > 1 { + for op in &OPCODE_DESCRIPTIONS { + if op.name.eq_ignore_ascii_case(args.get(1).unwrap().trim()) { + let _ = self.writeln(format!("{}", op)); + } + } + } else { + for op in &OPCODE_DESCRIPTIONS { + let _ = self.writeln(format!("{}\n", op)); } } - } else { - for op in &OPCODE_DESCRIPTIONS { - println!("{}\n", op); - } } - } - Ok(Command::OutputMode) => { - if args.len() < 2 { - println!("Error: No output mode specified."); - return; - } - match OutputMode::from_str(args[1], true) { + Command::OutputMode => match OutputMode::from_str(args[1], true) { Ok(mode) => { self.set_mode(mode); } Err(e) => { - println!("{e}"); + let _ = self.writeln(e); + } + }, + Command::SetOutput => { + if args.len() == 2 { + if let Err(e) = self.set_output_file(args[1]) { + let _ = self.writeln(format!("Error: {}", e)); + } + } else { + self.set_output_stdout(); } } - } - Ok(Command::SetOutput) => { - if args.len() == 2 { - if let Err(e) = self.set_output_file(args[1]) { - println!("Error: {}", e); + Command::Cwd => { + if args.len() < 2 { + println!("USAGE: .cd "); + return; } - } else { - self.set_output_stdout(); + let _ = std::env::set_current_dir(args[1]); + } + Command::ShowInfo => { + let _ = self.show_info(); + } + Command::Help => { + let _ = self.writeln(HELP_MSG); } } - Ok(Command::Cwd) => { - if args.len() < 2 { - println!("USAGE: .cd "); - return; - } - let _ = std::env::set_current_dir(args[1]); - } - Ok(Command::ShowInfo) => { - self.show_info(); - } - Ok(Command::Help) => { - self.display_help_message(); - } - _ => { - println!("Unknown command: {}", args[0]); - println!("enter: .help for all available commands"); - } + } else { + let _ = self.writeln(format!( + "Unknown command: {}\nenter: .help for all available commands", + args[0] + )); } } - pub fn query(&mut self, sql: &str) -> anyhow::Result<()> { + pub fn query(&mut self, sql: &str, interrupt_count: &Arc) -> anyhow::Result<()> { if self.conn.is_none() { - println!("Error: No database file specified."); + let _ = self.writeln("Error: No database file specified."); return Ok(()); } let conn = self.conn.as_ref().unwrap().clone(); match conn.query(sql) { Ok(Some(ref mut rows)) => match self.output_mode { OutputMode::Raw => loop { - if self.interrupt_count.load(Ordering::SeqCst) > 0 { + if interrupt_count.load(Ordering::SeqCst) > 0 { println!("Query interrupted."); return Ok(()); } @@ -291,7 +367,7 @@ impl Limbo { Ok(RowResult::Row(row)) => { for (i, value) in row.values.iter().enumerate() { if i > 0 { - let _ = self.write(b"|"); + let _ = self.writer.write(b"|"); } self.write( match value { @@ -306,7 +382,7 @@ impl Limbo { .as_bytes(), )?; } - self.writeln(""); + let _ = self.writeln(""); } Ok(RowResult::IO) => { self.io.run_once()?; @@ -321,7 +397,7 @@ impl Limbo { } }, OutputMode::Pretty => { - if self.interrupt_count.load(Ordering::SeqCst) > 0 { + if interrupt_count.load(Ordering::SeqCst) > 0 { println!("Query interrupted."); return Ok(()); } @@ -349,18 +425,21 @@ impl Limbo { } Ok(RowResult::Done) => break, Err(err) => { - eprintln!("{}", err); + let _ = self.writeln(format!("{}", err)); break; } } } - let table = table_rows.table(); - cli_table::print_stdout(table).unwrap(); + if let Ok(table) = table_rows.table().display() { + let _ = self.writeln(format!("{}", table)); + } else { + eprintln!("Error displaying table."); + } } }, Ok(None) => {} Err(err) => { - eprintln!("{}", err); + self.writeln(format!("{}", err)); } } // for now let's cache flush always @@ -386,7 +465,7 @@ impl Limbo { match rows.next_row()? { RowResult::Row(row) => { if let Some(Value::Text(schema)) = row.values.first() { - self.writeln(format!("{};", schema)); + let _ = self.writeln(format!("{};", schema)); found = true; } } @@ -398,9 +477,9 @@ impl Limbo { } if !found { if let Some(table_name) = table { - self.writeln(format!("Error: Table '{}' not found.", table_name)); + let _ = self.writeln(format!("Error: Table '{}' not found.", table_name)); } else { - self.writeln("No tables or indexes found in the database."); + let _ = self.writeln("No tables or indexes found in the database."); } } } @@ -430,6 +509,7 @@ In addition to standard SQL commands, the following special commands are availab Special Commands: ----------------- .quit Stop interpreting input stream and exit. +.show Display current settings. .open Open and connect to a database file. .output Change the output mode. Available modes are 'raw' and 'pretty'. .schema Show the schema of the specified table. @@ -469,3 +549,11 @@ Note: - 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, + }) +} diff --git a/cli/main.rs b/cli/main.rs index dab6d856c..d661450cd 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -11,9 +11,9 @@ fn main() -> anyhow::Result<()> { env_logger::init(); let opts = app::Opts::parse(); let mut app = app::Limbo::new(&opts)?; - + let interrupt_count = Arc::new(AtomicUsize::new(0)); { - let interrupt_count: Arc = app.interrupt_count.clone(); + let interrupt_count: Arc = Arc::clone(&interrupt_count); ctrlc::set_handler(move || { // Increment the interrupt count on Ctrl-C interrupt_count.fetch_add(1, Ordering::SeqCst); @@ -24,75 +24,45 @@ fn main() -> anyhow::Result<()> { if let Some(sql) = opts.sql { if sql.trim().starts_with('.') { app.handle_dot_command(&sql); - } else { - app.query(&sql)?; + } else if let Err(e) = app.query(&sql, &interrupt_count) { + eprintln!("{}", e); } return Ok(()); } + println!("Limbo v{}", env!("CARGO_PKG_VERSION")); + println!("Enter \".help\" for usage hints."); let mut rl = DefaultEditor::new()?; let home = dirs::home_dir().expect("Could not determine home directory"); let history_file = home.join(".limbo_history"); if history_file.exists() { rl.load_history(history_file.as_path())?; } - const PROMPT: &str = "limbo> "; - let mut input_buff = String::new(); - let mut prompt = PROMPT.to_string(); loop { - let readline = rl.readline(&prompt); + let readline = rl.readline(&app.prompt); match readline { - Ok(line) => { - let line = line.trim(); - if input_buff.is_empty() { - if line.is_empty() { - continue; - } - if line.starts_with('.') { - app.handle_dot_command(line); - rl.add_history_entry(line.to_owned())?; - app.reset_interrupt_count(); - continue; - } + Ok(line) => match app.handle_input_line(line.trim(), &interrupt_count, &mut rl) { + Ok(_) => {} + Err(e) => { + eprintln!("{}", e); } - if line.ends_with(';') { - input_buff.push_str(line); - input_buff.split(';').for_each(|stmt| { - if let Err(e) = app.query(stmt) { - eprintln!("{}", e); - } - }); - input_buff.clear(); - prompt = PROMPT.to_string(); - } else { - input_buff.push_str(line); - input_buff.push(' '); - prompt = match calc_parens_offset(&input_buff) { - n if n < 0 => String::from(")x!...>"), - 0 => String::from(" ...> "), - n if n < 10 => format!("(x{}...> ", n), - _ => String::from("(.....> "), - }; - } - rl.add_history_entry(line.to_owned())?; - app.reset_interrupt_count(); - } + }, Err(ReadlineError::Interrupted) => { // At prompt, increment interrupt count - if app.incr_inturrupt_count() >= 1 { + if interrupt_count.fetch_add(1, Ordering::SeqCst) >= 1 { eprintln!("Interrupted. Exiting..."); - app.close(); + app.close_conn(); break; } println!("Use .quit to exit or press Ctrl-C again to force quit."); - input_buff.clear(); + app.reset_input(); continue; } Err(ReadlineError::Eof) => { - app.close(); + app.close_conn(); break; } Err(err) => { - app.close(); + app.close_conn(); anyhow::bail!(err) } } @@ -100,11 +70,3 @@ fn main() -> anyhow::Result<()> { rl.save_history(history_file.as_path())?; Ok(()) } - -fn calc_parens_offset(input: &str) -> i32 { - input.chars().fold(0, |acc, c| match c { - '(' => acc + 1, - ')' => acc - 1, - _ => acc, - }) -} From 7a9fe8ac883078f3b608a7edebdf3441b3b41bbe Mon Sep 17 00:00:00 2001 From: PThorpe92 Date: Fri, 13 Dec 2024 11:44:39 -0500 Subject: [PATCH 3/3] 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, - }) -}