Add EXPLAIN support for trigger subprograms

They get printed after the parent program.
This commit is contained in:
Jussi Saurio
2025-11-18 13:08:00 +02:00
parent 423a1444d1
commit 9aa09d5ccf
2 changed files with 117 additions and 16 deletions

View File

@@ -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()),
}
}
}

View File

@@ -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,