mirror of
https://github.com/aljazceru/turso.git
synced 2026-02-03 07:14:33 +01:00
Merge 'Handle EXPLAIN QUERY PLAN like SQLite' from Lâm Hoàng Phúc
After this PR:
```
turso> EXPLAIN QUERY PLAN SELECT 1;
QUERY PLAN
`--SCAN CONSTANT ROW
turso> EXPLAIN QUERY PLAN SELECT 1 UNION SELECT 1;
QUERY PLAN
`--COMPOUND QUERY
|--LEFT-MOST SUBQUERY
| `--SCAN CONSTANT ROW
`--UNION USING TEMP B-TREE
`--SCAN CONSTANT ROW
turso> CREATE TABLE x(y);
turso> CREATE TABLE z(y);
turso> EXPLAIN QUERY PLAN SELECT * from x,z;
QUERY PLAN
|--SCAN x
`--SCAN z
turso> EXPLAIN QUERY PLAN SELECT * from x,z ON x.y = z.y;
QUERY PLAN
|--SCAN x
`--SEARCH z USING INDEX ephemeral_z_t2
turso>
```
Closes #3057
This commit is contained in:
71
cli/app.rs
71
cli/app.rs
@@ -740,6 +740,77 @@ impl Limbo {
|
||||
) -> 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,
|
||||
|
||||
82
core/lib.rs
82
core/lib.rs
@@ -41,7 +41,6 @@ mod numeric;
|
||||
|
||||
use crate::incremental::view::AllViewsTxState;
|
||||
use crate::storage::encryption::CipherMode;
|
||||
use crate::translate::optimizer::optimize_plan;
|
||||
use crate::translate::pragma::TURSO_CDC_DEFAULT_TABLE_NAME;
|
||||
#[cfg(all(feature = "fs", feature = "conn_raw_api"))]
|
||||
use crate::types::{WalFrameInfo, WalState};
|
||||
@@ -67,7 +66,6 @@ use std::{
|
||||
cell::{Cell, RefCell},
|
||||
collections::HashMap,
|
||||
fmt::{self, Display},
|
||||
io::Write,
|
||||
num::NonZero,
|
||||
ops::Deref,
|
||||
rc::Rc,
|
||||
@@ -91,7 +89,6 @@ pub use storage::{
|
||||
wal::{CheckpointMode, CheckpointResult, Wal, WalFile, WalFileShared},
|
||||
};
|
||||
use tracing::{instrument, Level};
|
||||
use translate::select::prepare_select_plan;
|
||||
use turso_macros::match_ignore_ascii_case;
|
||||
use turso_parser::{ast, ast::Cmd, parser::Parser};
|
||||
use types::IOResult;
|
||||
@@ -99,7 +96,6 @@ pub use types::RefValue;
|
||||
pub use types::Value;
|
||||
use util::parse_schema_rows;
|
||||
pub use util::IOExt;
|
||||
use vdbe::builder::TableRefIdCounter;
|
||||
pub use vdbe::{builder::QueryMode, explain::EXPLAIN_COLUMNS, explain::EXPLAIN_QUERY_PLAN_COLUMNS};
|
||||
|
||||
/// Configuration for database features
|
||||
@@ -1168,43 +1164,18 @@ 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(
|
||||
self.schema.borrow().deref(),
|
||||
stmt.clone(),
|
||||
pager.clone(),
|
||||
self.clone(),
|
||||
&syms,
|
||||
mode,
|
||||
input,
|
||||
)?;
|
||||
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(
|
||||
self.schema.borrow().deref(),
|
||||
select,
|
||||
&syms,
|
||||
&[],
|
||||
&mut table_ref_counter,
|
||||
translate::plan::QueryDestination::ResultRows,
|
||||
&self.clone(),
|
||||
)?;
|
||||
optimize_plan(&mut plan, self.schema.borrow().deref())?;
|
||||
let _ = std::io::stdout().write_all(plan.to_string().as_bytes());
|
||||
}
|
||||
_ => todo!(),
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
let (Cmd::Stmt(stmt) | Cmd::Explain(stmt) | Cmd::ExplainQueryPlan(stmt)) = cmd;
|
||||
let program = translate::translate(
|
||||
self.schema.borrow().deref(),
|
||||
stmt.clone(),
|
||||
pager.clone(),
|
||||
self.clone(),
|
||||
&syms,
|
||||
mode,
|
||||
input,
|
||||
)?;
|
||||
let stmt = Statement::new(program, self._db.mv_store.clone(), pager, mode);
|
||||
Ok(Some(stmt))
|
||||
}
|
||||
|
||||
pub fn query_runner<'a>(self: &'a Arc<Connection>, sql: &'a [u8]) -> QueryRunner<'a> {
|
||||
@@ -2075,7 +2046,6 @@ 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,
|
||||
/// Flag to show if the statement was busy
|
||||
@@ -2090,14 +2060,12 @@ impl Statement {
|
||||
query_mode: QueryMode,
|
||||
) -> Self {
|
||||
let accesses_db = program.accesses_db;
|
||||
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(),
|
||||
);
|
||||
let (max_registers, cursor_count) = match query_mode {
|
||||
QueryMode::Normal => (program.max_registers, program.cursor_ref.len()),
|
||||
QueryMode::Explain => (EXPLAIN_COLUMNS.len(), 0),
|
||||
QueryMode::ExplainQueryPlan => (EXPLAIN_QUERY_PLAN_COLUMNS.len(), 0),
|
||||
};
|
||||
let state = vdbe::ProgramState::new(max_registers, cursor_count);
|
||||
Self {
|
||||
program,
|
||||
state,
|
||||
@@ -2225,14 +2193,12 @@ impl Statement {
|
||||
};
|
||||
// Save parameters before they are reset
|
||||
let parameters = std::mem::take(&mut self.state.parameters);
|
||||
self._reset(
|
||||
Some(match self.query_mode {
|
||||
QueryMode::Normal => self.program.max_registers,
|
||||
QueryMode::Explain => EXPLAIN_COLUMNS.len(),
|
||||
QueryMode::ExplainQueryPlan => EXPLAIN_QUERY_PLAN_COLUMNS.len(),
|
||||
}),
|
||||
Some(self.program.cursor_ref.len()),
|
||||
);
|
||||
let (max_registers, cursor_count) = match self.query_mode {
|
||||
QueryMode::Normal => (self.program.max_registers, self.program.cursor_ref.len()),
|
||||
QueryMode::Explain => (EXPLAIN_COLUMNS.len(), 0),
|
||||
QueryMode::ExplainQueryPlan => (EXPLAIN_QUERY_PLAN_COLUMNS.len(), 0),
|
||||
};
|
||||
self._reset(Some(max_registers), Some(cursor_count));
|
||||
// Load the parameters back into the state
|
||||
self.state.parameters = parameters;
|
||||
Ok(())
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::translate::result_row::try_fold_expr_to_i64;
|
||||
use crate::vdbe::builder::{CursorType, ProgramBuilder};
|
||||
use crate::vdbe::insn::Insn;
|
||||
use crate::vdbe::BranchOffset;
|
||||
use crate::SymbolTable;
|
||||
use crate::{emit_explain, QueryMode, SymbolTable};
|
||||
use std::sync::Arc;
|
||||
use tracing::instrument;
|
||||
use turso_parser::ast::{CompoundOperator, SortOrder};
|
||||
@@ -98,6 +98,7 @@ pub fn emit_program_for_compound_select(
|
||||
_ => (None, None),
|
||||
};
|
||||
|
||||
emit_explain!(program, true, "COMPOUND QUERY".to_owned());
|
||||
emit_compound_select(
|
||||
program,
|
||||
plan,
|
||||
@@ -108,6 +109,7 @@ pub fn emit_program_for_compound_select(
|
||||
yield_reg,
|
||||
reg_result_cols_start,
|
||||
)?;
|
||||
program.pop_current_parent_explain();
|
||||
|
||||
program.result_columns = right_plan.result_columns;
|
||||
program.table_references.extend(right_plan.table_references);
|
||||
@@ -187,7 +189,10 @@ fn emit_compound_select(
|
||||
right_most.offset = offset;
|
||||
right_most_ctx.reg_offset = offset_reg;
|
||||
}
|
||||
|
||||
emit_explain!(program, true, "UNION ALL".to_owned());
|
||||
emit_query(program, &mut right_most, &mut right_most_ctx)?;
|
||||
program.pop_current_parent_explain();
|
||||
program.preassign_label_to_next_insn(label_next_select);
|
||||
}
|
||||
CompoundOperator::Union => {
|
||||
@@ -229,7 +234,10 @@ fn emit_compound_select(
|
||||
index: dedupe_index.1.clone(),
|
||||
is_delete: false,
|
||||
};
|
||||
|
||||
emit_explain!(program, true, "UNION USING TEMP B-TREE".to_owned());
|
||||
emit_query(program, &mut right_most, &mut right_most_ctx)?;
|
||||
program.pop_current_parent_explain();
|
||||
|
||||
if new_dedupe_index {
|
||||
read_deduplicated_union_or_except_rows(
|
||||
@@ -282,7 +290,9 @@ fn emit_compound_select(
|
||||
index: right_index,
|
||||
is_delete: false,
|
||||
};
|
||||
emit_explain!(program, true, "INTERSECT USING TEMP B-TREE".to_owned());
|
||||
emit_query(program, &mut right_most, &mut right_most_ctx)?;
|
||||
program.pop_current_parent_explain();
|
||||
read_intersect_rows(
|
||||
program,
|
||||
left_cursor_id,
|
||||
@@ -332,7 +342,9 @@ fn emit_compound_select(
|
||||
index: index.clone(),
|
||||
is_delete: true,
|
||||
};
|
||||
emit_explain!(program, true, "EXCEPT USING TEMP B-TREE".to_owned());
|
||||
emit_query(program, &mut right_most, &mut right_most_ctx)?;
|
||||
program.pop_current_parent_explain();
|
||||
if new_index {
|
||||
read_deduplicated_union_or_except_rows(
|
||||
program, cursor_id, &index, limit_ctx, offset_reg, yield_reg,
|
||||
@@ -349,7 +361,9 @@ fn emit_compound_select(
|
||||
right_most.offset = offset;
|
||||
right_most_ctx.reg_offset = offset_reg;
|
||||
}
|
||||
emit_explain!(program, true, "LEFT-MOST SUBQUERY".to_owned());
|
||||
emit_query(program, &mut right_most, &mut right_most_ctx)?;
|
||||
program.pop_current_parent_explain();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use turso_parser::ast::{self, SortOrder};
|
||||
|
||||
use crate::{
|
||||
emit_explain,
|
||||
schema::PseudoCursorType,
|
||||
translate::collate::CollationSeq,
|
||||
util::exprs_are_equivalent,
|
||||
@@ -8,7 +9,7 @@ use crate::{
|
||||
builder::{CursorType, ProgramBuilder},
|
||||
insn::Insn,
|
||||
},
|
||||
Result,
|
||||
QueryMode, Result,
|
||||
};
|
||||
|
||||
use super::{
|
||||
@@ -101,6 +102,10 @@ pub fn emit_order_by(
|
||||
let sorter_column_count =
|
||||
order_by.len() + remappings.iter().filter(|r| !r.deduplicated).count();
|
||||
|
||||
// TODO: we need to know how many indices used for sorting
|
||||
// to emit correct explain output.
|
||||
emit_explain!(program, false, "USE TEMP B-TREE FOR ORDER BY".to_owned());
|
||||
|
||||
let pseudo_cursor = program.alloc_cursor_id(CursorType::Pseudo(PseudoCursorType {
|
||||
column_count: sorter_column_count,
|
||||
}));
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
use crate::{
|
||||
emit_explain,
|
||||
schema::Table,
|
||||
vdbe::{builder::ProgramBuilder, insn::Insn},
|
||||
Result,
|
||||
QueryMode, Result,
|
||||
};
|
||||
|
||||
use super::{
|
||||
emitter::{emit_query, Resolver, TranslateCtx},
|
||||
main_loop::LoopLabels,
|
||||
plan::{QueryDestination, SelectPlan, TableReferences},
|
||||
plan::{Operation, QueryDestination, Search, SelectPlan, TableReferences},
|
||||
};
|
||||
|
||||
/// Emit the subqueries contained in the FROM clause.
|
||||
@@ -17,7 +18,45 @@ pub fn emit_subqueries(
|
||||
t_ctx: &mut TranslateCtx,
|
||||
tables: &mut TableReferences,
|
||||
) -> Result<()> {
|
||||
if tables.joined_tables().is_empty() {
|
||||
emit_explain!(program, false, "SCAN CONSTANT ROW".to_owned());
|
||||
}
|
||||
|
||||
for table_reference in tables.joined_tables_mut() {
|
||||
emit_explain!(
|
||||
program,
|
||||
true,
|
||||
match &table_reference.op {
|
||||
Operation::Scan { .. } => {
|
||||
if table_reference.table.get_name() == table_reference.identifier {
|
||||
format!("SCAN {}", table_reference.identifier)
|
||||
} else {
|
||||
format!(
|
||||
"SCAN {} AS {}",
|
||||
table_reference.table.get_name(),
|
||||
table_reference.identifier
|
||||
)
|
||||
}
|
||||
}
|
||||
Operation::Search(search) => match search {
|
||||
Search::RowidEq { .. } | Search::Seek { index: None, .. } => {
|
||||
format!(
|
||||
"SEARCH {} USING INTEGER PRIMARY KEY (rowid=?)",
|
||||
table_reference.identifier
|
||||
)
|
||||
}
|
||||
Search::Seek {
|
||||
index: Some(index), ..
|
||||
} => {
|
||||
format!(
|
||||
"SEARCH {} USING INDEX {}",
|
||||
table_reference.identifier, index.name
|
||||
)
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if let Table::FromClauseSubquery(from_clause_subquery) = &mut table_reference.table {
|
||||
// Emit the subquery and get the start register of the result columns.
|
||||
let result_columns_start =
|
||||
@@ -27,6 +66,8 @@ pub fn emit_subqueries(
|
||||
// as if it were reading from a regular table.
|
||||
from_clause_subquery.result_columns_start_reg = Some(result_columns_start);
|
||||
}
|
||||
|
||||
program.pop_current_parent_explain();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ pub struct ProgramBuilder {
|
||||
// Bitmask of cursors that have emitted a SeekRowid instruction.
|
||||
seekrowid_emitted_bitmask: u64,
|
||||
// map of instruction index to manual comment (used in EXPLAIN only)
|
||||
comments: Option<Vec<(InsnReference, &'static str)>>,
|
||||
comments: Vec<(InsnReference, &'static str)>,
|
||||
pub parameters: Parameters,
|
||||
pub result_columns: Vec<ResultSetColumn>,
|
||||
pub table_references: TableReferences,
|
||||
@@ -114,6 +114,10 @@ pub struct ProgramBuilder {
|
||||
// TODO: when we support multiple dbs, this should be a write mask to track which DBs need to be written
|
||||
txn_mode: TransactionMode,
|
||||
rollback: bool,
|
||||
/// The mode in which the query is being executed.
|
||||
query_mode: QueryMode,
|
||||
/// Current parent explain address, if any.
|
||||
current_parent_explain_idx: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -158,6 +162,18 @@ pub struct ProgramBuilderOpts {
|
||||
pub approx_num_labels: usize,
|
||||
}
|
||||
|
||||
/// Use this macro to emit an OP_Explain instruction.
|
||||
/// Please use this macro instead of calling emit_explain() directly,
|
||||
/// because we want to avoid allocating a String if we are not in explain mode.
|
||||
#[macro_export]
|
||||
macro_rules! emit_explain {
|
||||
($builder:expr, $push:expr, $detail:expr) => {
|
||||
if let QueryMode::ExplainQueryPlan = $builder.get_query_mode() {
|
||||
$builder.emit_explain($push, $detail);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl ProgramBuilder {
|
||||
pub fn new(
|
||||
query_mode: QueryMode,
|
||||
@@ -173,11 +189,7 @@ impl ProgramBuilder {
|
||||
constant_spans: Vec::new(),
|
||||
label_to_resolved_offset: Vec::with_capacity(opts.approx_num_labels),
|
||||
seekrowid_emitted_bitmask: 0,
|
||||
comments: if let QueryMode::Explain | QueryMode::ExplainQueryPlan = query_mode {
|
||||
Some(Vec::new())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
comments: Vec::new(),
|
||||
parameters: Parameters::new(),
|
||||
result_columns: Vec::new(),
|
||||
table_references: TableReferences::new(vec![], vec![]),
|
||||
@@ -189,6 +201,8 @@ impl ProgramBuilder {
|
||||
capture_data_changes_mode,
|
||||
txn_mode: TransactionMode::None,
|
||||
rollback: false,
|
||||
query_mode,
|
||||
current_parent_explain_idx: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -378,8 +392,40 @@ impl ProgramBuilder {
|
||||
}
|
||||
|
||||
pub fn add_comment(&mut self, insn_index: BranchOffset, comment: &'static str) {
|
||||
if let Some(comments) = &mut self.comments {
|
||||
comments.push((insn_index.as_offset_int(), comment));
|
||||
if let QueryMode::Explain | QueryMode::ExplainQueryPlan = self.query_mode {
|
||||
self.comments.push((insn_index.as_offset_int(), comment));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_query_mode(&self) -> QueryMode {
|
||||
self.query_mode
|
||||
}
|
||||
|
||||
/// use emit_explain macro instead, because we don't want to allocate
|
||||
/// String if we are not in explain mode
|
||||
pub fn emit_explain(&mut self, push: bool, detail: String) {
|
||||
if let QueryMode::ExplainQueryPlan = self.query_mode {
|
||||
self.emit_insn(Insn::Explain {
|
||||
p1: self.insns.len(),
|
||||
p2: self.current_parent_explain_idx,
|
||||
detail,
|
||||
});
|
||||
if push {
|
||||
self.current_parent_explain_idx = Some(self.insns.len() - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pop_current_parent_explain(&mut self) {
|
||||
if let QueryMode::ExplainQueryPlan = self.query_mode {
|
||||
if let Some(current) = self.current_parent_explain_idx {
|
||||
let (Insn::Explain { p2, .. }, _, _) = &self.insns[current] else {
|
||||
unreachable!("current_parent_explain_idx must point to an Explain insn");
|
||||
};
|
||||
self.current_parent_explain_idx = *p2;
|
||||
}
|
||||
} else {
|
||||
debug_assert!(self.current_parent_explain_idx.is_none())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -432,14 +478,44 @@ impl ProgramBuilder {
|
||||
}
|
||||
|
||||
// Fix comments to refer to new locations
|
||||
if let Some(comments) = &mut self.comments {
|
||||
for (old_offset, _) in comments.iter_mut() {
|
||||
let new_offset = self
|
||||
.insns
|
||||
.iter()
|
||||
.position(|(_, _, index)| *old_offset == *index as u32)
|
||||
.expect("comment must exist") as u32;
|
||||
*old_offset = new_offset;
|
||||
for (old_offset, _) in self.comments.iter_mut() {
|
||||
let new_offset = self
|
||||
.insns
|
||||
.iter()
|
||||
.position(|(_, _, index)| *old_offset == *index as u32)
|
||||
.expect("comment must exist") as u32;
|
||||
*old_offset = new_offset;
|
||||
}
|
||||
|
||||
if let QueryMode::ExplainQueryPlan = self.query_mode {
|
||||
self.current_parent_explain_idx =
|
||||
if let Some(old_parent) = self.current_parent_explain_idx {
|
||||
self.insns
|
||||
.iter()
|
||||
.position(|(_, _, index)| old_parent == *index)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
for i in 0..self.insns.len() {
|
||||
let (Insn::Explain { p2, .. }, _, _) = &self.insns[i] else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let new_p2 = if p2.is_some() {
|
||||
self.insns
|
||||
.iter()
|
||||
.position(|(_, _, index)| *p2 == Some(*index))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let (Insn::Explain { p1, p2, .. }, _, _) = &mut self.insns[i] else {
|
||||
unreachable!();
|
||||
};
|
||||
|
||||
*p1 = i;
|
||||
*p2 = new_p2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1723,6 +1723,15 @@ pub fn insn_to_row(
|
||||
0,
|
||||
format!("if (r[{}] < 0) goto {}", reg, target_pc.as_debug_int()),
|
||||
),
|
||||
Insn::Explain { p1, p2, detail } => (
|
||||
"Explain",
|
||||
*p1 as i32,
|
||||
p2.as_ref().map(|p| *p).unwrap_or(0) as i32,
|
||||
0,
|
||||
Value::build_text(detail.as_str()),
|
||||
0,
|
||||
String::new(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1086,6 +1086,13 @@ pub enum Insn {
|
||||
reg: usize,
|
||||
target_pc: BranchOffset,
|
||||
},
|
||||
|
||||
// OP_Explain
|
||||
Explain {
|
||||
p1: usize, // P1: address of instruction
|
||||
p2: Option<usize>, // P2: address of parent explain instruction
|
||||
detail: String, // P4: detail text
|
||||
},
|
||||
}
|
||||
|
||||
impl Insn {
|
||||
@@ -1224,6 +1231,7 @@ impl Insn {
|
||||
Insn::MaxPgcnt { .. } => execute::op_max_pgcnt,
|
||||
Insn::JournalMode { .. } => execute::op_journal_mode,
|
||||
Insn::IfNeg { .. } => execute::op_if_neg,
|
||||
Insn::Explain { .. } => execute::op_noop,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -477,7 +477,7 @@ pub struct Program {
|
||||
pub max_registers: usize,
|
||||
pub insns: Vec<(Insn, InsnFunction)>,
|
||||
pub cursor_ref: Vec<(Option<CursorKey>, CursorType)>,
|
||||
pub comments: Option<Vec<(InsnReference, &'static str)>>,
|
||||
pub comments: Vec<(InsnReference, &'static str)>,
|
||||
pub parameters: crate::parameters::Parameters,
|
||||
pub connection: Arc<Connection>,
|
||||
pub n_change: Cell<i64>,
|
||||
@@ -541,13 +541,11 @@ impl Program {
|
||||
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()
|
||||
}),
|
||||
self.comments
|
||||
.iter()
|
||||
.find(|(offset, _)| *offset == state.pc)
|
||||
.map(|(_, comment)| comment)
|
||||
.copied(),
|
||||
);
|
||||
|
||||
state.registers[0] = Register::Value(Value::Integer(state.pc as i64));
|
||||
@@ -570,10 +568,47 @@ impl Program {
|
||||
&self,
|
||||
state: &mut ProgramState,
|
||||
_mv_store: Option<Arc<MvStore>>,
|
||||
_pager: Rc<Pager>,
|
||||
pager: Rc<Pager>,
|
||||
) -> Result<StepResult> {
|
||||
debug_assert!(state.column_count() == EXPLAIN_QUERY_PLAN_COLUMNS.len());
|
||||
todo!("we need OP_Explain to be implemented first")
|
||||
loop {
|
||||
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 Insn::Explain { p1, p2, detail } = &self.insns[state.pc as usize].0 else {
|
||||
state.pc += 1;
|
||||
continue;
|
||||
};
|
||||
|
||||
state.registers[0] = Register::Value(Value::Integer(*p1 as i64));
|
||||
state.registers[1] =
|
||||
Register::Value(Value::Integer(p2.as_ref().map(|p| *p).unwrap_or(0) as i64));
|
||||
state.registers[2] = Register::Value(Value::Integer(0));
|
||||
state.registers[3] = Register::Value(Value::from_text(detail.as_str()));
|
||||
state.result_row = Some(Row {
|
||||
values: &state.registers[0] as *const Register,
|
||||
count: EXPLAIN_QUERY_PLAN_COLUMNS.len(),
|
||||
});
|
||||
state.pc += 1;
|
||||
return Ok(StepResult::Row);
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip_all, level = Level::DEBUG)]
|
||||
@@ -943,11 +978,12 @@ fn trace_insn(program: &Program, addr: InsnReference, insn: &Insn) {
|
||||
addr,
|
||||
insn,
|
||||
String::new(),
|
||||
program.comments.as_ref().and_then(|comments| comments
|
||||
program
|
||||
.comments
|
||||
.iter()
|
||||
.find(|(offset, _)| *offset == addr)
|
||||
.map(|(_, comment)| comment)
|
||||
.copied())
|
||||
.copied()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user