Merge 'Add several cli shell commands, re-structure shell api' from Preston Thorpe

After fixing the shell issue earlier I just had to revisit this. Let me
know if this is too opinionated of changes or out of scope.
This PR adds several of the sqlite CLI commands to the limbo shell user
interface, with their behavior matched as much as possible.
- `.show`:  displays a list of currently enabled settings
- `.open <file>` : opens a database file
- `.output <file>`: allows the user to direct shell output to a file, or
stdout if left blank
- `.cd <dir>`: changes the current working directory of the shell
environment
- `.mode <output_mode>`: allows the user to select between the (two)
current output modes "pretty | raw"
- `.nullvalue <string>`: allows user to set the value of NULL to be
displayed in the output
It also prevents a database file argument from needing to being passed
as an argv[1], but alerts the user that no database was selected, while
allowing them to `.open` a database file with a dot command.
![image](https://github.com/user-
attachments/assets/8249e28d-e545-4c49-ab30-909cf1e42563)
![image](https://github.com/user-
attachments/assets/cfda121b-c789-48cc-a35e-259d8b5a3a04)
This PR also restructures the CLI crate a bit, to make future commands a
bit easier to add.

Reviewed-by: Pekka Enberg <pere-altea@hotmail.com>

Closes #462
This commit is contained in:
jussisaurio
2024-12-14 15:45:48 +02:00
2 changed files with 602 additions and 331 deletions

581
cli/app.rs Normal file
View File

