mirror of
https://github.com/aljazceru/turso.git
synced 2026-02-22 16:35:30 +01:00
Add EXPLAIN support for trigger subprograms
They get printed after the parent program.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
use parking_lot::RwLock;
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
sync::{atomic::AtomicI64, Arc},
|
||||
@@ -38,7 +39,7 @@ impl TableRefIdCounter {
|
||||
}
|
||||
}
|
||||
|
||||
use super::{BranchOffset, CursorID, Insn, InsnReference, JumpTarget, Program};
|
||||
use super::{BranchOffset, CursorID, ExplainState, Insn, InsnReference, JumpTarget, Program};
|
||||
|
||||
/// A key that uniquely identifies a cursor.
|
||||
/// The key is a pair of table reference id and index.
|
||||
@@ -89,7 +90,7 @@ pub struct ProgramBuilder {
|
||||
next_free_register: usize,
|
||||
next_free_cursor_id: usize,
|
||||
/// Instruction, the function to execute it with, and its original index in the vector.
|
||||
insns: Vec<(Insn, usize)>,
|
||||
pub insns: Vec<(Insn, usize)>,
|
||||
/// A span of instructions from (offset_start_inclusive, offset_end_exclusive),
|
||||
/// that are deemed to be compile-time constant and can be hoisted out of loops
|
||||
/// so that they get evaluated only once at the start of the program.
|
||||
@@ -129,9 +130,6 @@ pub struct ProgramBuilder {
|
||||
needs_stmt_subtransactions: bool,
|
||||
/// If this ProgramBuilder is building trigger subprogram, a ref to the trigger is stored here.
|
||||
pub trigger: Option<Arc<Trigger>>,
|
||||
/// The type of resolution to perform if a constraint violation occurs during the execution of the program.
|
||||
/// At present this is required only for ignoring errors when there is an INSERT OR IGNORE statement that triggers a trigger subprogram
|
||||
/// which causes a conflict.
|
||||
pub resolve_type: ResolveType,
|
||||
}
|
||||
|
||||
@@ -806,9 +804,6 @@ impl ProgramBuilder {
|
||||
} => {
|
||||
resolve(end_offset, "Yield");
|
||||
}
|
||||
Insn::RowSetRead { pc_if_empty, .. } => {
|
||||
resolve(pc_if_empty, "RowSetRead");
|
||||
}
|
||||
Insn::SeekRowid { target_pc, .. } => {
|
||||
resolve(target_pc, "SeekRowid");
|
||||
}
|
||||
@@ -860,6 +855,9 @@ impl ProgramBuilder {
|
||||
Insn::VFilter { pc_if_empty, .. } => {
|
||||
resolve(pc_if_empty, "VFilter");
|
||||
}
|
||||
Insn::RowSetRead { pc_if_empty, .. } => {
|
||||
resolve(pc_if_empty, "RowSetRead");
|
||||
}
|
||||
Insn::NoConflict { target_pc, .. } => {
|
||||
resolve(target_pc, "NoConflict");
|
||||
}
|
||||
@@ -955,7 +953,6 @@ impl ProgramBuilder {
|
||||
/// Initialize the program with basic setup and return initial metadata and labels
|
||||
pub fn prologue(&mut self) {
|
||||
if self.trigger.is_some() {
|
||||
// Trigger subprograms don't start transactions so their init just falls through to the next instruction
|
||||
self.init_label = self.allocate_label();
|
||||
self.emit_insn(Insn::Init {
|
||||
target_pc: self.init_label,
|
||||
@@ -1003,7 +1000,6 @@ impl ProgramBuilder {
|
||||
/// query will jump to the Transaction instruction via init_label.
|
||||
pub fn epilogue(&mut self, schema: &Schema) {
|
||||
if self.trigger.is_some() {
|
||||
// Trigger subprograms don't start transactions
|
||||
self.emit_insn(Insn::Halt {
|
||||
err_code: 0,
|
||||
description: "trigger".to_string(),
|
||||
@@ -1159,8 +1155,9 @@ impl ProgramBuilder {
|
||||
sql: sql.to_string(),
|
||||
accesses_db: !matches!(self.txn_mode, TransactionMode::None),
|
||||
needs_stmt_subtransactions: self.needs_stmt_subtransactions,
|
||||
trigger: self.trigger,
|
||||
trigger: self.trigger.take(),
|
||||
resolve_type: self.resolve_type,
|
||||
explain_state: RwLock::new(ExplainState::default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
114
core/vdbe/mod.rs
114
core/vdbe/mod.rs
@@ -64,6 +64,7 @@ use execute::{
|
||||
InsnFunction, InsnFunctionStepResult, OpIdxDeleteState, OpIntegrityCheckState,
|
||||
OpOpenEphemeralState,
|
||||
};
|
||||
use parking_lot::RwLock;
|
||||
use turso_parser::ast::ResolveType;
|
||||
|
||||
use crate::vdbe::rowset::RowSet;
|
||||
@@ -598,6 +599,17 @@ macro_rules! get_cursor {
|
||||
};
|
||||
}
|
||||
|
||||
/// Tracks the state of explain mode execution, including which subprograms need to be processed.
|
||||
#[derive(Default)]
|
||||
pub struct ExplainState {
|
||||
/// Program counter positions in the parent program where `Insn::Program` instructions occur.
|
||||
parent_program_pcs: Vec<usize>,
|
||||
/// Index of the subprogram currently being processed, if any.
|
||||
current_subprogram_index: Option<usize>,
|
||||
/// PC value when we started processing the current subprogram, to detect if we need to reset.
|
||||
subprogram_start_pc: Option<usize>,
|
||||
}
|
||||
|
||||
pub struct Program {
|
||||
pub max_registers: usize,
|
||||
// we store original indices because we don't want to create new vec from
|
||||
@@ -620,12 +632,9 @@ pub struct Program {
|
||||
/// is determined by the parser flags "mayAbort" and "isMultiWrite". Essentially this means that the individual
|
||||
/// statement may need to be aborted due to a constraint conflict, etc. instead of the entire transaction.
|
||||
pub needs_stmt_subtransactions: bool,
|
||||
/// If this is a trigger subprogram, this is the trigger that is being executed.
|
||||
pub trigger: Option<Arc<Trigger>>,
|
||||
/// The type of resolution to perform if a constraint violation occurs during the execution of the program.
|
||||
/// At present this is required only for ignoring errors when there is an INSERT OR IGNORE statement that triggers a trigger subprogram
|
||||
/// which causes a conflict.
|
||||
pub resolve_type: ResolveType,
|
||||
pub explain_state: RwLock<ExplainState>,
|
||||
}
|
||||
|
||||
impl Program {
|
||||
@@ -671,11 +680,106 @@ impl Program {
|
||||
// FIXME: do we need this?
|
||||
state.metrics.vm_steps = state.metrics.vm_steps.saturating_add(1);
|
||||
|
||||
let mut explain_state = self.explain_state.write();
|
||||
|
||||
// Check if we're processing a subprogram
|
||||
if let Some(sub_idx) = explain_state.current_subprogram_index {
|
||||
if sub_idx >= explain_state.parent_program_pcs.len() {
|
||||
// All subprograms processed
|
||||
*explain_state = ExplainState::default();
|
||||
return Ok(StepResult::Done);
|
||||
}
|
||||
|
||||
let parent_pc = explain_state.parent_program_pcs[sub_idx];
|
||||
let Insn::Program { program: p, .. } = &self.insns[parent_pc].0 else {
|
||||
panic!("Expected program insn at pc {parent_pc}");
|
||||
};
|
||||
let p = &mut p.write().program;
|
||||
|
||||
let subprogram_insn_count = p.insns.len();
|
||||
|
||||
// Check if the subprogram has already finished (PC is out of bounds)
|
||||
// This can happen if the subprogram finished in a previous call but we're being called again
|
||||
if state.pc as usize >= subprogram_insn_count {
|
||||
// Subprogram is done, move to next one
|
||||
explain_state.subprogram_start_pc = None;
|
||||
if sub_idx + 1 < explain_state.parent_program_pcs.len() {
|
||||
explain_state.current_subprogram_index = Some(sub_idx + 1);
|
||||
state.pc = 0;
|
||||
drop(explain_state);
|
||||
return self.explain_step(state, _mv_store, pager);
|
||||
} else {
|
||||
*explain_state = ExplainState::default();
|
||||
return Ok(StepResult::Done);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset PC to 0 only when starting a new subprogram (when subprogram_start_pc is None)
|
||||
// Once we've started, let the subprogram manage its own PC through its explain_step
|
||||
if explain_state.subprogram_start_pc.is_none() {
|
||||
state.pc = 0;
|
||||
explain_state.subprogram_start_pc = Some(0);
|
||||
}
|
||||
|
||||
// Process the subprogram - it will handle its own explain_step internally
|
||||
// The subprogram's explain_step will process all its instructions (including any nested subprograms)
|
||||
// and return StepResult::Row for each instruction, then StepResult::Done when finished
|
||||
let result = p.step(state, None, pager.clone(), QueryMode::Explain, None)?;
|
||||
|
||||
match result {
|
||||
StepResult::Done => {
|
||||
// This subprogram is done, move to next one
|
||||
explain_state.subprogram_start_pc = None; // Clear the start PC marker
|
||||
if sub_idx + 1 < explain_state.parent_program_pcs.len() {
|
||||
// Move to next subprogram
|
||||
explain_state.current_subprogram_index = Some(sub_idx + 1);
|
||||
// Reset PC to 0 for the next subprogram
|
||||
state.pc = 0;
|
||||
// Recursively call to process the next subprogram
|
||||
drop(explain_state);
|
||||
return self.explain_step(state, _mv_store, pager);
|
||||
} else {
|
||||
// All subprograms done
|
||||
*explain_state = ExplainState::default();
|
||||
return Ok(StepResult::Done);
|
||||
}
|
||||
}
|
||||
StepResult::Row => {
|
||||
// Output a row from the subprogram
|
||||
// The subprogram's step already set up the registers with PC starting at 0
|
||||
// Don't reset subprogram_start_pc - we're still processing this subprogram
|
||||
drop(explain_state);
|
||||
return Ok(StepResult::Row);
|
||||
}
|
||||
other => {
|
||||
drop(explain_state);
|
||||
return Ok(other);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We're processing the parent program
|
||||
if state.pc as usize >= self.insns.len() {
|
||||
return Ok(StepResult::Done);
|
||||
// Parent program is done, start processing subprograms
|
||||
if explain_state.parent_program_pcs.is_empty() {
|
||||
// No subprograms to process
|
||||
*explain_state = ExplainState::default();
|
||||
return Ok(StepResult::Done);
|
||||
}
|
||||
|
||||
// Start processing the first subprogram
|
||||
explain_state.current_subprogram_index = Some(0);
|
||||
explain_state.subprogram_start_pc = None; // Will be set when we actually start processing
|
||||
state.pc = 0; // Reset PC to 0 for the first subprogram
|
||||
drop(explain_state);
|
||||
return self.explain_step(state, _mv_store, pager);
|
||||
}
|
||||
|
||||
let (current_insn, _) = &self.insns[state.pc as usize];
|
||||
|
||||
if matches!(current_insn, Insn::Program { .. }) {
|
||||
explain_state.parent_program_pcs.push(state.pc as usize);
|
||||
}
|
||||
let (opcode, p1, p2, p3, p4, p5, comment) = insn_to_row_with_comment(
|
||||
self,
|
||||
current_insn,
|
||||
|
||||
Reference in New Issue
Block a user