0
0
mirror of https://github.com/tursodatabase/libsql.git synced 2025-01-22 07:58:56 +00:00
2023-12-12 15:12:31 +05:30

1027 lines
37 KiB
Rust

use anyhow::Result;
use base64::{engine::general_purpose, Engine as _};
use clap::Parser;
use once_cell::sync::Lazy;
use rusqlite::ffi::{
sqlite3_changes64, sqlite3_db_config, sqlite3_total_changes64, SQLITE_DBCONFIG_DEFENSIVE,
SQLITE_DBCONFIG_DQS_DDL, SQLITE_DBCONFIG_DQS_DML, SQLITE_DBCONFIG_ENABLE_FKEY,
SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER, SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION,
SQLITE_DBCONFIG_ENABLE_QPSG, SQLITE_DBCONFIG_ENABLE_TRIGGER, SQLITE_DBCONFIG_ENABLE_VIEW,
SQLITE_DBCONFIG_LEGACY_ALTER_TABLE, SQLITE_DBCONFIG_LEGACY_FILE_FORMAT,
SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE, SQLITE_DBCONFIG_RESET_DATABASE,
SQLITE_DBCONFIG_REVERSE_SCANORDER, SQLITE_DBCONFIG_STMT_SCANSTATUS,
SQLITE_DBCONFIG_TRIGGER_EQP, SQLITE_DBCONFIG_TRUSTED_SCHEMA, SQLITE_DBCONFIG_WRITABLE_SCHEMA,
};
use rusqlite::{types::ValueRef, Connection, LoadExtensionGuard, OpenFlags, Statement};
use rusqlite::{DatabaseName, Params};
use rustyline::completion::{Completer, Pair};
use rustyline::error::ReadlineError;
use rustyline::history::FileHistory;
use rustyline::{CompletionType, Config, Context, Editor};
use rustyline_derive::{Helper, Highlighter, Hinter, Validator};
use std::collections::BTreeMap;
use std::fmt;
use std::io::{BufRead, BufReader, Write};
use std::path::{Path, PathBuf};
use std::process::exit;
use tabled::settings::Style;
use tabled::Table;
#[derive(Debug, Parser)]
#[command(name = "libsql")]
#[command(about = "libSQL client", long_about = None)]
struct Cli {
#[clap()]
db_path: Option<String>,
/// Print inputs before execution
#[arg(long, default_value = "false")]
echo: bool,
/// Refuse to open symbolic links to database files
#[arg(long = "nofollow", default_value = "false")]
no_follow: bool,
/// Run "COMMAND" before reading stdin
#[arg(long = "cmd", action = clap::ArgAction::Append)]
command: Option<Vec<String>>,
}
struct StrStatements {
value: String,
}
impl Iterator for StrStatements {
type Item = String;
fn next(&mut self) -> Option<Self::Item> {
let mut embedded = false;
let mut pos = 0;
for (index, char) in self.value.chars().enumerate() {
if char == '\'' {
embedded = !embedded;
continue;
}
if embedded || char != ';' {
continue;
}
let str_statement = self.value[pos..index + 1].to_string();
if str_statement.trim().starts_with(';') {
pos = index + 1;
continue;
}
self.value = self.value[index + 1..].to_string();
return Some(str_statement.trim().to_string());
}
None
}
}
fn get_str_statements(str: String) -> StrStatements {
StrStatements { value: str }
}
/// State information about the database connection is contained in an
/// instance of the following structure.
struct Shell {
/// The database
db: Connection,
/// Write results here
out: Out,
bail: bool,
changes: bool,
echo: bool,
eqp: bool,
explain: ExplainMode,
headers: bool,
mode: OutputMode,
null_value: String,
stats: StatsMode,
width: [usize; 5],
filename: PathBuf,
commands_before_repl: Option<Vec<String>>,
colseparator: String,
rowseparator: String,
main_prompt: String,
continuation_prompt: String,
}
#[allow(dead_code)]
enum Out {
Stdout,
File(std::fs::File, PathBuf),
}
impl Write for Out {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
match self {
Out::Stdout => std::io::stdout().write(buf),
Out::File(file, _) => file.write(buf),
}
}
fn flush(&mut self) -> std::io::Result<()> {
match self {
Out::Stdout => std::io::stdout().flush(),
Out::File(file, _) => file.flush(),
}
}
}
impl fmt::Display for Out {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Out::Stdout => write!(f, "stdout"),
Out::File(_, path) => write!(f, "{}", path.display()),
}
}
}
#[allow(dead_code)]
enum ExplainMode {
Off,
On,
Auto,
}
impl fmt::Display for ExplainMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}",
match *self {
ExplainMode::Off => "off",
ExplainMode::On => "on",
ExplainMode::Auto => "auto",
}
)
}
}
#[allow(dead_code)]
#[derive(Debug)]
enum OutputMode {
/// Columns/rows delimited by 0x1F and 0x1E
Ascii,
/// Tables using unicode box-drawing characters
Box,
/// Comma-separated values
Csv,
/// Output in columns. (see .width)
Column,
/// HTML <table> code
Html,
/// SQL insert statements for TABLE
Insert,
/// Results in a JSON array
Json,
/// One value per line
Line,
/// Values delimited by "|"
List,
/// Markdown table format
Markdown,
/// Escape answers as for SQL
Quote,
/// ASCII-art table
Table,
/// Tab-separated valeus
Tabs,
/// TCL list elements
Tcl,
}
impl fmt::Display for OutputMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", format!("{:?}", self).to_lowercase())
}
}
#[allow(dead_code)]
enum StatsMode {
/// Turn off automatic stat display
Off,
/// Turn on automatic stat display
On,
/// Show statement stats
Stmt,
/// Show the virtual machine step count only
Vmstep,
}
impl fmt::Display for StatsMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}",
match *self {
StatsMode::Off => "off",
StatsMode::On => "on",
StatsMode::Stmt => "stmt",
StatsMode::Vmstep => "vmstep",
}
)
}
}
impl Shell {
fn new(args: Cli) -> Result<Self> {
let connection = match args.db_path.as_deref() {
None | Some("") | Some(":memory:") => {
println!("Connected to a transient in-memory database.");
Connection::open_in_memory()?
}
Some(path) => {
let mut flags = OpenFlags::default();
if args.no_follow {
flags.insert(OpenFlags::SQLITE_OPEN_NOFOLLOW);
}
Connection::open_with_flags(path, flags)?
}
};
Ok(Self {
bail: false,
changes: false,
db: connection,
out: Out::Stdout,
echo: args.echo,
eqp: false,
explain: ExplainMode::Auto,
headers: true,
mode: OutputMode::Column,
stats: StatsMode::Off,
width: [0; 5],
null_value: String::new(),
filename: PathBuf::from(args.db_path.unwrap_or_else(|| ":memory:".to_string())),
commands_before_repl: args.command,
colseparator: String::from("|"),
rowseparator: String::from("\n"),
main_prompt: "libsql> ".to_string(),
continuation_prompt: " ...> ".to_string(),
})
}
fn parse_and_run_command(&mut self, line: &str) -> Result<()> {
// split line on whitespace, but not inside quotes.
let mut split = vec![];
for (i, chunk) in line.split_terminator(&['\'', '"']).enumerate() {
if i % 2 != 0 {
split.push(chunk);
} else {
split.extend(chunk.split_whitespace())
}
}
self.run_command(split[0], &split[1..])
}
fn run(mut self, rl: &mut Editor<ShellHelper, FileHistory>) -> Result<()> {
if let Some(commands) = self.commands_before_repl.take() {
for command in commands {
self.parse_and_run_command(&command)?;
}
}
let mut leftovers = String::new();
loop {
let prompt = if leftovers.is_empty() {
self.main_prompt.as_str()
} else {
self.continuation_prompt.as_str()
};
let readline = rl.readline(prompt);
match readline {
Ok(line) => {
let line = leftovers + line.trim_end();
if line.ends_with(';') || line.starts_with('.') {
leftovers = String::new();
} else {
leftovers = line + " ";
continue;
};
rl.add_history_entry(&line).ok();
if self.echo {
writeln!(self.out, "{}", line)?;
}
if line.starts_with('.') {
self.parse_and_run_command(&line)?;
} else {
for str_statement in get_str_statements(line) {
let table = self.run_statement(str_statement, (), false);
match table {
Ok(table) => {
if self.headers && table.count_rows() == 1
|| !self.headers && table.count_rows() == 0
{
continue;
}
writeln!(self.out, "{}", table)?;
if self.changes {
unsafe {
let db = self.db.handle();
let changes = sqlite3_changes64(db);
let total_changes = sqlite3_total_changes64(db);
writeln!(
self.out,
"changes: {} total_changes: {}",
changes, total_changes
)?;
}
}
}
Err(e) => {
println!("Error: {}", e);
}
}
}
}
}
Err(ReadlineError::Interrupted) => {
println!("^C");
leftovers.clear();
}
Err(ReadlineError::Eof) => {
println!("^D");
break;
}
Err(err) => {
println!("Error: {:?}", err);
break;
}
}
}
Ok(())
}
fn run_command(&mut self, command: &str, args: &[&str]) -> Result<()> {
let mut result = None;
match command {
".bail" => toggle_option(command, &mut self.bail, args),
".changes" => toggle_option(command, &mut self.changes, args),
".databases" => {
let statement = "pragma database_list;";
let mut stmt = self.db.prepare(statement)?;
let mut rows = stmt.query([])?;
while let Some(row) = rows.next()? {
let name = row.get::<_, String>(1)?;
let file = row.get::<_, String>(2)?;
let db_name = match name.as_str() {
"main" => DatabaseName::Main,
"temp" => DatabaseName::Temp,
s => DatabaseName::Attached(s),
};
let readonly = if self.db.is_readonly(db_name)? {
"r/o"
} else {
"r/w"
};
writeln!(self.out, "{}: {:?} {}", name, file, readonly)?;
}
}
".dbconfig" => {
static DBCONFIG: Lazy<BTreeMap<&str, i32>> = Lazy::new(|| {
[
("defensive", SQLITE_DBCONFIG_DEFENSIVE),
("dqs_ddl", SQLITE_DBCONFIG_DQS_DDL),
("dqs_dml", SQLITE_DBCONFIG_DQS_DML),
("enable_fkey", SQLITE_DBCONFIG_ENABLE_FKEY),
("enable_qpsg", SQLITE_DBCONFIG_ENABLE_QPSG),
("enable_trigger", SQLITE_DBCONFIG_ENABLE_TRIGGER),
("enable_view", SQLITE_DBCONFIG_ENABLE_VIEW),
("fts3_tokenizer", SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER),
("legacy_alter_table", SQLITE_DBCONFIG_LEGACY_ALTER_TABLE),
("legacy_file_format", SQLITE_DBCONFIG_LEGACY_FILE_FORMAT),
("load_extension", SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION),
("no_ckpt_on_close", SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE),
("reset_database", SQLITE_DBCONFIG_RESET_DATABASE),
("reverse_scanorder", SQLITE_DBCONFIG_REVERSE_SCANORDER),
("stmt_scanstatus", SQLITE_DBCONFIG_STMT_SCANSTATUS),
("trigger_eqp", SQLITE_DBCONFIG_TRIGGER_EQP),
("trusted_schema", SQLITE_DBCONFIG_TRUSTED_SCHEMA),
("writable_schema", SQLITE_DBCONFIG_WRITABLE_SCHEMA),
]
.into_iter()
.collect()
});
let db = unsafe { self.db.handle() };
if args.is_empty() {
for (name, opt) in DBCONFIG.iter() {
let enabled = 0;
unsafe {
sqlite3_db_config(db, *opt, -1, &enabled);
}
writeln!(
self.out,
"{:>19} {}",
name,
if enabled == 0 { "off" } else { "on" }
)?;
}
} else {
match DBCONFIG.get(args[0]) {
Some(opt) => match args.len() {
1 => {
let enabled = 0;
unsafe {
sqlite3_db_config(db, *opt, -1, &enabled);
}
writeln!(
self.out,
"{:>19} {}",
args[0],
if enabled == 0 { "off" } else { "on" }
)?;
}
2 => {
let enabled = match args[1].to_lowercase().as_str() {
"on" | "true" | "yes" => true,
"off" | "false" | "no" => false,
arg => {
if arg.chars().all(|a| a.is_ascii_digit()) {
arg != "0"
} else {
println!("ERROR: Not a boolean value: \"{}\". Assuming \"no\"", arg);
false
}
}
};
unsafe {
sqlite3_db_config(db, *opt, enabled as i32, 0);
}
writeln!(
self.out,
"{:>19} {}",
args[0],
if enabled { "on" } else { "off" }
)?;
}
_ => println!("Usage: .dbconfig ?op? ?val? List or change sqlite3_db_config() options"),
},
None => {
println!("Error: unknown dbconfig {:?}\nEnter \".dbconfig\" with no arguments for a list", args[0]);
}
}
}
}
".echo" => toggle_option(command, &mut self.echo, args),
".exit" => {
if args.len() != 1 {
exit(0);
}
let mut chars = args[0].bytes();
let mut code = 0;
let neg = match chars.next() {
Some(b'-') => true,
Some(c) if c.is_ascii_digit() => {
code = code * 10 + (c - b'0') as i32;
false
}
_ => exit(0),
};
for c in chars {
if c.is_ascii_digit() {
code = code * 10 + (c - b'0') as i32;
} else {
exit(0);
}
}
if neg {
code = -code;
}
// exit code is in range [0, 255]
exit(code);
}
".headers" => {
if args.len() != 1 {
writeln!(self.out, "Usage: .headers on|off")?;
return Ok(());
}
match args[0].to_lowercase().as_str() {
"on" | "true" => self.headers = true,
"off" | "false" => self.headers = false,
txt => {
self.headers = false;
writeln!(
self.out,
"ERROR: Not a boolean value: \"{}\". Assuming \"no\"",
txt
)?
}
}
}
".help" => self.show_help(args),
".indexes" => result = Some(self.list_tables(args.first().copied(), true)),
".load" => {
let (filename, entrypoint) = match args.len() {
1 => (args[0], None),
2 => (args[0], Some(args[1])),
_ => {
writeln!(self.out, "Usage: .load FILENAME ?ENTRYPOINT?")?;
return Ok(());
}
};
if let Err(e) = load_my_extension(&self.db, filename, entrypoint) {
println!("Error: {}", e);
}
}
".nullvalue" => {
if args.len() != 1 {
writeln!(self.out, "Usage: .nullvalue STRING")?;
return Ok(());
}
self.null_value = args[0].to_string();
}
".print" => {
writeln!(self.out, "{}", args.join(" "))?;
}
".prompt" => {
if !args.is_empty() {
self.main_prompt = args[0].to_string();
}
if args.len() > 1 {
self.continuation_prompt = args[1].to_string();
}
}
".quit" => exit(0),
".open" => {
// .open ?OPTIONS? ?FILE?
let mut filename = None;
let mut flags = OpenFlags::default();
for arg in args {
match *arg {
"--append" | "--deserialize" | "--hexdb" | "--maxsize" | "--zip" => {
println!("`{}` is not supported yet", arg);
return Ok(());
}
"--new" => flags.insert(OpenFlags::SQLITE_OPEN_CREATE),
"--nofollow" => flags.insert(OpenFlags::SQLITE_OPEN_NOFOLLOW),
"--readonly" => {
flags.remove(OpenFlags::SQLITE_OPEN_CREATE);
flags.remove(OpenFlags::SQLITE_OPEN_READ_WRITE);
flags.insert(OpenFlags::SQLITE_OPEN_READ_ONLY);
}
arg => {
if arg.starts_with('-') {
println!("unknown option: {}", arg);
return Ok(());
}
if filename.is_some() {
println!("extra argument: \"{}\"", arg);
return Ok(());
}
filename = Some(arg);
}
}
}
(self.filename, self.db) = match filename {
Some(path) => {
let db = match Connection::open_with_flags(path, flags) {
Ok(con) => con,
Err(e) => {
println!("Error: unable to open database \"{}\": {}\nNotice: using substitute in-memory database instead of \"{}\"", path, e, path);
return Ok(());
}
};
(path.into(), db)
}
None => {
let db = match Connection::open_in_memory() {
Ok(con) => con,
Err(_e) => {
println!("Error: unable to open database in memory");
return Ok(());
}
};
("".into(), db)
}
};
}
".read" => {
if args.len() != 1 {
writeln!(self.out, "Usage: .read FILE")?;
return Ok(());
}
let filename = args[0];
let reader = match std::fs::File::open(filename) {
Ok(file) => BufReader::new(file),
Err(_e) => {
println!("Error: cannot open \"{}\"", args[0]);
return Ok(());
}
};
for (i, line) in reader.lines().enumerate() {
let statement = line?;
match self.run_statement(statement, (), false) {
Ok(table) => {
if !table.is_empty() {
writeln!(self.out, "{}", table)?;
}
if self.changes {
unsafe {
let db = self.db.handle();
let changes = sqlite3_changes64(db);
let total_changes = sqlite3_total_changes64(db);
writeln!(
self.out,
"changes: {} total_changes: {}",
changes, total_changes
)?;
}
}
}
Err(e) => {
println!("Parse error near line {}: {}", i + 1, e);
if self.bail {
break;
}
}
}
}
}
".show" => {
if !args.is_empty() {
writeln!(self.out, "Usage: .show")?;
return Ok(());
}
let out_name = format!("{}", self.out);
write!(
self.out,
r#"{:>12}: {}
{:>12}: {}
{:>12}: {}
{:>12}: {}
{:>12}: {}
{:>12}: "{}"
{:>12}: {}
{:>12}: {:?}
{:>12}: {:?}
{:>12}: {}
{:>12}: {:?}
{:>12}: {}
"#,
"echo",
if self.echo { "on" } else { "off" },
"eqp",
if self.eqp { "on" } else { "off" },
"explain",
self.explain,
"headers",
if self.headers { "on" } else { "off" },
"mode",
self.mode,
"nullvalue",
self.null_value,
"output",
out_name,
"colseparator",
self.colseparator,
"rowseparator",
self.rowseparator,
"stats",
self.stats,
"width",
self.width,
"filename",
self.filename.display()
)?;
}
".tables" => result = Some(self.list_tables(args.first().copied(), false)),
_ => println!(
"Error: unknown command or invalid arguments: \"{}\". Enter \".help\" for help",
command
),
}
match result {
Some(Ok(mut table)) => {
if table.count_rows() != 0 {
table.with(Style::blank());
writeln!(self.out, "{}", table)?;
}
}
Some(Err(e)) => {
println!("Error: {e}");
}
None => {}
}
Ok(())
}
fn run_statement<P>(&self, statement: String, params: P, is_command: bool) -> Result<Table>
where
P: Params,
{
let mut stmt: Statement<'_> = self.db.prepare(&statement)?;
// TODO: introduce paging for presenting large results, get rid of Vec
let rows: Vec<Vec<String>> = {
let column_count = stmt.column_count();
let rows = stmt.query_map(params, |row| {
let row = (0..column_count)
.map(|idx| self.format_value(row.get_ref(idx).unwrap()))
.collect::<Vec<String>>();
Ok(row)
})?;
let mut mapped_rows = vec![];
for row in rows.flatten() {
mapped_rows.push(row);
}
mapped_rows
};
let mut builder = tabled::builder::Builder::new();
// TODO: switch style based on mode.
let style = Style::ascii();
if self.headers && !is_command {
// if we use a SQL statement to execute a command, don't include the headers.
// affected commands: .tables
builder.set_header(stmt.column_names());
}
for row in rows {
builder.push_record(row);
}
let mut table = builder.build();
table.with(style);
Ok(table)
}
// helper functions
// Presents libSQL values in human-readable form
fn format_value(&self, v: ValueRef) -> String {
match v {
ValueRef::Null => self.null_value.clone(),
ValueRef::Integer(i) => format!("{i}"),
ValueRef::Real(r) => format!("{r}"),
ValueRef::Text(s) => std::str::from_utf8(s).unwrap().to_owned(),
ValueRef::Blob(b) => format!("0x{}", general_purpose::STANDARD_NO_PAD.encode(b)),
}
}
// COMMANDS
fn list_tables(&self, pattern: Option<&str>, is_index: bool) -> Result<Table> {
let mut statement =
String::from("SELECT name FROM sqlite_schema WHERE name NOT LIKE 'sqlite_%'");
if is_index {
statement.push_str("AND type='index' ")
} else {
statement.push_str("AND type IN ('table','view') ")
}
match pattern {
Some(p) => {
if is_index {
statement.push_str("AND tbl_name LIKE :name;");
} else {
statement.push_str("AND name NOT LIKE 'sqlite_%' AND name LIKE :name;");
}
self.run_statement(statement, &[(":name", p)], true)
}
None => {
statement.push(';');
self.run_statement(statement, (), true)
}
}
}
// TODO: implement `-all` option: print detailed flags for each command
// TODO: implement `?PATTERN?` : allow narrowing using prefix search.
fn show_help(&mut self, _args: &[&str]) {
let help = r#"
.auth ON|OFF Show authorizer callbacks
.backup ?DB? FILE Backup DB (default "main") to FILE
.bail on|off Stop after hitting an error. Default OFF
.binary on|off Turn binary output on or off. Default OFF
.cd DIRECTORY Change the working directory to DIRECTORY
.changes on|off Show number of rows changed by SQL
.check GLOB Fail if output since .testcase does not match
.clone NEWDB Clone data into NEWDB from the existing database
.connection [close] [#] Open or close an auxiliary database connection
.databases List names and files of attached databases
.dbconfig ?op? ?val? List or change sqlite3_db_config() options
.dbinfo ?DB? Show status information about the database
.dump ?OBJECTS? Render database content as SQL
.echo on|off Turn command echo on or off
.eqp on|off|full|... Enable or disable automatic EXPLAIN QUERY PLAN
.excel Display the output of next command in spreadsheet
.exit ?CODE? Exit this program with return-code CODE
.expert EXPERIMENTAL. Suggest indexes for queries
.explain ?on|off|auto? Change the EXPLAIN formatting mode. Default: auto
.filectrl CMD ... Run various sqlite3_file_control() operations
.fullschema ?--indent? Show schema and the content of sqlite_stat tables
.headers on|off Turn display of headers on or off
.help ?-all? ?PATTERN? Show help text for PATTERN
.import FILE TABLE Import data from FILE into TABLE
.imposter INDEX TABLE Create imposter table TABLE on index INDEX
.indexes ?TABLE? Show names of indexes
.limit ?LIMIT? ?VAL? Display or change the value of an SQLITE_LIMIT
.lint OPTIONS Report potential schema issues.
.load FILE ?ENTRY? Load an extension library
.log FILE|off Turn logging on or off. FILE can be stderr/stdout
.mode MODE ?TABLE? Set output mode
.nonce STRING Disable safe mode for one command if the nonce matches
.nullvalue STRING Use STRING in place of NULL values
.once ?OPTIONS? ?FILE? Output for the next SQL command only to FILE
.open ?OPTIONS? ?FILE? Close existing database and reopen FILE
.output ?FILE? Send output to FILE or stdout if FILE is omitted
.parameter CMD ... Manage SQL parameter bindings
.print STRING... Print literal STRING
.progress N Invoke progress handler after every N opcodes
.prompt MAIN CONTINUE Replace the standard prompts
.quit Exit this program
.read FILE Read input from FILE
.recover Recover as much data as possible from corrupt db.
.restore ?DB? FILE Restore content of DB (default "main") from FILE
.save FILE Write in-memory database into FILE
.scanstats on|off Turn sqlite3_stmt_scanstatus() metrics on or off
.schema ?PATTERN? Show the CREATE statements matching PATTERN
.selftest ?OPTIONS? Run tests defined in the SELFTEST table
.separator COL ?ROW? Change the column and row separators
.session ?NAME? CMD ... Create or control sessions
.sha3sum ... Compute a SHA3 hash of database content
.shell CMD ARGS... Run CMD ARGS... in a system shell
.show Show the current values for various settings
.stats ?ARG? Show stats or turn stats on or off
.system CMD ARGS... Run CMD ARGS... in a system shell
.tables ?TABLE? List names of tables matching LIKE pattern TABLE
.testcase NAME Begin redirecting output to 'testcase-out.txt'
.testctrl CMD ... Run various sqlite3_test_control() operations
.timeout MS Try opening locked tables for MS milliseconds
.timer on|off Turn SQL timer on or off
.trace ?OPTIONS? Output each SQL statement as it is run
.vfsinfo ?AUX? Information about the top-level VFS
.vfslist List all available VFSes
.vfsname ?AUX? Print the name of the VFS stack
.width NUM1 NUM2 ... Set minimum column widths for columnar output
"#;
_ = writeln!(self.out, "{}", help.trim());
}
}
#[derive(Default)]
struct ShellCompleter {}
impl ShellCompleter {
fn new() -> Self {
Self::default()
}
fn complete(
&self,
line: &str,
_pos: usize,
_: &Context,
) -> Result<(usize, Vec<Pair>), ReadlineError> {
let mut pairs: Vec<Pair> = vec![];
let commands = vec![
".echo",
".headers",
".help",
".indexes",
".nullvalue",
".print",
".prompt",
".quit",
".show",
".tables",
];
for command in commands {
if command.starts_with(line) {
pairs.push(Pair {
display: command.to_string(),
replacement: command.to_string(),
})
}
}
Ok((0, pairs))
}
}
#[derive(Helper, Hinter, Validator, Highlighter)]
struct ShellHelper {
#[rustyline(Completer)]
completer: ShellCompleter,
}
impl Completer for ShellHelper {
type Candidate = Pair;
fn complete(
&self,
line: &str,
pos: usize,
ctx: &Context,
) -> Result<(usize, Vec<Pair>), ReadlineError> {
self.completer.complete(line, pos, ctx)
}
}
fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let args = Cli::parse();
let config = Config::builder()
.history_ignore_space(true)
.completion_type(CompletionType::Circular)
.build();
let mut rl = Editor::with_config(config)?;
let helper = ShellHelper {
completer: ShellCompleter::new(),
};
rl.set_helper(Some(helper));
let mut history = home::home_dir().unwrap_or_default();
history.push(".libsql_history");
rl.load_history(history.as_path()).ok();
println!("libSQL version 0.2.0");
let shell = Shell::new(args)?;
let result = shell.run(&mut rl);
rl.save_history(history.as_path()).ok();
result
}
fn toggle_option(name: &str, value: &mut bool, args: &[&str]) {
if args.len() != 1 {
println!("Usage: {} on|off", name);
return;
}
match args[0].to_lowercase().as_str() {
"on" | "true" | "yes" => *value = true,
"off" | "false" | "no" => *value = false,
arg => {
// FIXME Run with `.bail '123"`, it should not be legal, but `split_terminator`
// return args as ["123"] which however work here.
// It's not a big problem, but it doesn't behave same as `sqlite`.
if arg.chars().all(|a| a.is_ascii_digit()) {
*value = arg != "0";
} else {
*value = false;
println!("ERROR: Not a boolean value: \"{}\". Assuming \"no\"", arg)
}
}
}
}
fn load_my_extension<P: AsRef<Path>>(
conn: &Connection,
dylib_path: P,
entry_point: Option<&str>,
) -> rusqlite::Result<()> {
let _guard = unsafe { LoadExtensionGuard::new(conn)? };
unsafe { conn.load_extension(dylib_path, entry_point) }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_str_statements_iterator() {
let mut str_statements_iterator =
get_str_statements(String::from("SELECT ';' FROM test; SELECT * FROM test;;"));
assert_eq!(
str_statements_iterator.next(),
Some("SELECT ';' FROM test;".to_owned())
);
assert_eq!(
str_statements_iterator.next(),
Some("SELECT * FROM test;".to_owned())
);
assert_eq!(str_statements_iterator.next(), None);
let mut str_statements_iterator = get_str_statements(String::from(";;;"));
assert_eq!(str_statements_iterator.next(), None);
let mut str_statements_iterator = get_str_statements(String::from(" "));
assert_eq!(str_statements_iterator.next(), None);
let mut str_statements_iterator = get_str_statements(String::from(" ; ; ; "));
assert_eq!(str_statements_iterator.next(), None);
}
#[test]
fn test_empty_statement() {
let cli = Cli {
db_path: Some(":memory:".to_string()),
echo: false,
no_follow: false,
command: None,
};
let shell = Shell::new(cli).unwrap();
assert!(shell.headers);
let result = shell.run_statement(" ; ; ;".to_string(), [], false);
assert!(result.is_ok());
let table = result.unwrap();
assert_eq!(table.count_rows(), 1);
}
}