Merge 'handle EXPLAIN like sqlite' from Lâm Hoàng Phúc

we are hard coding `EXPLAIN` for debugging
```sh
turso> EXPLAIN SELECT 1; EXPLAIN SELECT 1;
addr  opcode             p1    p2    p3    p4             p5  comment
----  -----------------  ----  ----  ----  -------------  --  -------
0     Init               0     3     0                    0   Start at 3
1     ResultRow          1     1     0                    0   output=r[1]
2     Halt               0     0     0                    0
3     Integer            1     1     0                    0   r[1]=1
4     Goto               0     1     0                    0
```
```sh
sqlite> EXPLAIN SELECT 1; EXPLAIN SELECT 1;
addr  opcode         p1    p2    p3    p4             p5  comment
----  -------------  ----  ----  ----  -------------  --  -------------
0     Init           0     4     0                    0   Start at 4
1     Integer        1     1     0                    0   r[1]=1
2     ResultRow      1     1     0                    0   output=r[1]
3     Halt           0     0     0                    0
4     Goto           0     1     0                    0
addr  opcode         p1    p2    p3    p4             p5  comment
----  -------------  ----  ----  ----  -------------  --  -------------
0     Init           0     4     0                    0   Start at 4
1     Integer        1     1     0                    0   r[1]=1
2     ResultRow      1     1     0                    0   output=r[1]
3     Halt           0     0     0                    0
4     Goto           0     1     0                    0
```

Closes #3005
This commit is contained in:
Pekka Enberg
2025-09-11 18:43:24 +03:00
committed by GitHub
5 changed files with 304 additions and 243 deletions

View File

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

View File

@@ -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<Arc<MvStore>>, pager: Rc<Pager>) -> Self {
pub fn new(
program: vdbe::Program,
mv_store: Option<Arc<MvStore>>,
pager: Rc<Pager>,
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<StepResult> {
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<str> {
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;

View File

@@ -139,13 +139,15 @@ impl CursorType {
pub enum QueryMode {
Normal,
Explain,
ExplainQueryPlan,
}
impl From<ast::Cmd> 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

View File

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

View File

@@ -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<Arc<MvStore>>,
pager: Rc<Pager>,
query_mode: QueryMode,
) -> Result<StepResult> {
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<Arc<MvStore>>,
pager: Rc<Pager>,
) -> Result<StepResult> {
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<Arc<MvStore>>,
_pager: Rc<Pager>,
) -> Result<StepResult> {
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<Arc<MvStore>>,
pager: Rc<Pager>,
) -> Result<StepResult> {
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<usize> {
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<Self>
where