Files
turso/cli/app.rs
Pere Diaz Bou 70c5cf3970 Merge 'Refactor Cli Repl Commands to use clap' from Pedro Muniz
This PR changes the argument parsing and matching to use Clap instead of
our own handrolled one. This makes it much easier to add new commands
and modify existing ones by using the full power of clap's derive
macros. It also produces nice error and help messages for us. However,
there is a bug in Clap that is not correctly modifying the `display
name` in the help section. So the command would appear as `show` instead
of `.show` in the help messages. This is very minimal, but if this is a
blocker for this PR we can just overwrite the help message in its
entirety. Also using Clap would enable us to use its autocomplete crate
to generate autocompletions for these special repl commands which would
be a huge win when compared to the `sqlite3` cli.
This is the current help message:
```sh

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:

Usage: <COMMAND>

Commands:
  exit       Exit this program with return-code CODE
  quit       Quit the shell
  open       Open a database file
  schema     Print this message or the help of the given subcommand(s) Display schema for a table
  output     Set output file (or stdout if empty)
  mode       Set output display mode
  opcodes    Show vdbe opcodes
  cd         Change the current working directory
  show       Display information about settings
  nullvalue  Set the value of NULL to be printed in 'list' mode
  echo       Toggle 'echo' mode to repeat commands before execution
  tables     Display tables
  import     Import data from FILE into TABLE
  load       Loads an extension library
  dump       Dump the current database as a list of SQL statements
  listvfs    List vfs modules available
  help       Print this message or the help of the given subcommand(s)

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 tables:
   .tables

5. To list all available SQL opcodes:
   .opcodes

6. To change the current output mode to 'pretty':
   .mode pretty

7. Send output to STDOUT if no file is specified:
   .output

8. To change the current working directory to '/tmp':
   .cd /tmp

9. Show the current values of settings:
   .show

10. To import csv file 'sample.csv' into 'csv_table' table:
   .import --csv sample.csv csv_table

11. To display the database contents as SQL:
   .dump

12. To load an extension library:
   .load /target/debug/liblimbo_regexp

13. To list all available VFS:
   .listvfs

Note:
- All SQL commands must end with a semicolon (;).
- Special commands start with a dot (.) and are not required to end with a semicolon.

```
If we need more information on a specific command, we can leverage CLAP
and do for instance:
```sh
.open -h
```

Reviewed-by: Pere Diaz Bou <pere-altea@homail.com>

Closes #1110
2025-04-01 11:40:14 +02:00

826 lines
31 KiB
Rust

