diff --git a/cli/app.rs b/cli/app.rs index c6be2336b..8aadd48d4 100644 --- a/cli/app.rs +++ b/cli/app.rs @@ -30,7 +30,9 @@ use std::{ use tracing_appender::non_blocking::WorkerGuard; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; -use turso_core::{Connection, Database, LimboError, OpenFlags, Statement, StepResult, Value}; +use turso_core::{ + Connection, Database, LimboError, OpenFlags, QueryMode, Statement, StepResult, Value, +}; #[derive(Parser, Debug)] #[command(name = "Turso")] @@ -452,34 +454,15 @@ impl Limbo { } else { None }; - // TODO this is a quickfix. Some ideas to do case insensitive comparisons is to use - // Uncased or Unicase. - let explain_str = "explain"; - if input - .trim_start() - .get(..explain_str.len()) - .map(|s| s.eq_ignore_ascii_case(explain_str)) - .unwrap_or(false) - { - match self.conn.query(input) { - Ok(Some(stmt)) => { - let _ = self.writeln(stmt.explain().as_bytes()); - } - Err(e) => { - let _ = self.writeln(e.to_string()); - } - _ => {} - } - } else { - let conn = self.conn.clone(); - let runner = conn.query_runner(input.as_bytes()); - for output in runner { - if self - .print_query_result(input, output, stats.as_mut()) - .is_err() - { - break; - } + + 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; } } @@ -756,8 +739,56 @@ impl Limbo { mut statistics: Option<&mut QueryStatistics>, ) -> anyhow::Result<()> { match output { - Ok(Some(ref mut rows)) => match self.opts.output_mode { - OutputMode::List => { + Ok(Some(ref mut rows)) => match (self.opts.output_mode, rows.get_query_mode()) { + (_, QueryMode::Explain) => { + fn get_explain_indent( + indent_count: usize, + curr_insn: &str, + prev_insn: &str, + ) -> usize { + let indent_count = match prev_insn { + "Rewind" | "Last" | "SorterSort" | "SeekGE" | "SeekGT" | "SeekLE" + | "SeekLT" => indent_count + 1, + _ => indent_count, + }; + + match curr_insn { + "Next" | "SorterNext" | "Prev" => indent_count - 1, + _ => 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 = " "; + loop { + row_step_result_query!(self, sql, rows, statistics, { + let row = rows.row().unwrap(); + let insn = row.get_value(1).to_string(); + indent_count = get_explain_indent(indent_count, &insn, &prev_insn); + let _ = self.writeln(format!( + "{:<4} {:<17} {:<4} {:<4} {:<4} {:<13} {:<2} {}", + row.get_value(0).to_string(), + &(indent.repeat(indent_count) + &insn), + row.get_value(2).to_string(), + 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, { @@ -788,7 +819,7 @@ impl Limbo { }); } } - OutputMode::Pretty => { + (OutputMode::Pretty, _) => { let config = self.config.as_ref().unwrap(); let mut table = Table::new(); table @@ -837,7 +868,7 @@ impl Limbo { writeln!(self, "{table}")?; } } - OutputMode::Line => { + (OutputMode::Line, _) => { let mut first_row_printed = false; let max_width = (0..rows.num_columns()) diff --git a/core/lib.rs b/core/lib.rs index 38008a4e2..8a2167e7c 100644 --- a/core/lib.rs +++ b/core/lib.rs @@ -99,8 +99,8 @@ pub use types::RefValue; pub use types::Value; use util::parse_schema_rows; pub use util::IOExt; -use vdbe::builder::QueryMode; use vdbe::builder::TableRefIdCounter; +pub use vdbe::{builder::QueryMode, explain::EXPLAIN_COLUMNS, explain::EXPLAIN_QUERY_PLAN_COLUMNS}; /// Configuration for database features #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -969,21 +969,23 @@ impl Connection { .trim(); self.maybe_update_schema()?; let pager = self.pager.borrow().clone(); - match cmd { - Cmd::Stmt(stmt) => { - let program = translate::translate( - self.schema.borrow().deref(), - stmt, - pager.clone(), - self.clone(), - &syms, - QueryMode::Normal, - input, - )?; - Ok(Statement::new(program, self._db.mv_store.clone(), pager)) - } - _ => unreachable!(), - } + let mode = QueryMode::new(&cmd); + let (Cmd::Stmt(stmt) | Cmd::Explain(stmt) | Cmd::ExplainQueryPlan(stmt)) = cmd; + let program = translate::translate( + self.schema.borrow().deref(), + stmt, + pager.clone(), + self.clone(), + &syms, + mode, + input, + )?; + Ok(Statement::new( + program, + self._db.mv_store.clone(), + pager, + mode, + )) } /// Parse schema from scratch if version of schema for the connection differs from the schema cookie in the root page @@ -1117,23 +1119,19 @@ impl Connection { let input = str::from_utf8(&sql.as_bytes()[..byte_offset_end]) .unwrap() .trim(); - match cmd { - Cmd::Stmt(stmt) => { - let program = translate::translate( - self.schema.borrow().deref(), - stmt, - pager.clone(), - self.clone(), - &syms, - QueryMode::Normal, - input, - )?; - - Statement::new(program, self._db.mv_store.clone(), pager.clone()) - .run_ignore_rows()?; - } - _ => unreachable!(), - } + let mode = QueryMode::new(&cmd); + let (Cmd::Stmt(stmt) | Cmd::Explain(stmt) | Cmd::ExplainQueryPlan(stmt)) = cmd; + let program = translate::translate( + self.schema.borrow().deref(), + stmt, + pager.clone(), + self.clone(), + &syms, + mode, + input, + )?; + Statement::new(program, self._db.mv_store.clone(), pager.clone(), mode) + .run_ignore_rows()?; } Ok(()) } @@ -1169,6 +1167,7 @@ impl Connection { } let syms = self.syms.borrow(); let pager = self.pager.borrow().clone(); + let mode = QueryMode::new(&cmd); match cmd { Cmd::Stmt(ref stmt) | Cmd::Explain(ref stmt) => { let program = translate::translate( @@ -1177,14 +1176,16 @@ impl Connection { pager.clone(), self.clone(), &syms, - cmd.into(), + mode, input, )?; - let stmt = Statement::new(program, self._db.mv_store.clone(), pager); + let stmt = Statement::new(program, self._db.mv_store.clone(), pager, mode); Ok(Some(stmt)) } Cmd::ExplainQueryPlan(stmt) => { let mut table_ref_counter = TableRefIdCounter::new(); + + // TODO: we need OP_Explain match stmt { ast::Stmt::Select(select) => { let mut plan = prepare_select_plan( @@ -1227,35 +1228,19 @@ impl Connection { let input = str::from_utf8(&sql.as_bytes()[..byte_offset_end]) .unwrap() .trim(); - match cmd { - Cmd::Explain(stmt) => { - let program = translate::translate( - self.schema.borrow().deref(), - stmt, - pager, - self.clone(), - &syms, - QueryMode::Explain, - input, - )?; - let _ = std::io::stdout().write_all(program.explain().as_bytes()); - } - Cmd::ExplainQueryPlan(_stmt) => todo!(), - Cmd::Stmt(stmt) => { - let program = translate::translate( - self.schema.borrow().deref(), - stmt, - pager.clone(), - self.clone(), - &syms, - QueryMode::Normal, - input, - )?; - - Statement::new(program, self._db.mv_store.clone(), pager.clone()) - .run_ignore_rows()?; - } - } + let mode = QueryMode::new(&cmd); + let (Cmd::Stmt(stmt) | Cmd::Explain(stmt) | Cmd::ExplainQueryPlan(stmt)) = cmd; + let program = translate::translate( + self.schema.borrow().deref(), + stmt, + pager.clone(), + self.clone(), + &syms, + mode, + input, + )?; + Statement::new(program, self._db.mv_store.clone(), pager.clone(), mode) + .run_ignore_rows()?; } Ok(()) } @@ -2090,20 +2075,39 @@ pub struct Statement { /// Used to determine whether we need to check for schema changes when /// starting a transaction. accesses_db: bool, + + /// indicates if the statement is a NORMAL/EXPLAIN/EXPLAIN QUERY PLAN + query_mode: QueryMode, } impl Statement { - pub fn new(program: vdbe::Program, mv_store: Option>, pager: Rc) -> Self { + pub fn new( + program: vdbe::Program, + mv_store: Option>, + pager: Rc, + query_mode: QueryMode, + ) -> Self { let accesses_db = program.accesses_db; - let state = vdbe::ProgramState::new(program.max_registers, program.cursor_ref.len()); + let state = vdbe::ProgramState::new( + match query_mode { + QueryMode::Normal => program.max_registers, + QueryMode::Explain => EXPLAIN_COLUMNS.len(), + QueryMode::ExplainQueryPlan => EXPLAIN_QUERY_PLAN_COLUMNS.len(), + }, + program.cursor_ref.len(), + ); Self { program, state, mv_store, pager, accesses_db, + query_mode, } } + pub fn get_query_mode(&self) -> QueryMode { + self.query_mode + } pub fn n_change(&self) -> i64 { self.program.n_change.get() @@ -2119,13 +2123,20 @@ impl Statement { pub fn step(&mut self) -> Result { let res = if !self.accesses_db { - self.program - .step(&mut self.state, self.mv_store.clone(), self.pager.clone()) + self.program.step( + &mut self.state, + self.mv_store.clone(), + self.pager.clone(), + self.query_mode, + ) } else { const MAX_SCHEMA_RETRY: usize = 50; - let mut res = - self.program - .step(&mut self.state, self.mv_store.clone(), self.pager.clone()); + let mut res = self.program.step( + &mut self.state, + self.mv_store.clone(), + self.pager.clone(), + self.query_mode, + ); for attempt in 0..MAX_SCHEMA_RETRY { // Only reprepare if we still need to update schema if !matches!(res, Err(LimboError::SchemaUpdated)) { @@ -2133,9 +2144,12 @@ impl Statement { } tracing::debug!("reprepare: attempt={}", attempt); self.reprepare()?; - res = self - .program - .step(&mut self.state, self.mv_store.clone(), self.pager.clone()); + res = self.program.step( + &mut self.state, + self.mv_store.clone(), + self.pager.clone(), + self.query_mode, + ); } res }; @@ -2190,20 +2204,18 @@ impl Statement { let cmd = cmd.expect("Same SQL string should be able to be parsed"); let syms = conn.syms.borrow(); - - match cmd { - Cmd::Stmt(stmt) => translate::translate( - conn.schema.borrow().deref(), - stmt, - self.pager.clone(), - conn.clone(), - &syms, - QueryMode::Normal, - &self.program.sql, - )?, - Cmd::Explain(_stmt) => todo!(), - Cmd::ExplainQueryPlan(_stmt) => todo!(), - } + let mode = self.query_mode; + debug_assert_eq!(QueryMode::new(&cmd), mode,); + let (Cmd::Stmt(stmt) | Cmd::Explain(stmt) | Cmd::ExplainQueryPlan(stmt)) = cmd; + translate::translate( + conn.schema.borrow().deref(), + stmt, + self.pager.clone(), + conn.clone(), + &syms, + mode, + &self.program.sql, + )? }; // Save parameters before they are reset let parameters = std::mem::take(&mut self.state.parameters); @@ -2239,14 +2251,24 @@ impl Statement { } pub fn num_columns(&self) -> usize { - self.program.result_columns.len() + match self.query_mode { + QueryMode::Normal => self.program.result_columns.len(), + QueryMode::Explain => EXPLAIN_COLUMNS.len(), + QueryMode::ExplainQueryPlan => EXPLAIN_QUERY_PLAN_COLUMNS.len(), + } } pub fn get_column_name(&self, idx: usize) -> Cow { - let column = &self.program.result_columns.get(idx).expect("No column"); - match column.name(&self.program.table_references) { - Some(name) => Cow::Borrowed(name), - None => Cow::Owned(column.expr.to_string()), + match self.query_mode { + QueryMode::Normal => { + let column = &self.program.result_columns.get(idx).expect("No column"); + match column.name(&self.program.table_references) { + Some(name) => Cow::Borrowed(name), + None => Cow::Owned(column.expr.to_string()), + } + } + QueryMode::Explain => Cow::Borrowed(EXPLAIN_COLUMNS[idx]), + QueryMode::ExplainQueryPlan => Cow::Borrowed(EXPLAIN_QUERY_PLAN_COLUMNS[idx]), } } @@ -2303,10 +2325,6 @@ impl Statement { pub fn row(&self) -> Option<&Row> { self.state.result_row.as_ref() } - - pub fn explain(&self) -> String { - self.program.explain() - } } pub type Row = vdbe::Row; diff --git a/core/vdbe/builder.rs b/core/vdbe/builder.rs index ffa26c03d..059ce90a0 100644 --- a/core/vdbe/builder.rs +++ b/core/vdbe/builder.rs @@ -139,13 +139,15 @@ impl CursorType { pub enum QueryMode { Normal, Explain, + ExplainQueryPlan, } -impl From for QueryMode { - fn from(stmt: ast::Cmd) -> Self { - match stmt { - ast::Cmd::ExplainQueryPlan(_) | ast::Cmd::Explain(_) => QueryMode::Explain, - _ => QueryMode::Normal, +impl QueryMode { + pub fn new(cmd: &ast::Cmd) -> Self { + match cmd { + ast::Cmd::ExplainQueryPlan(_) => QueryMode::ExplainQueryPlan, + ast::Cmd::Explain(_) => QueryMode::Explain, + ast::Cmd::Stmt(_) => QueryMode::Normal, } } } @@ -171,7 +173,7 @@ impl ProgramBuilder { constant_spans: Vec::new(), label_to_resolved_offset: Vec::with_capacity(opts.approx_num_labels), seekrowid_emitted_bitmask: 0, - comments: if query_mode == QueryMode::Explain { + comments: if let QueryMode::Explain | QueryMode::ExplainQueryPlan = query_mode { Some(Vec::new()) } else { None diff --git a/core/vdbe/explain.rs b/core/vdbe/explain.rs index 6c850aadd..7d88fd8e4 100644 --- a/core/vdbe/explain.rs +++ b/core/vdbe/explain.rs @@ -5,13 +5,13 @@ use crate::vdbe::{builder::CursorType, insn::RegisterOrLiteral}; use super::{Insn, InsnReference, Program, Value}; use crate::function::{Func, ScalarFunc}; -pub fn insn_to_str( +pub const EXPLAIN_COLUMNS: [&str; 8] = ["addr", "opcode", "p1", "p2", "p3", "p4", "p5", "comment"]; +pub const EXPLAIN_QUERY_PLAN_COLUMNS: [&str; 4] = ["id", "parent", "notused", "detail"]; + +pub fn insn_to_row( program: &Program, - addr: InsnReference, insn: &Insn, - indent: String, - manual_comment: Option<&'static str>, -) -> String { +) -> (&'static str, i32, i32, i32, Value, u16, String) { let get_table_or_index_name = |cursor_id: usize| { let cursor_type = &program.cursor_ref[cursor_id].1; match cursor_type { @@ -23,8 +23,7 @@ pub fn insn_to_str( CursorType::Sorter => "sorter", } }; - let (opcode, p1, p2, p3, p4, p5, comment): (&str, i32, i32, i32, Value, u16, String) = - match insn { + match insn { Insn::Init { target_pc } => ( "Init", 0, @@ -1724,7 +1723,34 @@ pub fn insn_to_str( 0, format!("if (r[{}] < 0) goto {}", reg, target_pc.as_debug_int()), ), - }; + } +} + +pub fn insn_to_row_with_comment( + program: &Program, + insn: &Insn, + manual_comment: Option<&str>, +) -> (&'static str, i32, i32, i32, Value, u16, String) { + let (opcode, p1, p2, p3, p4, p5, comment) = insn_to_row(program, insn); + ( + opcode, + p1, + p2, + p3, + p4, + p5, + manual_comment.map_or(comment.to_string(), |mc| format!("{comment}; {mc}")), + ) +} + +pub fn insn_to_str( + program: &Program, + addr: InsnReference, + insn: &Insn, + indent: String, + manual_comment: Option<&str>, +) -> String { + let (opcode, p1, p2, p3, p4, p5, comment) = insn_to_row(program, insn); format!( "{:<4} {:<17} {:<4} {:<4} {:<4} {:<13} {:<2} {}", addr, diff --git a/core/vdbe/mod.rs b/core/vdbe/mod.rs index c5e4e91a8..6f28fe745 100644 --- a/core/vdbe/mod.rs +++ b/core/vdbe/mod.rs @@ -53,12 +53,13 @@ use crate::{ #[cfg(feature = "json")] use crate::json::JsonCacheCell; use crate::{Connection, MvStore, Result, TransactionState}; -use builder::CursorKey; +use builder::{CursorKey, QueryMode}; use execute::{ InsnFunction, InsnFunctionStepResult, OpIdxDeleteState, OpIntegrityCheckState, OpOpenEphemeralState, }; +use explain::{insn_to_row_with_comment, EXPLAIN_COLUMNS, EXPLAIN_QUERY_PLAN_COLUMNS}; use regex::Regex; use std::{cell::Cell, collections::HashMap, num::NonZero, rc::Rc, sync::Arc}; use tracing::{instrument, Level}; @@ -465,12 +466,92 @@ impl Program { self.connection.get_pager_from_database_index(idx) } - #[instrument(skip_all, level = Level::DEBUG)] pub fn step( &self, state: &mut ProgramState, mv_store: Option>, pager: Rc, + query_mode: QueryMode, + ) -> Result { + match query_mode { + QueryMode::Normal => self.normal_step(state, mv_store, pager), + QueryMode::Explain => self.explain_step(state, mv_store, pager), + QueryMode::ExplainQueryPlan => self.explain_query_plan_step(state, mv_store, pager), + } + } + + fn explain_step( + &self, + state: &mut ProgramState, + _mv_store: Option>, + pager: Rc, + ) -> Result { + debug_assert!(state.column_count() == EXPLAIN_COLUMNS.len()); + if self.connection.closed.get() { + // Connection is closed for whatever reason, rollback the transaction. + let state = self.connection.transaction_state.get(); + if let TransactionState::Write { .. } = state { + pager.io.block(|| pager.end_tx(true, &self.connection))?; + } + return Err(LimboError::InternalError("Connection closed".to_string())); + } + + if state.is_interrupted() { + return Ok(StepResult::Interrupt); + } + + // FIXME: do we need this? + state.metrics.vm_steps = state.metrics.vm_steps.saturating_add(1); + + if state.pc as usize >= self.insns.len() { + return Ok(StepResult::Done); + } + + let (current_insn, _) = &self.insns[state.pc as usize]; + let (opcode, p1, p2, p3, p4, p5, comment) = insn_to_row_with_comment( + self, + current_insn, + self.comments.as_ref().and_then(|comments| { + comments + .iter() + .find(|(offset, _)| *offset == state.pc) + .map(|(_, comment)| comment) + .copied() + }), + ); + + state.registers[0] = Register::Value(Value::Integer(state.pc as i64)); + state.registers[1] = Register::Value(Value::from_text(opcode)); + state.registers[2] = Register::Value(Value::Integer(p1 as i64)); + state.registers[3] = Register::Value(Value::Integer(p2 as i64)); + state.registers[4] = Register::Value(Value::Integer(p3 as i64)); + state.registers[5] = Register::Value(p4); + state.registers[6] = Register::Value(Value::Integer(p5 as i64)); + state.registers[7] = Register::Value(Value::from_text(&comment)); + state.result_row = Some(Row { + values: &state.registers[0] as *const Register, + count: EXPLAIN_COLUMNS.len(), + }); + state.pc += 1; + Ok(StepResult::Row) + } + + fn explain_query_plan_step( + &self, + state: &mut ProgramState, + _mv_store: Option>, + _pager: Rc, + ) -> Result { + debug_assert!(state.column_count() == EXPLAIN_QUERY_PLAN_COLUMNS.len()); + todo!("we need OP_Explain to be implemented first") + } + + #[instrument(skip_all, level = Level::DEBUG)] + fn normal_step( + &self, + state: &mut ProgramState, + mv_store: Option>, + pager: Rc, ) -> Result { let enable_tracing = tracing::enabled!(tracing::Level::TRACE); loop { @@ -787,27 +868,6 @@ impl Program { } } } - - #[rustfmt::skip] - pub fn explain(&self) -> String { - let mut buff = String::with_capacity(1024); - buff.push_str("addr opcode p1 p2 p3 p4 p5 comment\n"); - buff.push_str("---- ----------------- ---- ---- ---- ------------- -- -------\n"); - let indent = " "; - let indent_counts = get_indent_counts(&self.insns); - for (addr, (insn, _)) in self.insns.iter().enumerate() { - let indent_count = indent_counts[addr]; - print_insn( - self, - addr as InsnReference, - insn, - indent.repeat(indent_count), - &mut buff, - ); - buff.push('\n'); - } - buff - } } fn make_record(registers: &[Register], start_reg: &usize, count: &usize) -> ImmutableRecord { @@ -852,82 +912,6 @@ fn trace_insn(program: &Program, addr: InsnReference, insn: &Insn) { ); } -fn print_insn(program: &Program, addr: InsnReference, insn: &Insn, indent: String, w: &mut String) { - let s = explain::insn_to_str( - program, - addr, - insn, - indent, - program.comments.as_ref().and_then(|comments| { - comments - .iter() - .find(|(offset, _)| *offset == addr) - .map(|(_, comment)| comment) - .copied() - }), - ); - w.push_str(&s); -} - -// The indenting rules are(from SQLite): -// -// * For each "Next", "Prev", "VNext" or "VPrev" instruction, increase the ident number for -// all opcodes that occur between the p2 jump destination and the opcode itself. -// -// * Do the previous for "Return" instructions for when P2 is positive. -// -// * For each "Goto", if the jump destination is earlier in the program and ends on one of: -// Yield SeekGt SeekLt RowSetRead Rewind -// or if the P1 parameter is one instead of zero, then increase the indent number for all -// opcodes between the earlier instruction and "Goto" -fn get_indent_counts(insns: &[(Insn, InsnFunction)]) -> Vec { - let mut indents = vec![0; insns.len()]; - - for (i, (insn, _)) in insns.iter().enumerate() { - let mut start = 0; - let mut end = 0; - match insn { - Insn::Next { pc_if_next, .. } | Insn::VNext { pc_if_next, .. } => { - let dest = pc_if_next.as_debug_int() as usize; - if dest < i { - start = dest; - end = i; - } - } - Insn::Prev { pc_if_prev, .. } => { - let dest = pc_if_prev.as_debug_int() as usize; - if dest < i { - start = dest; - end = i; - } - } - - Insn::Goto { target_pc } => { - let dest = target_pc.as_debug_int() as usize; - if dest < i - && matches!( - insns.get(dest).map(|(insn, _)| insn), - Some(Insn::Yield { .. }) - | Some(Insn::SeekGT { .. }) - | Some(Insn::SeekLT { .. }) - | Some(Insn::Rewind { .. }) - ) - { - start = dest; - end = i; - } - } - - _ => {} - } - for indent in indents.iter_mut().take(end).skip(start) { - *indent += 1; - } - } - - indents -} - pub trait FromValueRow<'a> { fn from_value(value: &'a Value) -> Result where