From 8a6667a82982cb880cb6a65ef6a9a9579f54f122 Mon Sep 17 00:00:00 2001 From: TcMits Date: Tue, 9 Sep 2025 16:23:08 +0700 Subject: [PATCH 1/7] refactor cli: will write to --- cli/app.rs | 77 ++++++++++++++++++++++------------------------------- cli/main.rs | 14 +++++++--- 2 files changed, 42 insertions(+), 49 deletions(-) diff --git a/cli/app.rs b/cli/app.rs index f72e2a6c6..54c282749 100644 --- a/cli/app.rs +++ b/cli/app.rs @@ -375,11 +375,6 @@ impl Limbo { self.writer.as_mut().unwrap().write_all(b"\n") } - fn buffer_input(&mut self, line: &str) { - self.input_buff.push_str(line); - self.input_buff.push(' '); - } - fn run_query(&mut self, input: &str) { let echo = self.opts.echo; if echo { @@ -481,34 +476,38 @@ impl Limbo { } } - fn reset_line(&mut self, _line: &str) -> rustyline::Result<()> { + fn reset_line(&mut self) -> rustyline::Result<()> { // Entry is auto added to history // self.rl.add_history_entry(line.to_owned())?; self.interrupt_count.store(0, Ordering::Release); Ok(()) } - pub fn handle_input_line(&mut self, line: &str) -> anyhow::Result<()> { - if self.input_buff.is_empty() { - if line.is_empty() { - return Ok(()); + // consume will consume `input_buff` + pub fn consume(&mut self) -> anyhow::Result<()> { + if self.input_buff.trim().is_empty() { + return Ok(()); + } + + self.reset_line()?; + + // SAFETY: we don't reset input after we handle the command + let value: &'static str = + unsafe { std::mem::transmute::<&str, &'static str>(self.input_buff.as_str()) }.trim(); + match (value.starts_with('.'), value.ends_with(';')) { + (true, _) => { + self.handle_dot_command(value.strip_prefix('.').unwrap()); + self.reset_input(); } - if let Some(command) = line.strip_prefix('.') { - self.handle_dot_command(command); - let _ = self.reset_line(line); - return Ok(()); + (false, true) => { + self.run_query(value); + self.reset_input(); + } + (false, false) => { + self.set_multiline_prompt(); } } - self.reset_line(line)?; - if line.ends_with(';') { - self.buffer_input(line); - let buff = self.input_buff.clone(); - self.run_query(buff.as_str()); - } else { - self.buffer_input(format!("{line}\n").as_str()); - self.set_multiline_prompt(); - } Ok(()) } @@ -1331,35 +1330,23 @@ impl Limbo { Ok(()) } - pub fn handle_remaining_input(&mut self) { - if self.input_buff.is_empty() { - return; - } + // readline will read inputs from rustyline or stdin + // and write it to input_buff. + pub fn readline(&mut self) -> Result<(), ReadlineError> { + use std::fmt::Write; - let buff = self.input_buff.clone(); - self.run_query(buff.as_str()); - self.reset_input(); - } - - pub fn readline(&mut self) -> Result { if let Some(rl) = &mut self.rl { - Ok(rl.readline(&self.prompt)?) + let result = rl.readline(&self.prompt)?; + let _ = self.input_buff.write_str(result.as_str()); } else { - let mut input = String::new(); let mut reader = std::io::stdin().lock(); - if reader.read_line(&mut input)? == 0 { + if reader.read_line(&mut self.input_buff)? == 0 { return Err(ReadlineError::Eof); } - // Remove trailing newline - if input.ends_with('\n') { - input.pop(); - if input.ends_with('\r') { - input.pop(); - } - } - - Ok(input) } + + let _ = self.input_buff.write_char(' '); + Ok(()) } pub fn dump_database_from_conn( diff --git a/cli/main.rs b/cli/main.rs index a2df75cba..d1ea807e3 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -63,9 +63,8 @@ fn main() -> anyhow::Result<()> { } loop { - let readline = app.readline(); - match readline { - Ok(line) => match app.handle_input_line(line.trim()) { + match app.readline() { + Ok(_) => match app.consume() { Ok(_) => {} Err(e) => { eprintln!("{e}"); @@ -83,7 +82,14 @@ fn main() -> anyhow::Result<()> { continue; } Err(ReadlineError::Eof) => { - app.handle_remaining_input(); + // consume remaining input before exit + match app.consume() { + Ok(_) => {} + Err(e) => { + eprintln!("{e}"); + } + }; + let _ = app.close_conn(); break; } From 048e72abf517fd9fb7d7ae66c1eb533d2897d797 Mon Sep 17 00:00:00 2001 From: TcMits Date: Tue, 9 Sep 2025 16:27:31 +0700 Subject: [PATCH 2/7] consume remaining --- cli/app.rs | 6 +++++- cli/main.rs | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/cli/app.rs b/cli/app.rs index 54c282749..92371f5b1 100644 --- a/cli/app.rs +++ b/cli/app.rs @@ -484,7 +484,7 @@ impl Limbo { } // consume will consume `input_buff` - pub fn consume(&mut self) -> anyhow::Result<()> { + pub fn consume(&mut self, flush: bool) -> anyhow::Result<()> { if self.input_buff.trim().is_empty() { return Ok(()); } @@ -503,6 +503,10 @@ impl Limbo { self.run_query(value); self.reset_input(); } + (false, false) if flush => { + self.run_query(value); + self.reset_input(); + } (false, false) => { self.set_multiline_prompt(); } diff --git a/cli/main.rs b/cli/main.rs index d1ea807e3..affb73888 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -64,7 +64,7 @@ fn main() -> anyhow::Result<()> { loop { match app.readline() { - Ok(_) => match app.consume() { + Ok(_) => match app.consume(false) { Ok(_) => {} Err(e) => { eprintln!("{e}"); @@ -83,7 +83,7 @@ fn main() -> anyhow::Result<()> { } Err(ReadlineError::Eof) => { // consume remaining input before exit - match app.consume() { + match app.consume(true) { Ok(_) => {} Err(e) => { eprintln!("{e}"); From dbcd01bf8bd0708b6b288fb1b91af82811d72cdc Mon Sep 17 00:00:00 2001 From: TcMits Date: Wed, 10 Sep 2025 15:56:20 +0700 Subject: [PATCH 3/7] make consume safer --- cli/app.rs | 58 +++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 47 insertions(+), 11 deletions(-) diff --git a/cli/app.rs b/cli/app.rs index 92371f5b1..d1dcefa70 100644 --- a/cli/app.rs +++ b/cli/app.rs @@ -19,6 +19,7 @@ use comfy_table::{Attribute, Cell, CellAlignment, ContentArrangement, Row, Table use rustyline::{error::ReadlineError, history::DefaultHistory, Editor}; use std::{ io::{self, BufRead as _, IsTerminal, Write}, + mem::{forget, ManuallyDrop}, path::PathBuf, sync::{ atomic::{AtomicUsize, Ordering}, @@ -82,7 +83,7 @@ pub struct Limbo { writer: Option>, conn: Arc, pub interrupt_count: Arc, - input_buff: String, + input_buff: ManuallyDrop, opts: Settings, pub rl: Option>, config: Option, @@ -157,7 +158,7 @@ impl Limbo { writer: Some(get_writer(&opts.output)), conn, interrupt_count, - input_buff: String::new(), + input_buff: ManuallyDrop::new(String::new()), opts: Settings::from(opts), rl: None, config: Some(config), @@ -431,8 +432,6 @@ impl Limbo { let _ = self.writeln(output); } } - - self.reset_input(); } fn print_query_performance_stats(&mut self, start: Instant, stats: QueryStatistics) { @@ -491,20 +490,54 @@ impl Limbo { self.reset_line()?; - // SAFETY: we don't reset input after we handle the command - let value: &'static str = - unsafe { std::mem::transmute::<&str, &'static str>(self.input_buff.as_str()) }.trim(); + // we are taking ownership of input_buff here + // its always safe because we split the string in two parts + fn take_usable_part(app: &mut Limbo) -> (String, usize) { + let ptr = app.input_buff.as_mut_ptr(); + let (len, cap) = (app.input_buff.len(), app.input_buff.capacity()); + app.input_buff = + ManuallyDrop::new(unsafe { String::from_raw_parts(ptr.add(len), 0, cap - len) }); + (unsafe { String::from_raw_parts(ptr, len, len) }, unsafe { + ptr.add(len).addr() + }) + } + + fn concat_usable_part(app: &mut Limbo, mut part: String, old_address: usize) { + let ptr = app.input_buff.as_mut_ptr(); + let (len, cap) = (app.input_buff.len(), app.input_buff.capacity()); + + // if the address is not the same, meaning the string has been reallocated + // so we just drop the part we took earlier + if ptr.addr() != old_address { + return; + } + + let head_ptr = part.as_mut_ptr(); + let (head_len, head_cap) = (part.len(), part.capacity()); + forget(part); // move this part into `input_buff` + app.input_buff = ManuallyDrop::new(unsafe { + String::from_raw_parts(head_ptr, head_len + len, head_cap + cap) + }); + } + + let value = self.input_buff.trim(); match (value.starts_with('.'), value.ends_with(';')) { (true, _) => { - self.handle_dot_command(value.strip_prefix('.').unwrap()); + let (owned_value, old_address) = take_usable_part(self); + self.handle_dot_command(owned_value.trim().strip_prefix('.').unwrap()); + concat_usable_part(self, owned_value, old_address); self.reset_input(); } (false, true) => { - self.run_query(value); + let (owned_value, old_address) = take_usable_part(self); + self.run_query(owned_value.trim()); + concat_usable_part(self, owned_value, old_address); self.reset_input(); } (false, false) if flush => { - self.run_query(value); + let (owned_value, old_address) = take_usable_part(self); + self.run_query(owned_value.trim()); + concat_usable_part(self, owned_value, old_address); self.reset_input(); } (false, false) => { @@ -1645,6 +1678,9 @@ fn sql_quote_string(s: &str) -> String { } impl Drop for Limbo { fn drop(&mut self) { - self.save_history() + self.save_history(); + unsafe { + ManuallyDrop::drop(&mut self.input_buff); + } } } From 65f5fbd1f64c1cca2dc64d71ccf83fc79358de73 Mon Sep 17 00:00:00 2001 From: TcMits Date: Wed, 10 Sep 2025 16:31:12 +0700 Subject: [PATCH 4/7] no errors in consume --- cli/app.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/cli/app.rs b/cli/app.rs index 298891f38..2f0df3d61 100644 --- a/cli/app.rs +++ b/cli/app.rs @@ -546,20 +546,19 @@ impl Limbo { } } - fn reset_line(&mut self) -> rustyline::Result<()> { + fn reset_line(&mut self) { // Entry is auto added to history // self.rl.add_history_entry(line.to_owned())?; self.interrupt_count.store(0, Ordering::Release); - Ok(()) } // consume will consume `input_buff` - pub fn consume(&mut self, flush: bool) -> anyhow::Result<()> { + pub fn consume(&mut self, flush: bool) { if self.input_buff.trim().is_empty() { - return Ok(()); + return; } - self.reset_line()?; + self.reset_line(); // we are taking ownership of input_buff here // its always safe because we split the string in two parts @@ -615,8 +614,6 @@ impl Limbo { self.set_multiline_prompt(); } } - - Ok(()) } pub fn handle_dot_command(&mut self, line: &str) { From 688dc6dde32d06d2032e7ef1abd56a0d8d2ba88c Mon Sep 17 00:00:00 2001 From: TcMits Date: Wed, 10 Sep 2025 16:31:57 +0700 Subject: [PATCH 5/7] minor --- cli/main.rs | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/cli/main.rs b/cli/main.rs index affb73888..de4c6c681 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -64,12 +64,7 @@ fn main() -> anyhow::Result<()> { loop { match app.readline() { - Ok(_) => match app.consume(false) { - Ok(_) => {} - Err(e) => { - eprintln!("{e}"); - } - }, + Ok(_) => app.consume(false), Err(ReadlineError::Interrupted) => { // At prompt, increment interrupt count if app.interrupt_count.fetch_add(1, Ordering::SeqCst) >= 1 { @@ -83,13 +78,7 @@ fn main() -> anyhow::Result<()> { } Err(ReadlineError::Eof) => { // consume remaining input before exit - match app.consume(true) { - Ok(_) => {} - Err(e) => { - eprintln!("{e}"); - } - }; - + app.consume(true); let _ = app.close_conn(); break; } From eeef8b85fa5fefa1dda780ab59b6f5d292ea1127 Mon Sep 17 00:00:00 2001 From: TcMits Date: Wed, 10 Sep 2025 16:54:51 +0700 Subject: [PATCH 6/7] always use consume instead of run_query, handle_dot_command --- cli/app.rs | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/cli/app.rs b/cli/app.rs index 2f0df3d61..bcaf6bbf1 100644 --- a/cli/app.rs +++ b/cli/app.rs @@ -150,7 +150,7 @@ macro_rules! row_step_result_query { impl Limbo { pub fn new() -> anyhow::Result<(Self, WorkerGuard)> { - let opts = Opts::parse(); + let mut opts = Opts::parse(); let guard = Self::init_tracing(&opts)?; let db_file = opts @@ -203,7 +203,8 @@ impl Limbo { }) .expect("Error setting Ctrl-C handler"); } - let sql = opts.sql.clone(); + let sql = opts.sql.take(); + let has_sql = sql.is_some(); let quiet = opts.quiet; let config = Config::for_output_mode(opts.output_mode); let mut app = Self { @@ -212,12 +213,12 @@ impl Limbo { writer: Some(get_writer(&opts.output)), conn, interrupt_count, - input_buff: ManuallyDrop::new(String::new()), + input_buff: ManuallyDrop::new(sql.unwrap_or_default()), opts: Settings::from(opts), rl: None, config: Some(config), }; - app.first_run(sql, quiet)?; + app.first_run(has_sql, quiet)?; Ok((app, guard)) } @@ -236,14 +237,14 @@ impl Limbo { self } - fn first_run(&mut self, sql: Option, quiet: bool) -> Result<(), LimboError> { + fn first_run(&mut self, has_sql: bool, quiet: bool) -> Result<(), LimboError> { // Skip startup messages and SQL execution in MCP mode if self.is_mcp_mode() { return Ok(()); } - if let Some(sql) = sql { - self.handle_first_input(&sql)?; + if has_sql { + self.handle_first_input()?; } if !quiet { self.writeln_fmt(format_args!("Turso v{}", env!("CARGO_PKG_VERSION")))?; @@ -256,12 +257,8 @@ impl Limbo { Ok(()) } - fn handle_first_input(&mut self, cmd: &str) -> Result<(), LimboError> { - if cmd.trim().starts_with('.') { - self.handle_dot_command(&cmd[1..]); - } else { - self.run_query(cmd); - } + fn handle_first_input(&mut self) -> Result<(), LimboError> { + self.consume(true); self.close_conn()?; std::process::exit(0); } From 5caf9a26401980054da460efef559a85de9b02ca Mon Sep 17 00:00:00 2001 From: TcMits Date: Thu, 11 Sep 2025 00:14:38 +0700 Subject: [PATCH 7/7] make it more safe + clippy --- cli/app.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/app.rs b/cli/app.rs index bcaf6bbf1..c6be2336b 100644 --- a/cli/app.rs +++ b/cli/app.rs @@ -575,7 +575,7 @@ impl Limbo { // if the address is not the same, meaning the string has been reallocated // so we just drop the part we took earlier - if ptr.addr() != old_address { + if ptr.addr() != old_address || !app.input_buff.is_empty() { return; }