use crate::{
commands::{args::EchoMode, import::ImportFile, Command, CommandParser},
helper::LimboHelper,
input::{get_io, get_writer, DbLocation, OutputMode, Settings},
opcodes_dictionary::OPCODE_DESCRIPTIONS,
};
use comfy_table::{Attribute, Cell, CellAlignment, Color, ContentArrangement, Row, Table};
use limbo_core::{Database, LimboError, OwnedValue, Statement, StepResult};
use clap::Parser;
use rustyline::{history::DefaultHistory, Editor};
use std::{
fmt,
io::{self, Write},
path::PathBuf,
rc::Rc,
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
},
};
#[derive(Parser)]
#[command(name = "limbo")]
#[command(author, version, about, long_about = None)]
pub struct Opts {
#[clap(index = 1, help = "SQLite database file", default_value = ":memory:")]
pub database: Option<PathBuf>,
#[clap(index = 2, help = "Optional SQL command to execute")]
pub sql: Option<String>,
#[clap(short = 'm', long, default_value_t = OutputMode::Pretty)]
pub output_mode: OutputMode,
#[clap(short, long, default_value = "")]
pub output: String,
#[clap(
short,
long,
help = "don't display program information on start",
default_value_t = false
)]
pub quiet: bool,
#[clap(short, long, help = "Print commands before execution")]
pub echo: bool,
#[clap(
short = 'v',
long,
help = "Select VFS. options are io_uring (if feature enabled), memory, and syscall"
)]
pub vfs: Option<String>,
#[clap(long, help = "Enable experimental MVCC feature")]
pub experimental_mvcc: bool,
}
const PROMPT: &str = "limbo> ";
pub struct Limbo<'a> {
pub prompt: String,
io: Arc<dyn limbo_core::IO>,
writer: Box<dyn Write>,
conn: Rc<limbo_core::Connection>,
pub interrupt_count: Arc<AtomicUsize>,
input_buff: String,
opts: Settings,
pub rl: &'a mut Editor<LimboHelper, DefaultHistory>,
}
macro_rules! query_internal {
($self:expr, $query:expr, $body:expr) => {{
let rows = $self.conn.query($query)?;
if let Some(mut rows) = rows {
loop {
match rows.step()? {
StepResult::Row => {
let row = rows.row().unwrap();
$body(row)?;
}
StepResult::IO => {
$self.io.run_once()?;
}
StepResult::Interrupt => break,
StepResult::Done => break,
StepResult::Busy => {
Err(LimboError::InternalError("database is busy".into()))?;
}
}
}
}
Ok::<(), LimboError>(())
}};
}
static COLORS: &[Color] = &[Color::Green, Color::Black, Color::Grey];
impl<'a> Limbo<'a> {
pub fn new(rl: &'a mut rustyline::Editor<LimboHelper, DefaultHistory>) -> anyhow::Result<Self> {
let opts = Opts::parse();
let db_file = opts
.database
.as_ref()
.map_or(":memory:".to_string(), |p| p.to_string_lossy().to_string());
let (io, db) = if let Some(ref vfs) = opts.vfs {
Database::open_new(&db_file, vfs)?
} else {
let io = {
match db_file.as_str() {
":memory:" => get_io(
DbLocation::Memory,
opts.vfs.as_ref().map_or("", |s| s.as_str()),
)?,
_path => get_io(
DbLocation::Path,
opts.vfs.as_ref().map_or("", |s| s.as_str()),
)?,
}
};
(
io.clone(),
Database::open_file(io.clone(), &db_file, opts.experimental_mvcc)?,
)
};
let conn = db.connect()?;
let h = LimboHelper::new(conn.clone(), io.clone());
rl.set_helper(Some(h));
let interrupt_count = Arc::new(AtomicUsize::new(0));
{
let interrupt_count: Arc<AtomicUsize> = Arc::clone(&interrupt_count);
ctrlc::set_handler(move || {
// Increment the interrupt count on Ctrl-C
interrupt_count.fetch_add(1, Ordering::SeqCst);
})
.expect("Error setting Ctrl-C handler");
}
let mut app = Self {
prompt: PROMPT.to_string(),
io,
writer: get_writer(&opts.output),
conn,
interrupt_count,
input_buff: String::new(),
opts: Settings::from(&opts),
rl,
};
if opts.sql.is_some() {
app.handle_first_input(opts.sql.as_ref().unwrap());
}
if !opts.quiet {
app.write_fmt(format_args!("Limbo v{}", env!("CARGO_PKG_VERSION")))?;
app.writeln("Enter \".help\" for usage hints.")?;
app.display_in_memory()?;
}
Ok(app)
}
fn handle_first_input(&mut self, cmd: &str) {
if cmd.trim().starts_with('.') {
self.handle_dot_command(&cmd[1..]);
} else {
self.run_query(cmd);
}
std::process::exit(0);
}
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("(.....> "),
};
}
#[cfg(not(target_family = "wasm"))]
fn handle_load_extension(&mut self, path: &str) -> Result<(), String> {
let ext_path = limbo_core::resolve_ext_path(path).map_err(|e| e.to_string())?;
self.conn
.load_extension(ext_path)
.map_err(|e| e.to_string())
}
fn dump_table(&mut self, name: &str) -> Result<(), LimboError> {
let query = format!("pragma table_info={}", name);
let mut cols = vec![];
let mut value_types = vec![];
query_internal!(
self,
query,
|row: &limbo_core::Row| -> Result<(), LimboError> {
let name: &str = row.get::<&str>(1)?;
cols.push(name.to_string());
let value_type: &str = row.get::<&str>(2)?;
value_types.push(value_type.to_string());
Ok(())
}
)?;
// FIXME: sqlite has logic to check rowid and optionally preserve
// it, but it requires pragma index_list, and it seems to be relevant
// only for indexes.
let cols_str = cols.join(", ");
let select = format!("select {} from {}", cols_str, name);
query_internal!(
self,
select,
|row: &limbo_core::Row| -> Result<(), LimboError> {
let values = row
.get_values()
.zip(value_types.iter())
.map(|(value, value_type)| {
// If the type affinity is TEXT, replace each single
// quotation mark with two single quotation marks, and
// wrap it with single quotation marks.
if value_type.contains("CHAR")
|| value_type.contains("CLOB")
|| value_type.contains("TEXT")
{
format!("'{}'", value.to_string().replace("'", "''"))
} else if value_type.contains("BLOB") {
let blob = value.to_blob().unwrap_or(&[]);
let hex_string: String =
blob.iter().fold(String::new(), |mut output, b| {
let _ =
fmt::Write::write_fmt(&mut output, format_args!("{b:02x}"));
output
});
format!("X'{}'", hex_string)
} else {
value.to_string()
}
})
.collect::<Vec<_>>()
.join(",");
self.write_fmt(format_args!("INSERT INTO {} VALUES({});", name, values))?;
Ok(())
}
)?;
Ok(())
}
fn dump_database(&mut self) -> anyhow::Result<()> {
self.writeln("PRAGMA foreign_keys=OFF;")?;
self.writeln("BEGIN TRANSACTION;")?;
// FIXME: At this point, SQLite executes the following:
// sqlite3_exec(p->db, "SAVEPOINT dump; PRAGMA writable_schema=ON", 0, 0, 0);
// we don't have those yet, so don't.
let query = r#"
SELECT name, type, sql
FROM sqlite_schema AS o
WHERE type == 'table'
AND sql NOT NULL
ORDER BY tbl_name = 'sqlite_sequence', rowid"#;
let res = query_internal!(
self,
query,
|row: &limbo_core::Row| -> Result<(), LimboError> {
let sql: &str = row.get::<&str>(2)?;
let name: &str = row.get::<&str>(0)?;
self.write_fmt(format_args!("{};", sql))?;
self.dump_table(name)
}
);
match res {
Ok(_) => Ok(()),
Err(LimboError::Corrupt(x)) => {
// FIXME: SQLite at this point retry the query with a different
// order by, but for simplicity we are just ignoring for now
self.writeln("/****** CORRUPTION ERROR *******/")?;
Err(LimboError::Corrupt(x))
}
Err(x) => Err(x),
}?;
self.conn.close()?;
self.writeln("COMMIT;")?;
Ok(())
}
fn display_in_memory(&mut self) -> io::Result<()> {
if self.opts.db_file == ":memory:" {
self.writeln("Connected to a transient in-memory database.")?;
self.writeln("Use \".open FILENAME\" to reopen on a persistent database")?;
}
Ok(())
}
fn show_info(&mut self) -> io::Result<()> {
let opts = format!("{}", self.opts);
self.writeln(opts)
}
pub fn reset_input(&mut self) {
self.prompt = PROMPT.to_string();
self.input_buff.clear();
}
pub fn close_conn(&mut self) -> Result<(), LimboError> {
self.conn.close()
}
fn toggle_echo(&mut self, arg: EchoMode) {
match arg {
EchoMode::On => self.opts.echo = true,
EchoMode::Off => self.opts.echo = false,
}
}
fn open_db(&mut self, path: &str, vfs_name: Option<&str>) -> anyhow::Result<()> {
self.conn.close()?;
let (io, db) = if let Some(vfs_name) = vfs_name {
self.conn.open_new(path, vfs_name)?
} else {
let io = {
match path {
":memory:" => get_io(DbLocation::Memory, &self.opts.io.to_string())?,
_path => get_io(DbLocation::Path, &self.opts.io.to_string())?,
}
};
(io.clone(), Database::open_file(io.clone(), path, false)?)
};
self.io = io;
self.conn = db.connect()?;
self.opts.db_file = path.to_string();
Ok(())
}
fn set_output_file(&mut self, path: &str) -> Result<(), String> {
if path.is_empty() || path.trim().eq_ignore_ascii_case("stdout") {
self.set_output_stdout();
return Ok(());
}
match std::fs::File::create(path) {
Ok(file) => {
self.writer = Box::new(file);
self.opts.is_stdout = false;
self.opts.output_mode = OutputMode::List;
self.opts.output_filename = path.to_string();
Ok(())
}
Err(e) => Err(e.to_string()),
}
}
fn set_output_stdout(&mut self) {
let _ = self.writer.flush();
self.writer = Box::new(io::stdout());
self.opts.is_stdout = true;
}
fn set_mode(&mut self, mode: OutputMode) -> Result<(), String> {
if mode == OutputMode::Pretty && !self.opts.is_stdout {
Err("pretty output can only be written to a tty".to_string())
} else {
self.opts.output_mode = mode;
Ok(())
}
}
fn write_fmt(&mut self, fmt: std::fmt::Arguments) -> io::Result<()> {
let _ = self.writer.write_fmt(fmt);
self.writer.write_all(b"\n")
}
fn writeln<D: AsRef<[u8]>>(&mut self, data: D) -> io::Result<()> {
self.writer.write_all(data.as_ref())?;
self.writer.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 {
let _ = self.writeln(input);
}
if input.trim_start().starts_with("explain") {
if let Ok(Some(stmt)) = self.conn.query(input) {
let _ = self.writeln(stmt.explain().as_bytes());
}
} else {
let conn = self.conn.clone();
let runner = conn.query_runner(input.as_bytes());
for output in runner {
if self.print_query_result(input, output).is_err() {
break;
}
}
}
self.reset_input();
}
fn reset_line(&mut self, line: &str) -> rustyline::Result<()> {
self.rl.add_history_entry(line.to_owned())?;
self.interrupt_count.store(0, Ordering::SeqCst);
Ok(())
}
pub fn handle_input_line(&mut self, line: &str) -> anyhow::Result<()> {
if self.input_buff.is_empty() {
if line.is_empty() {
return Ok(());
}
if line.starts_with('.') {
self.handle_dot_command(&line[1..]);
let _ = self.reset_line(line);
return Ok(());
}
}
if line.trim_start().starts_with("--") {
if let Some(remaining) = line.split_once('\n') {
let after_comment = remaining.1.trim();
if !after_comment.is_empty() {
if after_comment.ends_with(';') {
self.run_query(after_comment);
if self.opts.echo {
let _ = self.writeln(after_comment);
}
let conn = self.conn.clone();
let runner = conn.query_runner(after_comment.as_bytes());
for output in runner {
if let Err(e) = self.print_query_result(after_comment, output) {
let _ = self.writeln(e.to_string());
}
}
self.reset_input();
return self.handle_input_line(after_comment);
} else {
self.set_multiline_prompt();
let _ = self.reset_line(line);
return Ok(());
}
}
}
return Ok(());
}
if let Some(comment_pos) = line.find("--") {
let before_comment = line[..comment_pos].trim();
if !before_comment.is_empty() {
return self.handle_input_line(before_comment);
}
}
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!("{}\n", line).as_str());
self.set_multiline_prompt();
}
self.reset_line(line)?;
Ok(())
}
pub fn handle_dot_command(&mut self, line: &str) {
let args: Vec<&str> = line.split_whitespace().collect();
if args.is_empty() {
return;
}
match CommandParser::try_parse_from(args) {
Err(err) => {
let _ = self.write_fmt(format_args!("{err}"));
}
Ok(cmd) => match cmd.command {
Command::Exit(args) => {
std::process::exit(args.code);
}
Command::Quit => {
let _ = self.writeln("Exiting Limbo SQL Shell.");
let _ = self.close_conn();
std::process::exit(0)
}
Command::Open(args) => {
if self.open_db(&args.path, args.vfs_name.as_deref()).is_err() {
let _ = self.writeln("Error: Unable to open database file.");
}
}
Command::Schema(args) => {
if let Err(e) = self.display_schema(args.table_name.as_deref()) {
let _ = self.writeln(e.to_string());
}
}
Command::Tables(args) => {
if let Err(e) = self.display_tables(args.pattern.as_deref()) {
let _ = self.writeln(e.to_string());
}
}
Command::Opcodes(args) => {
if let Some(opcode) = args.opcode {
for op in &OPCODE_DESCRIPTIONS {
if op.name.eq_ignore_ascii_case(opcode.trim()) {
let _ = self.write_fmt(format_args!("{}", op));
}
}
} else {
for op in &OPCODE_DESCRIPTIONS {
let _ = self.write_fmt(format_args!("{}\n", op));
}
}
}
Command::NullValue(args) => {
self.opts.null_value = args.value;
}
Command::OutputMode(args) => {
if let Err(e) = self.set_mode(args.mode) {
let _ = self.write_fmt(format_args!("Error: {}", e));
}
}
Command::SetOutput(args) => {
if let Some(path) = args.path {
if let Err(e) = self.set_output_file(&path) {
let _ = self.write_fmt(format_args!("Error: {}", e));
}
} else {
self.set_output_stdout();
}
}
Command::Echo(args) => {
self.toggle_echo(args.mode);
}
Command::Cwd(args) => {
let _ = std::env::set_current_dir(args.directory);
}
Command::ShowInfo => {
let _ = self.show_info();
}
Command::Import(args) => {
let mut import_file =
ImportFile::new(self.conn.clone(), self.io.clone(), &mut self.writer);
import_file.import(args)
}
Command::LoadExtension(args) => {
#[cfg(not(target_family = "wasm"))]
if let Err(e) = self.handle_load_extension(&args.path) {
let _ = self.writeln(&e);
}
}
Command::Dump => {
if let Err(e) = self.dump_database() {
let _ = self.write_fmt(format_args!("/****** ERROR: {} ******/", e));
}
}
Command::ListVfs => {
let _ = self.writeln("Available VFS modules:");
self.conn.list_vfs().iter().for_each(|v| {
let _ = self.writeln(v);
});
}
},
}
}
fn print_query_result(
&mut self,
sql: &str,
mut output: Result<Option<Statement>, LimboError>,
) -> 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(());
}
match rows.step() {
Ok(StepResult::Row) => {
let row = rows.row().unwrap();
for (i, value) in row.get_values().enumerate() {
if i > 0 {
let _ = self.writer.write(b"|");
}
if matches!(value, OwnedValue::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) => {
self.io.run_once()?;
}
Ok(StepResult::Interrupt) => break,
Ok(StepResult::Done) => {
break;
}
Ok(StepResult::Busy) => {
let _ = self.writeln("database is busy");
break;
}
Err(err) => {
let _ = self.writeln(err.to_string());
break;
}
}
},
OutputMode::Pretty => {
if self.interrupt_count.load(Ordering::SeqCst) > 0 {
println!("Query interrupted.");
return Ok(());
}
let mut table = Table::new();
table
.set_content_arrangement(ContentArrangement::Dynamic)
.set_truncation_indicator("")
.apply_modifier("││──├─┼┤│─┼├┤┬┴┌┐└┘");
if rows.num_columns() > 0 {
let header = (0..rows.num_columns())
.map(|i| {
let name = rows.get_column_name(i);
Cell::new(name)
.add_attribute(Attribute::Bold)
.fg(Color::White)
})
.collect::<Vec<_>>();
table.set_header(header);
}
loop {
match rows.step() {
Ok(StepResult::Row) => {
let record = rows.row().unwrap();
let mut row = Row::new();
row.max_height(1);
for (idx, value) in record.get_values().enumerate() {
let (content, alignment) = match value {
OwnedValue::Null => {
(self.opts.null_value.clone(), CellAlignment::Left)
}
OwnedValue::Integer(_) => {
(format!("{}", value), CellAlignment::Right)
}
OwnedValue::Float(_) => {
(format!("{}", value), CellAlignment::Right)
}
OwnedValue::Text(_) => {
(format!("{}", value), CellAlignment::Left)
}
OwnedValue::Blob(_) => {
(format!("{}", value), CellAlignment::Left)
}
};
row.add_cell(
Cell::new(content)
.set_alignment(alignment)
.fg(COLORS[idx % COLORS.len()]),
);
}
table.add_row(row);
}
Ok(StepResult::IO) => {
self.io.run_once()?;
}
Ok(StepResult::Interrupt) => break,
Ok(StepResult::Done) => break,
Ok(StepResult::Busy) => {
let _ = self.writeln("database is busy");
break;
}
Err(err) => {
let _ = self.write_fmt(format_args!(
"{:?}",
miette::Error::from(err).with_source_code(sql.to_owned())
));
break;
}
}
}
if table.header().is_some() {
let _ = self.write_fmt(format_args!("{}", table));
}
}
},
Ok(None) => {}
Err(err) => {
let _ = self.write_fmt(format_args!(
"{:?}",
miette::Error::from(err).with_source_code(sql.to_owned())
));
anyhow::bail!("We have to throw here, even if we printed error");
}
}
// for now let's cache flush always
self.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.query(&sql) {
Ok(Some(ref mut rows)) => {
let mut found = false;
loop {
match rows.step()? {
StepResult::Row => {
let row = rows.row().unwrap();
if let Ok(OwnedValue::Text(schema)) = row.get::<&OwnedValue>(0) {
let _ = self.write_fmt(format_args!("{};", schema.as_str()));
found = true;
}
}
StepResult::IO => {
self.io.run_once()?;
}
StepResult::Interrupt => break,
StepResult::Done => break,
StepResult::Busy => {
let _ = self.writeln("database is busy");
break;
}
}
}
if !found {
if let Some(table_name) = table {
let _ = self
.write_fmt(format_args!("-- Error: Table '{}' not found.", table_name));
} else {
let _ = self.writeln("-- No tables or indexes found in the database.");
}
}
}
Ok(None) => {
let _ = self.writeln("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 display_tables(&mut self, pattern: Option<&str>) -> anyhow::Result<()> {
let sql = match pattern {
Some(pattern) => format!(
"SELECT name FROM sqlite_schema WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name LIKE '{}' ORDER BY 1",
pattern
),
None => String::from(
"SELECT name FROM sqlite_schema WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY 1"
),
};
match self.conn.query(&sql) {
Ok(Some(ref mut rows)) => {
let mut tables = String::new();
loop {
match rows.step()? {
StepResult::Row => {
let row = rows.row().unwrap();
if let Ok(OwnedValue::Text(table)) = row.get::<&OwnedValue>(0) {
tables.push_str(table.as_str());
tables.push(' ');
}
}
StepResult::IO => {
self.io.run_once()?;
}
StepResult::Interrupt => break,
StepResult::Done => break,
StepResult::Busy => {
let _ = self.writeln("database is busy");
break;
}
}
}
if !tables.is_empty() {
let _ = self.writeln(tables.trim_end());
} else if let Some(pattern) = pattern {
let _ = self.write_fmt(format_args!(
"Error: Tables with pattern '{}' not found.",
pattern
));
} else {
let _ = self.writeln("No tables found in the database.");
}
}
Ok(None) => {
let _ = self.writeln("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(())
}
pub fn handle_remaining_input(&mut self) {
if self.input_buff.is_empty() {
return;
}
let buff = self.input_buff.clone();
self.run_query(buff.as_str());
self.reset_input();
}
}