@@ -0,0 +1,581 @@
use crate::opcodes_dictionary::OPCODE_DESCRIPTIONS;
use cli_table::{Cell, Table};
use limbo_core::{Database, RowResult, Value};
use clap::{Parser, ValueEnum};
use std::{
io::{self, Write},
path::PathBuf,
rc::Rc,
str::FromStr,
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
},
};
#[derive(Parser)]
#[command(name = "limbo")]
#[command(author, version, about, long_about = None)]
pub struct Opts {
#[clap(index = 1)]
pub database: Option<PathBuf>,
#[clap(index = 2)]
pub sql: Option<String>,
#[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)]
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 (or stdout if empty)
SetOutput,
/// Set output display mode
OutputMode,
/// Show vdbe opcodes
Opcodes,
/// Change the current working directory
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::Schema
| Self::Help
| Self::Opcodes
| Self::ShowInfo
| Self::SetOutput => 0,
Self::Open | Self::OutputMode | Self::Cwd | Self::NullValue => 1,
} + 1) // argv0
}
fn useage(&self) -> &str {
match self {
Self::Quit => ".quit",
Self::Open => ".open <file>",
Self::Help => ".help",
Self::Schema => ".schema ?<table>?",
Self::Opcodes => ".opcodes",
Self::OutputMode => ".mode <mode>",
Self::SetOutput => ".output ?file?",
Self::Cwd => ".cd <directory>",
Self::ShowInfo => ".show",
Self::NullValue => ".nullvalue <string>",
}
}
}
impl FromStr for Command {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
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),
".nullvalue" => Ok(Self::NullValue),
_ => Err("Unknown command".to_string()),
}
}
}
const PROMPT: &str = "limbo> ";
pub struct Limbo {
pub prompt: String,
io: Arc<dyn limbo_core::IO>,
writer: Box<dyn Write>,
conn: Option<Rc<limbo_core::Connection>>,
output_filename: String,
db_file: Option<String>,
output_mode: OutputMode,
is_stdout: bool,
input_buff: String,
null_value: String,
}
impl Limbo {
#[allow(clippy::arc_with_non_send_sync)]
pub fn new(opts: &Opts) -> anyhow::Result<Self> {
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 <file> to open a database.");
None
};
let writer: Box<dyn Write> = if !opts.output.is_empty() {
Box::new(std::fs::File::create(&opts.output)?)
} 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: 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))?;
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 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()
}
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());
}
fn open_db(&mut self, path: &str) -> anyhow::Result<()> {
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) -> 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());
}
}
}
fn set_output_stdout(&mut self) {
let _ = self.writer.flush();
self.writer = Box::new(io::stdout());
self.is_stdout = true;
}
fn set_mode(&mut self, mode: OutputMode) {
self.output_mode = mode;
}
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(' ');
}
pub fn handle_input_line(
&mut self,
line: &str,
interrupt_count: &Arc<AtomicUsize>,
rl: &mut rustyline::DefaultEditor,
) -> anyhow::Result<()> {
if self.input_buff.is_empty() {
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) {
let _ = self.writeln(e.to_string());
}
});
self.reset_input();
} else {
self.buffer_input(line);
self.set_multiline_prompt();
}
rl.add_history_entry(line.to_owned())?;
interrupt_count.store(0, Ordering::SeqCst);
Ok(())
}
pub fn handle_dot_command(&mut self, line: &str) {
let args: Vec<&str> = line.split_whitespace().collect();
if args.is_empty() {
return;
}
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;
}
match cmd {
Command::Quit => {
let _ = self.writeln("Exiting Limbo SQL Shell.");
self.close_conn();
std::process::exit(0)
}
Command::Open => {
if self.open_db(args[1]).is_err() {
let _ = self.writeln("Error: Unable to open database file.");
}
}
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(e.to_string());
}
}
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));
}
}
}
Command::NullValue => {
self.null_value = args[1].to_string();
}
Command::OutputMode => match OutputMode::from_str(args[1], true) {
Ok(mode) => {
self.set_mode(mode);
}
Err(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();
}
}
Command::Cwd => {
let _ = std::env::set_current_dir(args[1]);
}
Command::ShowInfo => {
let _ = self.show_info();
}
Command::Help => {
let _ = self.writeln(HELP_MSG);
}
}
} else {
let _ = self.writeln(format!(
"Unknown command: {}\nenter: .help for all available commands",
args[0]
));
}
}
pub fn query(&mut self, sql: &str, interrupt_count: &Arc<AtomicUsize>) -> anyhow::Result<()> {
if self.conn.is_none() {
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 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.writer.write(b"|");
}
let _ = self.writer.write(
match value {
Value::Null => self.null_value.clone(),
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(),
)?;
}
let _ = self.writeln("");
}
Ok(RowResult::IO) => {
self.io.run_once()?;
}
Ok(RowResult::Done) => {
break;
}
Err(err) => {
let _ = self.writeln(err.to_string());
break;
}
}
},
OutputMode::Pretty => {
if interrupt_count.load(Ordering::SeqCst) > 0 {
println!("Query interrupted.");
return Ok(());
}
let mut table_rows: Vec<Vec<_>> = vec![];
loop {
match rows.next_row() {
Ok(RowResult::Row(row)) => {
table_rows.push(
row.values
.iter()
.map(|value| match value {
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(),
Value::Blob(b) => {
format!("{}", String::from_utf8_lossy(b)).cell()
}
})
.collect(),
);
}
Ok(RowResult::IO) => {
self.io.run_once()?;
}
Ok(RowResult::Done) => break,
Err(err) => {
let _ = self.writeln(format!("{}", err));
break;
}
}
}
if let Ok(table) = table_rows.table().display() {
let _ = self.writeln(format!("{}", table));
} else {
let _ = self.writeln("Error displaying table.");
}
}
},
Ok(None) => {}
Err(err) => {
let _ = self.writeln(format!("{}", 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() {
let _ = self.writeln(format!("{};", schema));
found = true;
}
}
RowResult::IO => {
self.io.run_once()?;
}
RowResult::Done => break,
}
}
if !found {
if let Some(table_name) = table {
let _ = self.writeln(format!("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(())
}
}
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.
.show Display current settings.
.open <database_file> Open and connect to a database file.
.output <mode> Change the output mode. Available modes are 'raw' and 'pretty'.
.schema <table_name> Show the schema of the specified table.
.opcodes Display all the opcodes defined by the virtual machine
.cd <directory> Change the current working directory.
.nullvalue <string> Set the value to be displayed for null values.
.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
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.
"#;

View File

@@ -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<String>,
#[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 opts = app::Opts::parse();
let mut app = app::Limbo::new(&opts)?;
let interrupt_count = Arc::new(AtomicUsize::new(0));
{
let interrupt_count = Arc::clone(&interrupt_count);
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);
@@ -57,81 +23,46 @@ fn main() -> anyhow::Result<()> {
if let Some(sql) = opts.sql {
if sql.trim().starts_with('.') {
handle_dot_command(io.clone(), &conn, &sql)?;
} else {
query(io.clone(), &conn, &sql, &opts.output_mode, &interrupt_count)?;
app.handle_dot_command(&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().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();
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;
} else if line.starts_with('.') {
if let Err(e) = handle_dot_command(io.clone(), &conn, line) {
eprintln!("{}", e);
}
rl.add_history_entry(line.to_owned())?;
interrupt_count.store(0, Ordering::SeqCst);
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) =
query(io.clone(), &conn, stmt, &opts.output_mode, &interrupt_count)
{
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())?;
interrupt_count.store(0, Ordering::SeqCst);
}
},
Err(ReadlineError::Interrupted) => {
// At prompt, increment interrupt count
if interrupt_count.fetch_add(1, Ordering::SeqCst) >= 1 {
eprintln!("Interrupted. Exiting...");
conn.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) => {
conn.close()?;
app.close_conn();
break;
}
Err(err) => {
conn.close()?;
app.close_conn();
anyhow::bail!(err)
}
}
@@ -139,244 +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,
})
}
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 <table_name> 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<dyn limbo_core::IO>,
conn: &Rc<limbo_core::Connection>,
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 <table_name> - Display the schema for a specific table");
println!(
" .opcodes - Display all the opcodes defined by the virtual machine"
);
}
}
Ok(())
}
fn display_schema(
io: Arc<dyn limbo_core::IO>,
conn: &Rc<limbo_core::Connection>,
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<dyn limbo_core::IO>,
conn: &Rc<limbo_core::Connection>,
sql: &str,
output_mode: &OutputMode,
interrupt_count: &Arc<AtomicUsize>,
) -> 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<_>> = 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(())
}