diff --git a/cli/app.rs b/cli/app.rs index f6cec868..a5a65e13 100644 --- a/cli/app.rs +++ b/cli/app.rs @@ -1,6 +1,6 @@ use crate::{ commands::{ - args::{EchoMode, TimerMode}, + args::{EchoMode, HeadersMode, TimerMode}, import::ImportFile, Command, CommandParser, }, @@ -676,6 +676,12 @@ impl Limbo { TimerMode::Off => false, }; } + Command::Headers(headers_mode) => { + self.opts.headers = match headers_mode.mode { + HeadersMode::On => true, + HeadersMode::Off => false, + }; + } }, } } @@ -688,62 +694,83 @@ impl Limbo { ) -> anyhow::Result<()> { match output { Ok(Some(ref mut rows)) => match self.opts.output_mode { - OutputMode::List => loop { - if self.interrupt_count.load(Ordering::SeqCst) > 0 { - println!("Query interrupted."); - return Ok(()); - } + OutputMode::List => { + let mut headers_printed = false; + loop { + if self.interrupt_count.load(Ordering::SeqCst) > 0 { + println!("Query interrupted."); + return Ok(()); + } - let start = Instant::now(); + let start = Instant::now(); - match rows.step() { - Ok(StepResult::Row) => { - if let Some(ref mut stats) = statistics { - stats.execute_time_elapsed_samples.push(start.elapsed()); - } - let row = rows.row().unwrap(); - for (i, value) in row.get_values().enumerate() { - if i > 0 { - let _ = self.writer.write(b"|"); + match rows.step() { + Ok(StepResult::Row) => { + if let Some(ref mut stats) = statistics { + stats.execute_time_elapsed_samples.push(start.elapsed()); } - if matches!(value, Value::Null) { - let _ = self.writer.write(self.opts.null_value.as_bytes())?; - } else { - let _ = self.writer.write(format!("{}", value).as_bytes())?; + + // Print headers if enabled and not already printed + if self.opts.headers && !headers_printed { + for i in 0..rows.num_columns() { + if i > 0 { + let _ = self.writer.write(b"|"); + } + let _ = + self.writer.write(rows.get_column_name(i).as_bytes()); + } + let _ = self.writeln(""); + headers_printed = true; + } + + let row = rows.row().unwrap(); + for (i, value) in row.get_values().enumerate() { + if i > 0 { + let _ = self.writer.write(b"|"); + } + if matches!(value, Value::Null) { + let _ = + self.writer.write(self.opts.null_value.as_bytes())?; + } else { + let _ = + self.writer.write(format!("{}", value).as_bytes())?; + } + } + let _ = self.writeln(""); + } + Ok(StepResult::IO) => { + let start = Instant::now(); + self.io.run_once()?; + if let Some(ref mut stats) = statistics { + stats.io_time_elapsed_samples.push(start.elapsed()); } } - let _ = self.writeln(""); - } - Ok(StepResult::IO) => { - let start = Instant::now(); - self.io.run_once()?; - if let Some(ref mut stats) = statistics { - stats.io_time_elapsed_samples.push(start.elapsed()); + Ok(StepResult::Interrupt) => break, + Ok(StepResult::Done) => { + if let Some(ref mut stats) = statistics { + stats.execute_time_elapsed_samples.push(start.elapsed()); + } + break; } - } - Ok(StepResult::Interrupt) => break, - Ok(StepResult::Done) => { - if let Some(ref mut stats) = statistics { - stats.execute_time_elapsed_samples.push(start.elapsed()); + Ok(StepResult::Busy) => { + if let Some(ref mut stats) = statistics { + stats.execute_time_elapsed_samples.push(start.elapsed()); + } + let _ = self.writeln("database is busy"); + break; } - break; - } - Ok(StepResult::Busy) => { - if let Some(ref mut stats) = statistics { - stats.execute_time_elapsed_samples.push(start.elapsed()); + Err(err) => { + if let Some(ref mut stats) = statistics { + stats.execute_time_elapsed_samples.push(start.elapsed()); + } + let report = + miette::Error::from(err).with_source_code(sql.to_owned()); + let _ = self.write_fmt(format_args!("{:?}", report)); + break; } - let _ = self.writeln("database is busy"); - break; - } - Err(err) => { - if let Some(ref mut stats) = statistics { - stats.execute_time_elapsed_samples.push(start.elapsed()); - } - let _ = self.writeln(err.to_string()); - break; } } - }, + } OutputMode::Pretty => { if self.interrupt_count.load(Ordering::SeqCst) > 0 { println!("Query interrupted."); diff --git a/cli/commands/args.rs b/cli/commands/args.rs index 4c36e6ef..2ee467fe 100644 --- a/cli/commands/args.rs +++ b/cli/commands/args.rs @@ -124,3 +124,14 @@ pub struct TimerArgs { #[arg(value_enum)] pub mode: TimerMode, } + +#[derive(Debug, Clone, Args)] +pub struct HeadersArgs { + pub mode: HeadersMode, +} + +#[derive(ValueEnum, Copy, Clone, Debug, PartialEq, Eq)] +pub enum HeadersMode { + On, + Off, +} diff --git a/cli/commands/mod.rs b/cli/commands/mod.rs index a4a9a8d4..86c4dd47 100644 --- a/cli/commands/mod.rs +++ b/cli/commands/mod.rs @@ -2,8 +2,8 @@ pub mod args; pub mod import; use args::{ - CwdArgs, EchoArgs, ExitArgs, IndexesArgs, LoadExtensionArgs, NullValueArgs, OpcodesArgs, - OpenArgs, OutputModeArgs, SchemaArgs, SetOutputArgs, TablesArgs, TimerArgs, + CwdArgs, EchoArgs, ExitArgs, HeadersArgs, IndexesArgs, LoadExtensionArgs, NullValueArgs, + OpcodesArgs, OpenArgs, OutputModeArgs, SchemaArgs, SetOutputArgs, TablesArgs, TimerArgs, }; use clap::Parser; use import::ImportArgs; @@ -77,6 +77,9 @@ pub enum Command { ListIndexes(IndexesArgs), #[command(name = "timer", display_name = ".timer")] Timer(TimerArgs), + /// Toggle column headers on/off in list mode + #[command(name = "headers", display_name = ".headers")] + Headers(HeadersArgs), } const _HELP_TEMPLATE: &str = "{before-help}{name} diff --git a/cli/input.rs b/cli/input.rs index 1ade1528..deb65975 100644 --- a/cli/input.rs +++ b/cli/input.rs @@ -83,6 +83,7 @@ pub struct Settings { pub io: Io, pub tracing_output: Option, pub timer: bool, + pub headers: bool, } impl From for Settings { @@ -107,6 +108,7 @@ impl From for Settings { }, tracing_output: opts.tracing_output, timer: false, + headers: false, } } } @@ -115,7 +117,7 @@ impl std::fmt::Display for Settings { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, - "Settings:\nOutput mode: {}\nDB: {}\nOutput: {}\nNull value: {}\nCWD: {}\nEcho: {}", + "Settings:\nOutput mode: {}\nDB: {}\nOutput: {}\nNull value: {}\nCWD: {}\nEcho: {}\nHeaders: {}", self.output_mode, self.db_file, match self.is_stdout { @@ -127,6 +129,10 @@ impl std::fmt::Display for Settings { match self.echo { true => "on", false => "off", + }, + match self.headers { + true => "on", + false => "off", } ) } @@ -221,6 +227,12 @@ pub const AFTER_HELP_MSG: &str = r#"Usage Examples: 14. To show names of indexes: .indexes ?TABLE? +15. To turn on column headers in list mode: + .headers on + +16. To turn off column headers in list mode: + .headers off + Note: - All SQL commands must end with a semicolon (;). - Special commands start with a dot (.) and are not required to end with a semicolon."#;