mirror of
https://github.com/aljazceru/turso.git
synced 2025-12-17 00:24:21 +01:00
Closes #1282 # Support for WHERE clause subqueries This PR implements support for subqueries that appear in the WHERE clause of SELECT statements. ## What are those lol 1. **EXISTS subqueries**: `WHERE EXISTS (SELECT ...)` 2. **Row value subqueries**: `WHERE x = (SELECT ...)` or `WHERE (x, y) = (SELECT ...)`. The latter are not yet supported - only the single-column ("scalar subquery") case is. 3. **IN subqueries**: `WHERE x IN (SELECT ...)` or `WHERE (x, y) IN (SELECT ...)` ## Correlated vs Uncorrelated Subqueries - **Uncorrelated subqueries** reference only their own tables and can be evaluated once. - **Correlated subqueries** reference columns from the outer query (e.g., `WHERE EXISTS (SELECT * FROM t2 WHERE t2.id = t1.id)`) and must be re-evaluated for each row of the outer query ## Implementation ### Planning During query planning, the WHERE clause is walked to find subquery expressions (`Expr::Exists`, `Expr::Subquery`, `Expr::InSelect`). Each subquery is: 1. Assigned a unique internal ID 2. Compiled into its own `SelectPlan` with outer query tables provided as available references 3. Replaced in the AST with an `Expr::SubqueryResult` node that references the subquery with its internal ID 4. Stored in a `Vec<NonFromClauseSubquery>` on the `SelectPlan` For IN subqueries, an ephemeral index is created to store the subquery results; for other kinds, the results are stored in register(s). ### Translation Before emitting bytecode, we need to determine when each subquery should be evaluated: - **Uncorrelated**: Evaluated once before opening any table cursors - **Correlated**: Evaluated at the appropriate nested loop depth after all referenced outer tables are in scope This is calculated by examining which outer query tables the subquery references and finding the right-most (innermost) loop that opens those tables - using similar mechanisms that we use for figuring out when to evaluate other `WhereTerm`s too. ### Code Generation - **EXISTS**: Sets a register to 1 if any row is produced, 0 otherwise. Has new `QueryDestination::ExistsSubqueryResult` variant. - **IN**: Results stored in an ephemeral index and the index is probed. - **RowValue**: Results stored in a range of registers. Has new `QueryDestination::RowValueSubqueryResult` variant. ## Annoying details ### Which cursor to read from in a subquery? Sometimes a query will use a covering index, i.e. skip opening the table cursor at all if the index contains All The Needed Stuff. Correlated subqueries reading columns from outer tables is a bit problematic in this regard: with our current translation code, the subquery doesn't know whether the outer query opened a table cursor, index cursor, or both. So, for now, we try to find a table cursor first, then fall back to finding any index cursor for that table. Reviewed-by: Preston Thorpe <preston@turso.tech> Closes #3847
1809 lines
70 KiB
Rust
1809 lines
70 KiB
Rust
use crate::{
|
||
commands::{
|
||
args::{EchoMode, HeadersMode, TimerMode},
|
||
import::ImportFile,
|
||
Command, CommandParser,
|
||
},
|
||
config::Config,
|
||
helper::LimboHelper,
|
||
input::{
|
||
get_io, get_writer, ApplyWriter, DbLocation, NoopProgress, OutputMode, ProgressSink,
|
||
Settings, StderrProgress,
|
||
},
|
||
manual,
|
||
opcodes_dictionary::OPCODE_DESCRIPTIONS,
|
||
HISTORY_FILE,
|
||
};
|
||
use anyhow::anyhow;
|
||
use clap::Parser;
|
||
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},
|
||
Arc,
|
||
},
|
||
time::{Duration, Instant},
|
||
};
|
||
|
||
use tracing_appender::non_blocking::WorkerGuard;
|
||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
|
||
use turso_core::{
|
||
Connection, Database, LimboError, OpenFlags, QueryMode, Statement, StepResult, Value,
|
||
};
|
||
|
||
#[derive(Parser, Debug)]
|
||
#[command(name = "Turso")]
|
||
#[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 = "Open the database in read-only mode")]
|
||
pub readonly: bool,
|
||
#[clap(long, help = "Enable experimental MVCC feature")]
|
||
pub experimental_mvcc: bool,
|
||
#[clap(long, help = "Enable experimental views feature")]
|
||
pub experimental_views: bool,
|
||
#[clap(long, help = "Enable experimental indexing feature")]
|
||
pub experimental_indexes: Option<bool>,
|
||
#[clap(long, help = "Enable experimental strict schema mode")]
|
||
pub experimental_strict: bool,
|
||
#[clap(short = 't', long, help = "specify output file for log traces")]
|
||
pub tracing_output: Option<String>,
|
||
#[clap(long, help = "Start MCP server instead of interactive shell")]
|
||
pub mcp: bool,
|
||
#[clap(long, help = "Enable experimental encryption feature")]
|
||
pub experimental_encryption: bool,
|
||
#[clap(long, help = "Enable experimental index method feature")]
|
||
pub experimental_index_method: bool,
|
||
}
|
||
|
||
const PROMPT: &str = "turso> ";
|
||
|
||
pub struct Limbo {
|
||
pub prompt: String,
|
||
io: Arc<dyn turso_core::IO>,
|
||
writer: Option<Box<dyn Write>>,
|
||
conn: Arc<turso_core::Connection>,
|
||
pub interrupt_count: Arc<AtomicUsize>,
|
||
input_buff: ManuallyDrop<String>,
|
||
opts: Settings,
|
||
pub rl: Option<Editor<LimboHelper, DefaultHistory>>,
|
||
config: Option<Config>,
|
||
}
|
||
|
||
struct QueryStatistics {
|
||
io_time_elapsed_samples: Vec<Duration>,
|
||
execute_time_elapsed_samples: Vec<Duration>,
|
||
}
|
||
|
||
macro_rules! row_step_result_query {
|
||
($app:expr, $sql:expr, $rows:expr, $stats:expr, $row_handle:expr) => {
|
||
if $app.interrupt_count.load(Ordering::Acquire) > 0 {
|
||
println!("Query interrupted.");
|
||
return Ok(());
|
||
}
|
||
|
||
let start = if $stats.is_some() {
|
||
Some(Instant::now())
|
||
} else {
|
||
None
|
||
};
|
||
match $rows.step() {
|
||
Ok(StepResult::Row) => {
|
||
if let Some(ref mut stats) = $stats {
|
||
stats
|
||
.execute_time_elapsed_samples
|
||
.push(start.unwrap().elapsed());
|
||
}
|
||
|
||
$row_handle
|
||
}
|
||
Ok(StepResult::IO) => {
|
||
if let Some(ref mut stats) = $stats {
|
||
stats.io_time_elapsed_samples.push(start.unwrap().elapsed());
|
||
}
|
||
let start = if $stats.is_some() {
|
||
Some(Instant::now())
|
||
} else {
|
||
None
|
||
};
|
||
$rows.run_once()?;
|
||
if let Some(ref mut stats) = $stats {
|
||
stats.io_time_elapsed_samples.push(start.unwrap().elapsed());
|
||
}
|
||
}
|
||
Ok(StepResult::Interrupt) => {
|
||
if let Some(ref mut stats) = $stats {
|
||
stats
|
||
.execute_time_elapsed_samples
|
||
.push(start.unwrap().elapsed());
|
||
}
|
||
break;
|
||
}
|
||
Ok(StepResult::Done) => {
|
||
if let Some(ref mut stats) = $stats {
|
||
stats
|
||
.execute_time_elapsed_samples
|
||
.push(start.unwrap().elapsed());
|
||
}
|
||
break;
|
||
}
|
||
Ok(StepResult::Busy) => {
|
||
if let Some(ref mut stats) = $stats {
|
||
stats
|
||
.execute_time_elapsed_samples
|
||
.push(start.unwrap().elapsed());
|
||
}
|
||
let _ = $app.writeln("database is busy");
|
||
break;
|
||
}
|
||
Err(err) => {
|
||
if let Some(ref mut stats) = $stats {
|
||
stats
|
||
.execute_time_elapsed_samples
|
||
.push(start.unwrap().elapsed());
|
||
}
|
||
let report = miette::Error::from(err).with_source_code($sql.to_owned());
|
||
let _ = $app.writeln_fmt(format_args!("{report:?}"));
|
||
break;
|
||
}
|
||
}
|
||
};
|
||
}
|
||
|
||
impl Limbo {
|
||
pub fn new() -> anyhow::Result<(Self, WorkerGuard)> {
|
||
let mut opts = Opts::parse();
|
||
let guard = Self::init_tracing(&opts)?;
|
||
|
||
let db_file = opts
|
||
.database
|
||
.as_ref()
|
||
.map_or(":memory:".to_string(), |p| p.to_string_lossy().to_string());
|
||
let indexes_enabled = opts.experimental_indexes.unwrap_or(true);
|
||
let (io, conn) = if db_file.contains([':', '?', '&', '#']) {
|
||
Connection::from_uri(
|
||
&db_file,
|
||
indexes_enabled,
|
||
opts.experimental_mvcc,
|
||
opts.experimental_views,
|
||
opts.experimental_strict,
|
||
opts.experimental_encryption,
|
||
opts.experimental_index_method,
|
||
)?
|
||
} else {
|
||
let flags = if opts.readonly {
|
||
OpenFlags::ReadOnly
|
||
} else {
|
||
OpenFlags::default()
|
||
};
|
||
let (io, db) = Database::open_new(
|
||
&db_file,
|
||
opts.vfs.as_ref(),
|
||
flags,
|
||
turso_core::DatabaseOpts::new()
|
||
.with_mvcc(opts.experimental_mvcc)
|
||
.with_indexes(indexes_enabled)
|
||
.with_views(opts.experimental_views)
|
||
.with_strict(opts.experimental_strict)
|
||
.with_encryption(opts.experimental_encryption)
|
||
.with_index_method(opts.experimental_index_method)
|
||
.turso_cli(),
|
||
None,
|
||
)?;
|
||
let conn = db.connect()?;
|
||
(io, conn)
|
||
};
|
||
unsafe {
|
||
let mut ext_api = conn._build_turso_ext();
|
||
if !limbo_completion::register_extension_static(&mut ext_api).is_ok() {
|
||
return Err(anyhow!(
|
||
"Failed to register completion extension".to_string()
|
||
));
|
||
}
|
||
conn._free_extension_ctx(ext_api);
|
||
}
|
||
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::Release);
|
||
})
|
||
.expect("Error setting Ctrl-C handler");
|
||
}
|
||
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 {
|
||
prompt: PROMPT.to_string(),
|
||
io,
|
||
writer: Some(get_writer(&opts.output)),
|
||
conn,
|
||
interrupt_count,
|
||
input_buff: ManuallyDrop::new(sql.unwrap_or_default()),
|
||
opts: Settings::from(opts),
|
||
rl: None,
|
||
config: Some(config),
|
||
};
|
||
app.first_run(has_sql, quiet)?;
|
||
Ok((app, guard))
|
||
}
|
||
|
||
pub fn with_config(mut self, config: Config) -> Self {
|
||
self.config = Some(config);
|
||
self
|
||
}
|
||
|
||
pub fn with_readline(mut self, mut rl: Editor<LimboHelper, DefaultHistory>) -> Self {
|
||
let h = LimboHelper::new(
|
||
self.conn.clone(),
|
||
self.config.as_ref().map(|c| c.highlight.clone()),
|
||
);
|
||
rl.set_helper(Some(h));
|
||
self.rl = Some(rl);
|
||
self
|
||
}
|
||
|
||
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 has_sql {
|
||
self.handle_first_input()?;
|
||
}
|
||
if !quiet {
|
||
self.writeln_fmt(format_args!("Turso v{}", env!("CARGO_PKG_VERSION")))?;
|
||
self.writeln("Enter \".help\" for usage hints.")?;
|
||
|
||
// Add random feature hint
|
||
if let Some(hint) = manual::get_random_feature_hint() {
|
||
self.writeln(&hint)?;
|
||
}
|
||
|
||
self.writeln(
|
||
"This software is in BETA, use caution with production data and ensure you have backups."
|
||
)?;
|
||
self.display_in_memory()?;
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
fn handle_first_input(&mut self) -> Result<(), LimboError> {
|
||
self.consume(true);
|
||
self.close_conn()?;
|
||
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 = turso_core::resolve_ext_path(path).map_err(|e| e.to_string())?;
|
||
self.conn
|
||
.load_extension(ext_path)
|
||
.map_err(|e| e.to_string())
|
||
}
|
||
|
||
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)
|
||
}
|
||
|
||
fn display_stats(&mut self, args: crate::commands::args::StatsArgs) -> io::Result<()> {
|
||
use crate::commands::args::StatsToggle;
|
||
|
||
// Handle on/off toggle
|
||
if let Some(toggle) = args.toggle {
|
||
match toggle {
|
||
StatsToggle::On => {
|
||
self.opts.stats = true;
|
||
self.writeln("Stats display enabled.")?;
|
||
}
|
||
StatsToggle::Off => {
|
||
self.opts.stats = false;
|
||
self.writeln("Stats display disabled.")?;
|
||
}
|
||
}
|
||
return Ok(());
|
||
}
|
||
|
||
// Display all metrics
|
||
let output = {
|
||
let metrics = self.conn.metrics.read();
|
||
format!("{metrics}")
|
||
};
|
||
|
||
self.writeln(output)?;
|
||
|
||
if args.reset {
|
||
self.conn.metrics.write().reset();
|
||
self.writeln("Statistics reset.")?;
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
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()
|
||
}
|
||
|
||
pub fn get_connection(&self) -> Arc<turso_core::Connection> {
|
||
self.conn.clone()
|
||
}
|
||
|
||
pub fn is_mcp_mode(&self) -> bool {
|
||
self.opts.mcp
|
||
}
|
||
|
||
pub fn get_interrupt_count(&self) -> Arc<AtomicUsize> {
|
||
self.interrupt_count.clone()
|
||
}
|
||
|
||
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, 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 = Some(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.as_mut().unwrap().flush();
|
||
self.writer = Some(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<()> {
|
||
self.writer.as_mut().unwrap().write_fmt(fmt)
|
||
}
|
||
|
||
fn writeln_fmt(&mut self, fmt: std::fmt::Arguments) -> io::Result<()> {
|
||
self.writer.as_mut().unwrap().write_fmt(fmt)?;
|
||
self.writer.as_mut().unwrap().write_all(b"\n")
|
||
}
|
||
|
||
fn write<D: AsRef<[u8]>>(&mut self, data: D) -> io::Result<()> {
|
||
self.writer.as_mut().unwrap().write_all(data.as_ref())
|
||
}
|
||
|
||
fn writeln<D: AsRef<[u8]>>(&mut self, data: D) -> io::Result<()> {
|
||
self.writer.as_mut().unwrap().write_all(data.as_ref())?;
|
||
self.writer.as_mut().unwrap().write_all(b"\n")
|
||
}
|
||
|
||
fn write_null(&mut self) -> io::Result<()> {
|
||
self.writer
|
||
.as_mut()
|
||
.unwrap()
|
||
.write_all(self.opts.null_value.as_bytes())
|
||
}
|
||
fn run_query(&mut self, input: &str) {
|
||
let echo = self.opts.echo;
|
||
if echo {
|
||
let _ = self.writeln(input);
|
||
}
|
||
|
||
let start = Instant::now();
|
||
let mut stats = if self.opts.timer {
|
||
Some(QueryStatistics {
|
||
io_time_elapsed_samples: vec![],
|
||
execute_time_elapsed_samples: vec![],
|
||
})
|
||
} else {
|
||
None
|
||
};
|
||
|
||
let conn = self.conn.clone();
|
||
let runner = conn.query_runner(input.as_bytes());
|
||
for output in runner {
|
||
if self
|
||
.print_query_result(input, output, stats.as_mut())
|
||
.is_err()
|
||
{
|
||
break;
|
||
}
|
||
}
|
||
|
||
self.print_query_performance_stats(start, stats.as_ref());
|
||
|
||
// Display stats if enabled
|
||
if self.opts.stats {
|
||
let stats_output = {
|
||
let metrics = self.conn.metrics.read();
|
||
metrics
|
||
.last_statement
|
||
.as_ref()
|
||
.map(|last| format!("\n{last}"))
|
||
};
|
||
if let Some(output) = stats_output {
|
||
let _ = self.writeln(output);
|
||
}
|
||
}
|
||
}
|
||
|
||
fn print_query_performance_stats(&mut self, start: Instant, stats: Option<&QueryStatistics>) {
|
||
let elapsed_as_str = |duration: Duration| {
|
||
if duration.as_secs() >= 1 {
|
||
format!("{} s", duration.as_secs_f64())
|
||
} else if duration.as_millis() >= 1 {
|
||
format!("{} ms", duration.as_millis() as f64)
|
||
} else if duration.as_micros() >= 1 {
|
||
format!("{} us", duration.as_micros() as f64)
|
||
} else {
|
||
format!("{} ns", duration.as_nanos())
|
||
}
|
||
};
|
||
let sample_stats_as_str = |name: &str, samples: &Vec<Duration>| {
|
||
if samples.is_empty() {
|
||
return format!("{name}: No samples available");
|
||
}
|
||
let avg_time_spent = samples.iter().sum::<Duration>() / samples.len() as u32;
|
||
let total_time = samples.iter().fold(Duration::ZERO, |acc, x| acc + *x);
|
||
format!(
|
||
"{}: avg={}, total={}",
|
||
name,
|
||
elapsed_as_str(avg_time_spent),
|
||
elapsed_as_str(total_time),
|
||
)
|
||
};
|
||
if self.opts.timer {
|
||
if let Some(stats) = stats {
|
||
let _ = self.writeln("Command stats:\n----------------------------");
|
||
let _ = self.writeln(format!(
|
||
"total: {} (this includes parsing/coloring of cli app)\n",
|
||
elapsed_as_str(start.elapsed())
|
||
));
|
||
|
||
let _ = self.writeln("query execution stats:\n----------------------------");
|
||
let _ = self.writeln(sample_stats_as_str(
|
||
"Execution",
|
||
&stats.execute_time_elapsed_samples,
|
||
));
|
||
let _ = self.writeln(sample_stats_as_str("I/O", &stats.io_time_elapsed_samples));
|
||
}
|
||
}
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
// consume will consume `input_buff`
|
||
pub fn consume(&mut self, flush: bool) {
|
||
if self.input_buff.trim().is_empty() {
|
||
return;
|
||
}
|
||
|
||
self.reset_line();
|
||
|
||
// 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 || !app.input_buff.is_empty() {
|
||
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, _) => {
|
||
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) => {
|
||
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 => {
|
||
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) => {
|
||
self.set_multiline_prompt();
|
||
}
|
||
}
|
||
}
|
||
|
||
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 clap print with Styled Colors instead
|
||
let _ = err.print();
|
||
}
|
||
Ok(cmd) => match cmd.command {
|
||
Command::Exit(args) => {
|
||
self.save_history();
|
||
std::process::exit(args.code);
|
||
}
|
||
Command::Quit => {
|
||
let _ = self.writeln("Exiting Turso SQL Shell.");
|
||
let _ = self.close_conn();
|
||
self.save_history();
|
||
std::process::exit(0)
|
||
}
|
||
Command::Open(args) => {
|
||
if let Err(e) = self.open_db(&args.path, args.vfs_name.as_deref()) {
|
||
let _ = self.writeln(e.to_string());
|
||
}
|
||
}
|
||
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::Databases => {
|
||
if let Err(e) = self.display_databases() {
|
||
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.writeln_fmt(format_args!("{op}"));
|
||
}
|
||
}
|
||
} else {
|
||
for op in &OPCODE_DESCRIPTIONS {
|
||
let _ = self.writeln_fmt(format_args!("{op}\n"));
|
||
}
|
||
}
|
||
}
|
||
Command::NullValue(args) => {
|
||
self.opts.null_value = args.value;
|
||
}
|
||
Command::OutputMode(args) => {
|
||
if let Err(e) = self.set_mode(args.mode) {
|
||
let _ = self.writeln_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.writeln_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::Stats(args) => {
|
||
if let Err(e) = self.display_stats(args) {
|
||
let _ = self.writeln(e.to_string());
|
||
}
|
||
}
|
||
Command::Import(args) => {
|
||
let w = self.writer.as_mut().unwrap();
|
||
let mut import_file = ImportFile::new(self.conn.clone(), w);
|
||
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.writeln_fmt(format_args!("/****** ERROR: {e} ******/"));
|
||
}
|
||
}
|
||
Command::DbConfig(_args) => {
|
||
let _ = self.writeln("dbconfig currently ignored");
|
||
}
|
||
Command::ListVfs => {
|
||
let _ = self.writeln("Available VFS modules:");
|
||
self.conn.list_vfs().iter().for_each(|v| {
|
||
let _ = self.writeln(v);
|
||
});
|
||
}
|
||
Command::ListIndexes(args) => {
|
||
if let Err(e) = self.display_indexes(args.tbl_name) {
|
||
let _ = self.writeln(e.to_string());
|
||
}
|
||
}
|
||
Command::Timer(timer_mode) => {
|
||
self.opts.timer = match timer_mode.mode {
|
||
TimerMode::On => true,
|
||
TimerMode::Off => false,
|
||
};
|
||
}
|
||
Command::Headers(headers_mode) => {
|
||
self.opts.headers = match headers_mode.mode {
|
||
HeadersMode::On => true,
|
||
HeadersMode::Off => false,
|
||
};
|
||
}
|
||
Command::Clone(args) => {
|
||
if let Err(e) = self.clone_database(&args.output_file) {
|
||
let _ = self.writeln(e.to_string());
|
||
}
|
||
}
|
||
Command::Manual(args) => {
|
||
let w = self.writer.as_mut().unwrap();
|
||
if let Err(e) = manual::display_manual(args.page.as_deref(), w) {
|
||
let _ = self.writeln(e.to_string());
|
||
}
|
||
}
|
||
},
|
||
}
|
||
}
|
||
|
||
fn print_query_result(
|
||
&mut self,
|
||
sql: &str,
|
||
mut output: Result<Option<Statement>, LimboError>,
|
||
mut statistics: Option<&mut QueryStatistics>,
|
||
) -> anyhow::Result<()> {
|
||
match output {
|
||
Ok(Some(ref mut rows)) => match (self.opts.output_mode, rows.get_query_mode()) {
|
||
(_, QueryMode::ExplainQueryPlan) => {
|
||
struct Entry {
|
||
id: usize,
|
||
detail: String,
|
||
child_prefix: String,
|
||
children: Vec<Entry>,
|
||
}
|
||
|
||
let mut root = Entry {
|
||
id: 0,
|
||
detail: "QUERY PLAN".to_owned(),
|
||
child_prefix: "".to_owned(),
|
||
children: vec![],
|
||
};
|
||
|
||
fn add_children(
|
||
id: usize,
|
||
parent_id: usize,
|
||
detail: String,
|
||
current: &mut Entry,
|
||
) -> bool {
|
||
if current.id == parent_id {
|
||
current.children.push(Entry {
|
||
id,
|
||
detail,
|
||
child_prefix: current.child_prefix.clone() + " ",
|
||
children: vec![],
|
||
});
|
||
if current.children.len() > 1 {
|
||
let idx = current.children.len() - 2;
|
||
current.children[idx].child_prefix =
|
||
current.child_prefix.clone() + "| ";
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
for child in &mut current.children {
|
||
if !add_children(id, parent_id, detail.clone(), child) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
true
|
||
}
|
||
|
||
fn print_entry(app: &mut Limbo, entry: &Entry, prefix: &str) {
|
||
writeln!(app, "{}{}", prefix, entry.detail).unwrap();
|
||
for (i, child) in entry.children.iter().enumerate() {
|
||
let is_last = i == entry.children.len() - 1;
|
||
let child_prefix = format!(
|
||
"{}{}",
|
||
entry.child_prefix,
|
||
if is_last { "`--" } else { "|--" }
|
||
);
|
||
print_entry(app, child, child_prefix.as_str());
|
||
}
|
||
}
|
||
|
||
loop {
|
||
row_step_result_query!(self, sql, rows, statistics, {
|
||
let row = rows.row().unwrap();
|
||
let id: usize = row.get_value(0).as_uint() as usize;
|
||
let parent_id: usize = row.get_value(1).as_uint() as usize;
|
||
let detail = row.get_value(3).to_string();
|
||
add_children(id, parent_id, detail, &mut root);
|
||
});
|
||
}
|
||
|
||
print_entry(self, &root, "");
|
||
}
|
||
(_, QueryMode::Explain) => {
|
||
fn get_explain_indent(
|
||
indent_count: usize,
|
||
curr_insn: &str,
|
||
prev_insn: &str,
|
||
p1: &str,
|
||
unclosed_begin_subrtns: &mut Vec<String>,
|
||
) -> usize {
|
||
let indent_count = match prev_insn {
|
||
"Rewind" | "Last" | "SorterSort" | "SeekGE" | "SeekGT" | "SeekLE"
|
||
| "SeekLT" | "BeginSubrtn" => indent_count + 1,
|
||
_ => indent_count,
|
||
};
|
||
|
||
// The corresponding closing instruction for BeginSubrtn is Return,
|
||
// but Return is also used for other purposes, so we need to track pairs of
|
||
// BeginSubrtn and Return that share the same 1st parameter (the subroutine register).
|
||
if curr_insn == "BeginSubrtn" {
|
||
unclosed_begin_subrtns.push(p1.to_string());
|
||
}
|
||
|
||
match curr_insn {
|
||
"Next" | "SorterNext" | "Prev" => indent_count - 1,
|
||
"Return" => {
|
||
let matching_begin_subrtn =
|
||
unclosed_begin_subrtns.iter().position(|b| b == p1);
|
||
if let Some(matching_begin_subrtn) = matching_begin_subrtn {
|
||
unclosed_begin_subrtns.remove(matching_begin_subrtn);
|
||
indent_count - 1
|
||
} else {
|
||
indent_count
|
||
}
|
||
}
|
||
_ => indent_count,
|
||
}
|
||
}
|
||
|
||
let _ = self.writeln(
|
||
"addr opcode p1 p2 p3 p4 p5 comment",
|
||
);
|
||
let _ = self.writeln(
|
||
"---- ----------------- ---- ---- ---- ------------- -- -------",
|
||
);
|
||
|
||
let mut prev_insn: String = "".to_string();
|
||
let mut indent_count = 0;
|
||
let indent = " ";
|
||
let mut unclosed_begin_subrtns = vec![];
|
||
loop {
|
||
row_step_result_query!(self, sql, rows, statistics, {
|
||
let row = rows.row().unwrap();
|
||
let insn = row.get_value(1).to_string();
|
||
let p1 = row.get_value(2).to_string();
|
||
indent_count = get_explain_indent(
|
||
indent_count,
|
||
&insn,
|
||
&prev_insn,
|
||
&p1,
|
||
&mut unclosed_begin_subrtns,
|
||
);
|
||
let _ = self.writeln(format!(
|
||
"{:<4} {:<17} {:<4} {:<4} {:<4} {:<13} {:<2} {}",
|
||
row.get_value(0).to_string(),
|
||
&(indent.repeat(indent_count) + &insn),
|
||
p1,
|
||
row.get_value(3).to_string(),
|
||
row.get_value(4).to_string(),
|
||
row.get_value(5).to_string(),
|
||
row.get_value(6).to_string(),
|
||
row.get_value(7),
|
||
));
|
||
prev_insn = insn;
|
||
});
|
||
}
|
||
}
|
||
(OutputMode::List, _) => {
|
||
let mut headers_printed = false;
|
||
loop {
|
||
row_step_result_query!(self, sql, rows, statistics, {
|
||
// 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.write(b"|");
|
||
}
|
||
let _ = self.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.write(b"|");
|
||
}
|
||
if matches!(value, Value::Null) {
|
||
self.write_null()?;
|
||
} else {
|
||
write!(self, "{value}")?;
|
||
}
|
||
}
|
||
let _ = self.writeln("");
|
||
});
|
||
}
|
||
}
|
||
(OutputMode::Pretty, _) => {
|
||
let config = self.config.as_ref().unwrap();
|
||
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(config.table.header_color.as_comfy_table_color())
|
||
})
|
||
.collect::<Vec<_>>();
|
||
table.set_header(header);
|
||
}
|
||
loop {
|
||
row_step_result_query!(self, sql, rows, statistics, {
|
||
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 {
|
||
Value::Null => {
|
||
(self.opts.null_value.clone(), CellAlignment::Left)
|
||
}
|
||
Value::Integer(_) => (format!("{value}"), CellAlignment::Right),
|
||
Value::Float(_) => (format!("{value}"), CellAlignment::Right),
|
||
Value::Text(_) => (format!("{value}"), CellAlignment::Left),
|
||
Value::Blob(_) => (format!("{value}"), CellAlignment::Left),
|
||
};
|
||
row.add_cell(
|
||
Cell::new(content)
|
||
.set_alignment(alignment)
|
||
.fg(config.table.column_colors
|
||
[idx % config.table.column_colors.len()]
|
||
.as_comfy_table_color()),
|
||
);
|
||
}
|
||
table.add_row(row);
|
||
});
|
||
}
|
||
|
||
if !table.is_empty() {
|
||
writeln!(self, "{table}")?;
|
||
}
|
||
}
|
||
(OutputMode::Line, _) => {
|
||
let mut first_row_printed = false;
|
||
|
||
let max_width = (0..rows.num_columns())
|
||
.map(|i| rows.get_column_name(i).len())
|
||
.max()
|
||
.unwrap_or(0);
|
||
|
||
let formatted_columns: Vec<String> = (0..rows.num_columns())
|
||
.map(|i| format!("{:>width$}", rows.get_column_name(i), width = max_width))
|
||
.collect();
|
||
|
||
loop {
|
||
row_step_result_query!(self, sql, rows, statistics, {
|
||
let record = rows.row().unwrap();
|
||
|
||
if !first_row_printed {
|
||
first_row_printed = true;
|
||
} else {
|
||
self.writeln("")?;
|
||
}
|
||
|
||
for (i, value) in record.get_values().enumerate() {
|
||
self.write(&formatted_columns[i])?;
|
||
self.write(b" = ")?;
|
||
if matches!(value, Value::Null) {
|
||
self.write_null()?;
|
||
} else {
|
||
write!(self, "{value}")?;
|
||
}
|
||
self.writeln("")?;
|
||
}
|
||
});
|
||
}
|
||
}
|
||
},
|
||
Ok(None) => {}
|
||
Err(err) => {
|
||
let report = miette::Error::from(err).with_source_code(sql.to_owned());
|
||
let _ = self.writeln_fmt(format_args!("{report:?}"));
|
||
anyhow::bail!("We have to throw here, even if we printed error");
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
pub fn init_tracing(opts: &Opts) -> Result<WorkerGuard, std::io::Error> {
|
||
let ((non_blocking, guard), should_emit_ansi) = if let Some(file) = &opts.tracing_output {
|
||
(
|
||
tracing_appender::non_blocking(
|
||
std::fs::File::options()
|
||
.append(true)
|
||
.create(true)
|
||
.open(file)?,
|
||
),
|
||
false,
|
||
)
|
||
} else {
|
||
(
|
||
tracing_appender::non_blocking(std::io::stderr()),
|
||
IsTerminal::is_terminal(&std::io::stderr()),
|
||
)
|
||
};
|
||
// Disable rustyline traces
|
||
if let Err(e) = tracing_subscriber::registry()
|
||
.with(
|
||
tracing_subscriber::fmt::layer()
|
||
.with_writer(non_blocking)
|
||
.with_line_number(true)
|
||
.with_thread_ids(true)
|
||
.with_ansi(should_emit_ansi),
|
||
)
|
||
.with(EnvFilter::from_default_env().add_directive("rustyline=off".parse().unwrap()))
|
||
.try_init()
|
||
{
|
||
println!("Unable to setup tracing appender: {e:?}");
|
||
}
|
||
Ok(guard)
|
||
}
|
||
|
||
fn print_schema_entry(&mut self, db_display_name: &str, row: &turso_core::Row) -> bool {
|
||
if let (Ok(Value::Text(schema)), Ok(Value::Text(obj_type)), Ok(Value::Text(obj_name))) = (
|
||
row.get::<&Value>(0),
|
||
row.get::<&Value>(1),
|
||
row.get::<&Value>(2),
|
||
) {
|
||
let modified_schema = if db_display_name == "main" {
|
||
schema.as_str().to_string()
|
||
} else {
|
||
// We need to modify the SQL to include the database prefix in table names
|
||
// This is a simple approach - for CREATE TABLE statements, insert db name after "TABLE "
|
||
// For CREATE INDEX statements, insert db name after "ON "
|
||
let schema_str = schema.as_str();
|
||
if schema_str.to_uppercase().contains("CREATE TABLE ") {
|
||
// Find "CREATE TABLE " and insert database name after it
|
||
if let Some(pos) = schema_str.to_uppercase().find("CREATE TABLE ") {
|
||
let before = &schema_str[..pos + "CREATE TABLE ".len()];
|
||
let after = &schema_str[pos + "CREATE TABLE ".len()..];
|
||
format!("{before}{db_display_name}.{after}")
|
||
} else {
|
||
schema_str.to_string()
|
||
}
|
||
} else if schema_str.to_uppercase().contains(" ON ") {
|
||
// For indexes, find " ON " and insert database name after it
|
||
if let Some(pos) = schema_str.to_uppercase().find(" ON ") {
|
||
let before = &schema_str[..pos + " ON ".len()];
|
||
let after = &schema_str[pos + " ON ".len()..];
|
||
format!("{before}{db_display_name}.{after}")
|
||
} else {
|
||
schema_str.to_string()
|
||
}
|
||
} else {
|
||
schema_str.to_string()
|
||
}
|
||
};
|
||
let _ = self.writeln_fmt(format_args!("{modified_schema};"));
|
||
// For views, add the column comment like SQLite does
|
||
if obj_type.as_str() == "view" {
|
||
let columns = self
|
||
.get_view_columns(obj_name.as_str())
|
||
.unwrap_or_else(|_| "x".to_string());
|
||
let _ = self.writeln_fmt(format_args!("/* {}({}) */", obj_name.as_str(), columns));
|
||
}
|
||
true
|
||
} else {
|
||
false
|
||
}
|
||
}
|
||
|
||
/// Get column names for a view to generate the SQLite-compatible comment
|
||
fn get_view_columns(&mut self, view_name: &str) -> anyhow::Result<String> {
|
||
// Get column information using PRAGMA table_info
|
||
let pragma_sql = format!("PRAGMA table_info({view_name})");
|
||
|
||
let mut columns = Vec::new();
|
||
let handler = |row: &turso_core::Row| -> anyhow::Result<()> {
|
||
// Column name is in the second column (index 1) of PRAGMA table_info
|
||
if let Ok(Value::Text(col_name)) = row.get::<&Value>(1) {
|
||
columns.push(col_name.as_str().to_string());
|
||
}
|
||
Ok(())
|
||
};
|
||
if let Err(err) = self.handle_row(&pragma_sql, handler) {
|
||
return Err(anyhow::anyhow!(
|
||
"Error retrieving columns for view '{}': {}",
|
||
view_name,
|
||
err
|
||
));
|
||
}
|
||
if columns.is_empty() {
|
||
anyhow::bail!("PRAGMA table_info returned no columns for view '{}'. The view may be corrupted or the database schema is invalid.", view_name);
|
||
}
|
||
Ok(columns.join(","))
|
||
}
|
||
|
||
fn query_one_table_schema(
|
||
&mut self,
|
||
db_prefix: &str,
|
||
db_display_name: &str,
|
||
table_name: &str,
|
||
) -> anyhow::Result<bool> {
|
||
// Yeah, sqlite also has this hardcoded: https://github.com/sqlite/sqlite/blob/31efe5a0f2f80a263457a1fc6524783c0c45769b/src/shell.c.in#L10765
|
||
match table_name {
|
||
"sqlite_master" | "sqlite_schema" | "sqlite_temp_master" | "sqlite_temp_schema" => {
|
||
let schema = format!(
|
||
"CREATE TABLE {table_name} (\n type text,\n name text,\n tbl_name text,\n rootpage integer,\n sql text\n);",
|
||
);
|
||
let _ = self.writeln(&schema);
|
||
return Ok(true);
|
||
}
|
||
_ => {}
|
||
}
|
||
let sql = format!(
|
||
"SELECT sql, type, name FROM {db_prefix}.sqlite_schema WHERE type IN ('table', 'index', 'view') AND (tbl_name = '{table_name}' OR name = '{table_name}') AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '__turso_internal_%' ORDER BY CASE type WHEN 'table' THEN 1 WHEN 'view' THEN 2 WHEN 'index' THEN 3 END, rowid"
|
||
);
|
||
|
||
let mut found = false;
|
||
match self.conn.query(&sql) {
|
||
Ok(Some(ref mut rows)) => loop {
|
||
match rows.step()? {
|
||
StepResult::Row => {
|
||
let row = rows.row().unwrap();
|
||
found |= self.print_schema_entry(db_display_name, row);
|
||
}
|
||
StepResult::IO => {
|
||
rows.run_once()?;
|
||
}
|
||
StepResult::Interrupt => break,
|
||
StepResult::Done => break,
|
||
StepResult::Busy => {
|
||
let _ = self.writeln("database is busy");
|
||
break;
|
||
}
|
||
}
|
||
},
|
||
Ok(None) => {}
|
||
Err(_) => {} // Table not found in this database
|
||
}
|
||
Ok(found)
|
||
}
|
||
|
||
fn query_all_tables_schema(
|
||
&mut self,
|
||
db_prefix: &str,
|
||
db_display_name: &str,
|
||
) -> anyhow::Result<()> {
|
||
let sql = format!("SELECT sql, type, name FROM {db_prefix}.sqlite_schema WHERE type IN ('table', 'index', 'view') AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '__turso_internal_%' ORDER BY CASE type WHEN 'table' THEN 1 WHEN 'view' THEN 2 WHEN 'index' THEN 3 END, rowid");
|
||
|
||
match self.conn.query(&sql) {
|
||
Ok(Some(ref mut rows)) => loop {
|
||
match rows.step()? {
|
||
StepResult::Row => {
|
||
let row = rows.row().unwrap();
|
||
self.print_schema_entry(db_display_name, row);
|
||
}
|
||
StepResult::IO => {
|
||
rows.run_once()?;
|
||
}
|
||
StepResult::Interrupt => break,
|
||
StepResult::Done => break,
|
||
StepResult::Busy => {
|
||
let _ = self.writeln("database is busy");
|
||
break;
|
||
}
|
||
}
|
||
},
|
||
Ok(None) => {}
|
||
Err(err) => {
|
||
// If we can't access this database's schema, just skip it
|
||
if !err.to_string().contains("no such table") {
|
||
eprintln!(
|
||
"Warning: Could not query schema for database '{db_display_name}': {err}"
|
||
);
|
||
}
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
fn display_schema(&mut self, table: Option<&str>) -> anyhow::Result<()> {
|
||
match table {
|
||
Some(table_spec) => {
|
||
// Parse table name to handle database prefixes (e.g., "db.table")
|
||
let clean_table_spec = table_spec.trim_end_matches(';');
|
||
|
||
let (target_db, table_name) =
|
||
if let Some((db, tbl)) = clean_table_spec.split_once('.') {
|
||
(db, tbl)
|
||
} else {
|
||
("main", clean_table_spec)
|
||
};
|
||
|
||
// Query only the specific table in the specific database
|
||
if target_db == "main" {
|
||
self.query_one_table_schema("main", "main", table_name)?;
|
||
} else {
|
||
// Check if the database is attached
|
||
let attached_databases = self.conn.list_attached_databases();
|
||
if attached_databases.contains(&target_db.to_string()) {
|
||
self.query_one_table_schema(target_db, target_db, table_name)?;
|
||
}
|
||
}
|
||
}
|
||
None => {
|
||
// Show schema for all tables in all databases
|
||
let attached_databases = self.conn.list_attached_databases();
|
||
|
||
// Query main database first
|
||
self.query_all_tables_schema("main", "main")?;
|
||
|
||
// Query all attached databases
|
||
for db_name in attached_databases {
|
||
self.query_all_tables_schema(&db_name, &db_name)?;
|
||
}
|
||
}
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
fn display_indexes(&mut self, maybe_table: Option<String>) -> anyhow::Result<()> {
|
||
let mut indexes = String::new();
|
||
|
||
for name in self.database_names()? {
|
||
let prefix = (name != "main").then_some(&name);
|
||
let sql = match maybe_table {
|
||
Some(ref tbl_name) => format!(
|
||
"SELECT name FROM {name}.sqlite_schema WHERE type='index' AND tbl_name = '{tbl_name}' ORDER BY 1"
|
||
),
|
||
None => format!("SELECT name FROM {name}.sqlite_schema WHERE type='index' ORDER BY 1"),
|
||
};
|
||
let handler = |row: &turso_core::Row| -> anyhow::Result<()> {
|
||
if let Ok(Value::Text(idx)) = row.get::<&Value>(0) {
|
||
if let Some(prefix) = prefix {
|
||
indexes.push_str(prefix);
|
||
indexes.push('.');
|
||
}
|
||
indexes.push_str(idx.as_str());
|
||
indexes.push(' ');
|
||
}
|
||
Ok(())
|
||
};
|
||
if let Err(err) = self.handle_row(&sql, handler) {
|
||
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));
|
||
}
|
||
}
|
||
}
|
||
if !indexes.is_empty() {
|
||
let _ = self.writeln(indexes.trim_end().as_bytes());
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
fn display_tables(&mut self, pattern: Option<&str>) -> anyhow::Result<()> {
|
||
let mut tables = String::new();
|
||
|
||
for name in self.database_names()? {
|
||
let prefix = (name != "main").then_some(&name);
|
||
let sql = match pattern {
|
||
Some(pattern) => format!(
|
||
"SELECT name FROM {name}.sqlite_schema WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name LIKE '{pattern}' ORDER BY 1"
|
||
),
|
||
None => format!(
|
||
"SELECT name FROM {name}.sqlite_schema WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY 1"
|
||
),
|
||
};
|
||
let handler = |row: &turso_core::Row| -> anyhow::Result<()> {
|
||
if let Ok(Value::Text(table)) = row.get::<&Value>(0) {
|
||
if let Some(prefix) = prefix {
|
||
tables.push_str(prefix);
|
||
tables.push('.');
|
||
}
|
||
tables.push_str(table.as_str());
|
||
tables.push(' ');
|
||
}
|
||
Ok(())
|
||
};
|
||
if let Err(e) = self.handle_row(&sql, handler) {
|
||
if e.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: {}", e));
|
||
}
|
||
}
|
||
}
|
||
if !tables.is_empty() {
|
||
let _ = self.writeln(tables.trim_end().as_bytes());
|
||
} else if let Some(pattern) = pattern {
|
||
let _ = self.writeln_fmt(format_args!(
|
||
"Error: Tables with pattern '{pattern}' not found."
|
||
));
|
||
} else {
|
||
let _ = self.writeln(b"No tables found in the database.");
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
fn database_names(&mut self) -> anyhow::Result<Vec<String>> {
|
||
let sql = "PRAGMA database_list";
|
||
let mut db_names: Vec<String> = Vec::new();
|
||
let handler = |row: &turso_core::Row| -> anyhow::Result<()> {
|
||
if let Ok(Value::Text(name)) = row.get::<&Value>(1) {
|
||
db_names.push(name.to_string());
|
||
}
|
||
Ok(())
|
||
};
|
||
match self.handle_row(sql, handler) {
|
||
Ok(_) => Ok(db_names),
|
||
Err(e) => Err(anyhow::anyhow!("Error in database list: {}", e)),
|
||
}
|
||
}
|
||
|
||
fn handle_row<F>(&mut self, sql: &str, mut handler: F) -> anyhow::Result<()>
|
||
where
|
||
F: FnMut(&turso_core::Row) -> anyhow::Result<()>,
|
||
{
|
||
match self.conn.query(sql) {
|
||
Ok(Some(ref mut rows)) => loop {
|
||
match rows.step()? {
|
||
StepResult::Row => {
|
||
let row = rows.row().unwrap();
|
||
handler(row)?;
|
||
}
|
||
StepResult::IO => {
|
||
rows.run_once()?;
|
||
}
|
||
StepResult::Interrupt => break,
|
||
StepResult::Done => break,
|
||
StepResult::Busy => {
|
||
let _ = self.writeln("database is busy");
|
||
break;
|
||
}
|
||
}
|
||
},
|
||
Ok(None) => {
|
||
let _ = self.writeln("No results returned from the query.");
|
||
}
|
||
Err(err) => {
|
||
return Err(anyhow::anyhow!("Error querying database: {}", err));
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
fn display_databases(&mut self) -> anyhow::Result<()> {
|
||
let sql = "PRAGMA database_list";
|
||
|
||
match self.conn.query(sql) {
|
||
Ok(Some(ref mut rows)) => {
|
||
loop {
|
||
match rows.step()? {
|
||
StepResult::Row => {
|
||
let row = rows.row().unwrap();
|
||
if let (
|
||
Ok(Value::Integer(seq)),
|
||
Ok(Value::Text(name)),
|
||
Ok(file_value),
|
||
) = (
|
||
row.get::<&Value>(0),
|
||
row.get::<&Value>(1),
|
||
row.get::<&Value>(2),
|
||
) {
|
||
let file = match file_value {
|
||
Value::Text(path) => path.as_str(),
|
||
Value::Null => "",
|
||
_ => "",
|
||
};
|
||
|
||
// Format like SQLite: "main: /path/to/file r/w"
|
||
let file_display = if file.is_empty() {
|
||
"\"\"".to_string()
|
||
} else {
|
||
file.to_string()
|
||
};
|
||
|
||
// Detect readonly mode from connection
|
||
let mode = if self.conn.is_readonly(*seq as usize) {
|
||
"r/o"
|
||
} else {
|
||
"r/w"
|
||
};
|
||
|
||
let _ = self.writeln(format!(
|
||
"{}: {} {}",
|
||
name.as_str(),
|
||
file_display,
|
||
mode
|
||
));
|
||
}
|
||
}
|
||
StepResult::IO => {
|
||
rows.run_once()?;
|
||
}
|
||
StepResult::Interrupt => break,
|
||
StepResult::Done => break,
|
||
StepResult::Busy => {
|
||
let _ = self.writeln("database is busy");
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
Ok(None) => {
|
||
let _ = self.writeln("No results returned from the query.");
|
||
}
|
||
Err(err) => {
|
||
return Err(anyhow::anyhow!("Error querying database list: {}", err));
|
||
}
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
// 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;
|
||
|
||
if let Some(rl) = &mut self.rl {
|
||
let result = rl.readline(&self.prompt)?;
|
||
let _ = self.input_buff.write_str(result.as_str());
|
||
} else {
|
||
let mut reader = std::io::stdin().lock();
|
||
if reader.read_line(&mut self.input_buff)? == 0 {
|
||
return Err(ReadlineError::Eof);
|
||
}
|
||
}
|
||
|
||
let _ = self.input_buff.write_char(' ');
|
||
Ok(())
|
||
}
|
||
|
||
pub fn dump_database_from_conn<W: Write, P: ProgressSink>(
|
||
fk: bool,
|
||
conn: Arc<Connection>,
|
||
out: &mut W,
|
||
mut progress: P,
|
||
) -> anyhow::Result<()> {
|
||
// Snapshot for consistency
|
||
Self::exec_all_conn(&conn, "BEGIN")?;
|
||
// FIXME: we don't yet support PRAGMA foreign_keys=OFF internally,
|
||
// so for now this hacky boolean that decides not to emit it when cloning
|
||
if fk {
|
||
writeln!(out, "PRAGMA foreign_keys=OFF;")?;
|
||
}
|
||
writeln!(out, "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 q_tables = r#"
|
||
SELECT name, sql
|
||
FROM sqlite_schema
|
||
WHERE type='table' AND sql NOT NULL
|
||
ORDER BY tbl_name = 'sqlite_sequence', rowid
|
||
"#;
|
||
if let Some(mut rows) = conn.query(q_tables)? {
|
||
loop {
|
||
match rows.step()? {
|
||
StepResult::Row => {
|
||
let row = rows.row().unwrap();
|
||
let name: &str = row.get::<&str>(0)?;
|
||
// Skip sqlite_sequence table
|
||
if name == "sqlite_sequence" {
|
||
continue;
|
||
}
|
||
let ddl: &str = row.get::<&str>(1)?;
|
||
writeln!(out, "{ddl};")?;
|
||
Self::dump_table_from_conn(&conn, out, name, &mut progress)?;
|
||
progress.on(name);
|
||
}
|
||
StepResult::IO => rows.run_once()?,
|
||
StepResult::Done | StepResult::Interrupt => break,
|
||
StepResult::Busy => anyhow::bail!("database is busy"),
|
||
}
|
||
}
|
||
}
|
||
Self::dump_sqlite_sequence(&conn, out)?;
|
||
Self::dump_schema_objects(&conn, out, &mut progress)?;
|
||
Self::exec_all_conn(&conn, "COMMIT")?;
|
||
writeln!(out, "COMMIT;")?;
|
||
Ok(())
|
||
}
|
||
|
||
fn exec_all_conn(conn: &Arc<Connection>, sql: &str) -> anyhow::Result<()> {
|
||
if let Some(mut rows) = conn.query(sql)? {
|
||
loop {
|
||
match rows.step()? {
|
||
StepResult::IO => rows.run_once()?,
|
||
StepResult::Done | StepResult::Interrupt => break,
|
||
StepResult::Busy => anyhow::bail!("database is busy"),
|
||
StepResult::Row => {}
|
||
}
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
fn dump_table_from_conn<W: Write, P: ProgressSink>(
|
||
conn: &Arc<Connection>,
|
||
out: &mut W,
|
||
table_name: &str,
|
||
progress: &mut P,
|
||
) -> anyhow::Result<()> {
|
||
let pragma = format!("PRAGMA table_info({})", quote_ident(table_name));
|
||
let (mut cols, mut types) = (Vec::new(), Vec::new());
|
||
|
||
if let Some(mut rows) = conn.query(pragma)? {
|
||
loop {
|
||
match rows.step()? {
|
||
StepResult::Row => {
|
||
let row = rows.row().unwrap();
|
||
let ty = row.get::<&str>(2)?.to_string();
|
||
let name = row.get::<&str>(1)?.to_string();
|
||
match ty.as_str() {
|
||
"index" => progress.on(&name),
|
||
"view" => progress.on(&name),
|
||
"trigger" => progress.on(&name),
|
||
_ => {}
|
||
}
|
||
cols.push(name);
|
||
types.push(ty);
|
||
}
|
||
StepResult::IO => rows.run_once()?,
|
||
StepResult::Done | StepResult::Interrupt => break,
|
||
StepResult::Busy => anyhow::bail!("database is busy"),
|
||
}
|
||
}
|
||
}
|
||
// 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
|
||
.iter()
|
||
.map(|c| quote_ident(c))
|
||
.collect::<Vec<_>>()
|
||
.join(", ");
|
||
let select = format!("SELECT {cols_str} FROM {}", quote_ident(table_name));
|
||
if let Some(mut rows) = conn.query(select)? {
|
||
loop {
|
||
match rows.step()? {
|
||
StepResult::Row => {
|
||
let row = rows.row().unwrap();
|
||
write!(out, "INSERT INTO {} VALUES(", quote_ident(table_name))?;
|
||
for i in 0..cols.len() {
|
||
if i > 0 {
|
||
out.write_all(b",")?;
|
||
}
|
||
let v = row.get::<&Value>(i)?;
|
||
Self::write_sql_value_from_value(out, v)?;
|
||
}
|
||
out.write_all(b");\n")?;
|
||
}
|
||
StepResult::IO => rows.run_once()?,
|
||
StepResult::Done | StepResult::Interrupt => break,
|
||
StepResult::Busy => anyhow::bail!("database is busy"),
|
||
}
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
fn dump_sqlite_sequence<W: Write>(conn: &Arc<Connection>, out: &mut W) -> anyhow::Result<()> {
|
||
let mut has_seq = false;
|
||
if let Some(mut rows) =
|
||
conn.query("SELECT 1 FROM sqlite_schema WHERE name='sqlite_sequence' AND type='table'")?
|
||
{
|
||
loop {
|
||
match rows.step()? {
|
||
StepResult::Row => {
|
||
has_seq = true;
|
||
}
|
||
StepResult::IO => rows.run_once()?,
|
||
StepResult::Done | StepResult::Interrupt => break,
|
||
StepResult::Busy => anyhow::bail!("database is busy"),
|
||
}
|
||
}
|
||
}
|
||
if !has_seq {
|
||
return Ok(());
|
||
}
|
||
writeln!(out, "DELETE FROM sqlite_sequence;")?;
|
||
if let Some(mut rows) = conn.query("SELECT name, seq FROM sqlite_sequence")? {
|
||
loop {
|
||
match rows.step()? {
|
||
StepResult::Row => {
|
||
let r = rows.row().unwrap();
|
||
let name = r.get::<&str>(0)?;
|
||
let seq = r.get::<i64>(1)?;
|
||
writeln!(
|
||
out,
|
||
"INSERT INTO sqlite_sequence(name,seq) VALUES({},{});",
|
||
sql_quote_string(name),
|
||
seq
|
||
)?;
|
||
}
|
||
StepResult::IO => rows.run_once()?,
|
||
StepResult::Done | StepResult::Interrupt => break,
|
||
StepResult::Busy => anyhow::bail!("database is busy"),
|
||
}
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
fn dump_schema_objects<W: Write, P: ProgressSink>(
|
||
conn: &Arc<Connection>,
|
||
out: &mut W,
|
||
progress: &mut P,
|
||
) -> anyhow::Result<()> {
|
||
// SQLite’s shell usually emits views after tables.
|
||
// Emit only user objects: sql NOT NULL and name NOT LIKE 'sqlite_%'
|
||
let sql = r#"
|
||
SELECT name, sql FROM sqlite_schema
|
||
WHERE sql NOT NULL
|
||
AND name NOT LIKE 'sqlite_%'
|
||
AND type IN ('index','trigger','view')
|
||
ORDER BY CASE type WHEN 'view' THEN 1 WHEN 'index' THEN 2 WHEN 'trigger' THEN 3 END, rowid
|
||
"#;
|
||
if let Some(mut rows) = conn.query(sql)? {
|
||
loop {
|
||
match rows.step()? {
|
||
StepResult::Row => {
|
||
let row = rows.row().unwrap();
|
||
let ddl: &str = row.get::<&str>(1)?;
|
||
let name: &str = row.get::<&str>(0)?;
|
||
progress.on(name);
|
||
writeln!(out, "{ddl};")?;
|
||
}
|
||
StepResult::IO => rows.run_once()?,
|
||
StepResult::Done | StepResult::Interrupt => break,
|
||
StepResult::Busy => anyhow::bail!("database is busy"),
|
||
}
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
fn write_sql_value_from_value<W: Write>(out: &mut W, v: &Value) -> io::Result<()> {
|
||
match v {
|
||
Value::Null => out.write_all(b"NULL"),
|
||
Value::Integer(i) => out.write_all(format!("{i}").as_bytes()),
|
||
Value::Float(f) => write!(out, "{f}").map(|_| ()),
|
||
Value::Text(s) => {
|
||
out.write_all(b"'")?;
|
||
let bytes = &s.value;
|
||
let mut i = 0;
|
||
while i < bytes.len() {
|
||
let b = bytes[i];
|
||
if b == b'\'' {
|
||
out.write_all(b"''")?;
|
||
} else {
|
||
out.write_all(&[b])?;
|
||
}
|
||
i += 1;
|
||
}
|
||
out.write_all(b"'")
|
||
}
|
||
Value::Blob(b) => {
|
||
out.write_all(b"X'")?;
|
||
const HEX: &[u8; 16] = b"0123456789abcdef";
|
||
for &byte in b {
|
||
out.write_all(&[HEX[(byte >> 4) as usize], HEX[(byte & 0x0F) as usize]])?;
|
||
}
|
||
out.write_all(b"'")
|
||
}
|
||
}
|
||
}
|
||
|
||
fn dump_database(&mut self) -> anyhow::Result<()> {
|
||
// Move writer out so we don’t hold a field-borrow of self during the call.
|
||
let mut out = std::mem::take(&mut self.writer).unwrap();
|
||
let conn = self.conn.clone();
|
||
// dont print progress because it would interfere with piping output of .dump
|
||
let res = Self::dump_database_from_conn(true, conn, &mut out, NoopProgress);
|
||
// Put writer back
|
||
self.writer = Some(out);
|
||
res
|
||
}
|
||
|
||
fn clone_database(&mut self, output_file: &str) -> anyhow::Result<()> {
|
||
use std::path::Path;
|
||
if Path::new(output_file).exists() {
|
||
anyhow::bail!("Refusing to overwrite existing file: {output_file}");
|
||
}
|
||
let io: Arc<dyn turso_core::IO> = Arc::new(turso_core::PlatformIO::new()?);
|
||
let db = Database::open_file(io.clone(), output_file, false, true)?;
|
||
let target = db.connect()?;
|
||
|
||
let mut applier = ApplyWriter::new(&target);
|
||
Self::dump_database_from_conn(false, self.conn.clone(), &mut applier, StderrProgress)?;
|
||
applier.finish()?;
|
||
Ok(())
|
||
}
|
||
|
||
fn save_history(&mut self) {
|
||
if let Some(rl) = &mut self.rl {
|
||
let _ = rl.save_history(HISTORY_FILE.as_path());
|
||
}
|
||
}
|
||
}
|
||
|
||
fn quote_ident(s: &str) -> String {
|
||
let mut out = String::with_capacity(s.len() + 2);
|
||
out.push('"');
|
||
for ch in s.chars() {
|
||
if ch == '"' {
|
||
out.push('"');
|
||
}
|
||
out.push(ch);
|
||
}
|
||
out.push('"');
|
||
out
|
||
}
|
||
|
||
fn sql_quote_string(s: &str) -> String {
|
||
let mut out = String::with_capacity(s.len() + 2);
|
||
out.push('\'');
|
||
for ch in s.chars() {
|
||
if ch == '\'' {
|
||
out.push('\'');
|
||
} // escape as ''
|
||
out.push(ch);
|
||
}
|
||
out.push('\'');
|
||
out
|
||
}
|
||
impl Drop for Limbo {
|
||
fn drop(&mut self) {
|
||
self.save_history();
|
||
unsafe {
|
||
ManuallyDrop::drop(&mut self.input_buff);
|
||
}
|
||
}
|
||
}
|