Merge 'More structured query planner' from Jussi Saurio

Reader's guide to this PR:

The aim is to have a more structured and maintainable approach to generating bytecode from the query AST so that different parts of the query processing pipeline have clearer responsibilities, so that developing new functionality is easier. E.g.:

- If you want to implement join reordering -> you do it in `Optimizer`
- If you want to implement `GROUP BY` -> you change `QueryPlanNode::Aggregate` to include it, parse it in `Planner` and handle the code generation for it in `Emitter`

The pipeline is:

`SQL text -> Parser -> Planner -> Optimizer -> Emitter`

and this pipeline generates:

`SQL text -> AST -> Logical Plan -> Optimized Logical Plan -> SQLite Bytecode`

---

Module structure:

`plan.rs`: defines the `Operator` enum. An `Operator` is a tree of other `Operators`, e.g. an `Operator::Join` has `left` and `right` children, etc.

`planner.rs`: Parses an `ast::Select` into a `Plan` which is mainly a wrapper for a root `Operator`

`optimizer.rs`: Makes a new `Plan` from an input `Plan` - does predicate pushdown, constant elimination and turns `Scan` nodes into `SeekRowId` nodes where applicable

`emitter.rs`: Generates bytecode instructions from an input `Plan`.

---

Adds feature `EXPLAIN QUERY PLAN <stmt>` which shows the logical query plan instead of the bytecode plan

---

Other changes:

- Almost everything from `select.rs` removed; things like `translate_aggregation()` moved to `expr.rs`
- `where_clause.rs` removed, some things from it like `translate_condition_expr()` moved to `expr.rs`
- i.e.: there is nothing _new_ in `expr.rs`, stuff just moved there

---

Concerns:

- Perf impact: there's a lot more indirection than before (`Operator`s are very "traditional" trees where they refer to other operators via Boxes etc)

Closes #281
This commit is contained in:
Pekka Enberg
2024-08-18 16:36:51 +03:00
15 changed files with 3525 additions and 2409 deletions

View File

@@ -17,6 +17,7 @@ static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
use fallible_iterator::FallibleIterator;
use log::trace;
use schema::Schema;
use sqlite3_parser::ast;
use sqlite3_parser::{ast::Cmd, lexer::sql::Parser};
use std::sync::Arc;
use std::{cell::RefCell, rc::Rc};
@@ -27,6 +28,9 @@ use storage::sqlite3_ondisk::DatabaseHeader;
#[cfg(feature = "fs")]
use storage::wal::WalFile;
use translate::optimizer::optimize_plan;
use translate::planner::prepare_select_plan;
pub use error::LimboError;
pub type Result<T> = std::result::Result<T, error::LimboError>;
@@ -173,7 +177,17 @@ impl Connection {
program.explain();
Ok(None)
}
Cmd::ExplainQueryPlan(_stmt) => Ok(None),
Cmd::ExplainQueryPlan(stmt) => {
match stmt {
ast::Stmt::Select(select) => {
let plan = prepare_select_plan(&self.schema, select)?;
let plan = optimize_plan(plan)?;
println!("{}", plan);
}
_ => todo!(),
}
Ok(None)
}
}
} else {
Ok(None)

898
core/translate/emitter.rs Normal file
View File

@@ -0,0 +1,898 @@
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use std::usize;
use crate::schema::{BTreeTable, Column, PseudoTable, Table};
use crate::storage::sqlite3_ondisk::DatabaseHeader;
use crate::types::{OwnedRecord, OwnedValue};
use crate::vdbe::builder::ProgramBuilder;
use crate::vdbe::{BranchOffset, Insn, Program};
use crate::Result;
use super::expr::{
translate_aggregation, translate_condition_expr, translate_expr, translate_table_columns,
ConditionMetadata,
};
use super::plan::Plan;
use super::plan::{Operator, ProjectionColumn};
/**
* The Emitter trait is used to emit bytecode instructions for a given operator in the query plan.
*
* - step: perform a single step of the operator, emitting bytecode instructions as needed,
and returning a result indicating whether the operator is ready to emit a result row
*/
pub trait Emitter {
fn step(
&mut self,
pb: &mut ProgramBuilder,
m: &mut Metadata,
referenced_tables: &[(Rc<BTreeTable>, String)],
) -> Result<OpStepResult>;
fn result_columns(
&self,
program: &mut ProgramBuilder,
referenced_tables: &[(Rc<BTreeTable>, String)],
metadata: &mut Metadata,
cursor_override: Option<usize>,
) -> Result<usize>;
fn result_row(
&mut self,
program: &mut ProgramBuilder,
referenced_tables: &[(Rc<BTreeTable>, String)],
metadata: &mut Metadata,
cursor_override: Option<usize>,
) -> Result<()>;
}
#[derive(Debug)]
pub struct LeftJoinMetadata {
// integer register that holds a flag that is set to true if the current row has a match for the left join
pub match_flag_register: usize,
// label for the instruction that sets the match flag to true
pub set_match_flag_true_label: BranchOffset,
// label for the instruction that checks if the match flag is true
pub check_match_flag_label: BranchOffset,
// label for the instruction where the program jumps to if the current row has a match for the left join
pub on_match_jump_to_label: BranchOffset,
}
#[derive(Debug)]
pub struct SortMetadata {
// cursor id for the Sorter table where the sorted rows are stored
pub sort_cursor: usize,
// cursor id for the Pseudo table where rows are temporarily inserted from the Sorter table
pub pseudo_table_cursor: usize,
// label where the SorterData instruction is emitted; SorterNext will jump here if there is more data to read
pub sorter_data_label: BranchOffset,
// label for the instruction immediately following SorterNext; SorterSort will jump here in case there is no data
pub done_label: BranchOffset,
}
#[derive(Debug, Default)]
pub struct Metadata {
// labels for the instructions that terminate the execution when a conditional check evaluates to false. typically jumps to Halt, but can also jump to AggFinal if a parent in the tree is an aggregation
termination_labels: Vec<BranchOffset>,
// labels for the instructions that jump to the next row in the current operator.
// for example, in a join with two nested scans, the inner loop will jump to its Next instruction when the join condition is false;
// in a join with a scan and a seek, the seek will jump to the scan's Next instruction when the join condition is false.
next_row_labels: HashMap<usize, BranchOffset>,
// labels for the Rewind instructions.
rewind_labels: Vec<BranchOffset>,
// mapping between Aggregation operator id and the register that holds the start of the aggregation result
aggregation_start_registers: HashMap<usize, usize>,
// mapping between Order operator id and associated metadata
sorts: HashMap<usize, SortMetadata>,
// mapping between Join operator id and associated metadata (for left joins only)
left_joins: HashMap<usize, LeftJoinMetadata>,
}
/**
* Emitters return one of three possible results from the step() method:
* - Continue: the operator is not yet ready to emit a result row
* - ReadyToEmit: the operator is ready to emit a result row
* - Done: the operator has completed execution
* For example, a Scan operator will return Continue until it has opened a cursor, rewound it and applied any predicates.
* At that point, it will return ReadyToEmit.
* Finally, when the Scan operator has emitted a Next instruction, it will return Done.
*
* Parent operators are free to make decisions based on the result a child operator's step() method.
*
* When the root operator of a Plan returns ReadyToEmit, a ResultRow will always be emitted.
* When the root operator returns Done, the bytecode plan is complete.
*
*/
#[derive(Debug, PartialEq)]
pub enum OpStepResult {
Continue,
ReadyToEmit,
Done,
}
impl Emitter for Operator {
fn step(
&mut self,
program: &mut ProgramBuilder,
m: &mut Metadata,
referenced_tables: &[(Rc<BTreeTable>, String)],
) -> Result<OpStepResult> {
match self {
Operator::Scan {
table,
table_identifier,
id,
step,
predicates,
..
} => {
*step += 1;
const SCAN_OPEN_READ: usize = 1;
const SCAN_REWIND_AND_CONDITIONS: usize = 2;
const SCAN_NEXT: usize = 3;
match *step {
SCAN_OPEN_READ => {
let cursor_id = program.alloc_cursor_id(
Some(table_identifier.clone()),
Some(Table::BTree(table.clone())),
);
let root_page = table.root_page;
let next_row_label = program.allocate_label();
m.next_row_labels.insert(*id, next_row_label);
program.emit_insn(Insn::OpenReadAsync {
cursor_id,
root_page,
});
program.emit_insn(Insn::OpenReadAwait);
Ok(OpStepResult::Continue)
}
SCAN_REWIND_AND_CONDITIONS => {
let cursor_id = program.resolve_cursor_id(table_identifier, None);
program.emit_insn(Insn::RewindAsync { cursor_id });
let rewind_label = program.allocate_label();
let halt_label = m.termination_labels.last().unwrap();
m.rewind_labels.push(rewind_label);
program.defer_label_resolution(rewind_label, program.offset() as usize);
program.emit_insn_with_label_dependency(
Insn::RewindAwait {
cursor_id,
pc_if_empty: *halt_label,
},
*halt_label,
);
let jump_label = m.next_row_labels.get(id).unwrap_or(halt_label);
if let Some(preds) = predicates {
for expr in preds {
let jump_target_when_true = program.allocate_label();
let condition_metadata = ConditionMetadata {
jump_if_condition_is_true: false,
jump_target_when_true,
jump_target_when_false: *jump_label,
};
translate_condition_expr(
program,
referenced_tables,
expr,
None,
condition_metadata,
)?;
program.resolve_label(jump_target_when_true, program.offset());
}
}
Ok(OpStepResult::ReadyToEmit)
}
SCAN_NEXT => {
let cursor_id = program.resolve_cursor_id(table_identifier, None);
program
.resolve_label(*m.next_row_labels.get(id).unwrap(), program.offset());
program.emit_insn(Insn::NextAsync { cursor_id });
let jump_label = m.rewind_labels.pop().unwrap();
program.emit_insn_with_label_dependency(
Insn::NextAwait {
cursor_id,
pc_if_next: jump_label,
},
jump_label,
);
Ok(OpStepResult::Done)
}
_ => Ok(OpStepResult::Done),
}
}
Operator::SeekRowid {
table,
table_identifier,
rowid_predicate,
predicates,
step,
id,
..
} => {
*step += 1;
const SEEKROWID_OPEN_READ: usize = 1;
const SEEKROWID_SEEK_AND_CONDITIONS: usize = 2;
match *step {
SEEKROWID_OPEN_READ => {
let cursor_id = program.alloc_cursor_id(
Some(table_identifier.clone()),
Some(Table::BTree(table.clone())),
);
let root_page = table.root_page;
program.emit_insn(Insn::OpenReadAsync {
cursor_id,
root_page,
});
program.emit_insn(Insn::OpenReadAwait);
Ok(OpStepResult::Continue)
}
SEEKROWID_SEEK_AND_CONDITIONS => {
let cursor_id = program.resolve_cursor_id(table_identifier, None);
let rowid_reg = program.alloc_register();
translate_expr(
program,
Some(referenced_tables),
rowid_predicate,
rowid_reg,
None,
)?;
let jump_label = m
.next_row_labels
.get(id)
.unwrap_or(&m.termination_labels.last().unwrap());
program.emit_insn_with_label_dependency(
Insn::SeekRowid {
cursor_id,
src_reg: rowid_reg,
target_pc: *jump_label,
},
*jump_label,
);
if let Some(predicates) = predicates {
for predicate in predicates.iter() {
let jump_target_when_true = program.allocate_label();
let condition_metadata = ConditionMetadata {
jump_if_condition_is_true: false,
jump_target_when_true,
jump_target_when_false: *jump_label,
};
translate_condition_expr(
program,
referenced_tables,
predicate,
None,
condition_metadata,
)?;
program.resolve_label(jump_target_when_true, program.offset());
}
}
Ok(OpStepResult::ReadyToEmit)
}
_ => Ok(OpStepResult::Done),
}
}
Operator::Join {
left,
right,
outer,
predicates,
step,
id,
..
} => {
*step += 1;
const JOIN_INIT: usize = 1;
const JOIN_DO_JOIN: usize = 2;
const JOIN_END: usize = 3;
match *step {
JOIN_INIT => {
if *outer {
let lj_metadata = LeftJoinMetadata {
match_flag_register: program.alloc_register(),
set_match_flag_true_label: program.allocate_label(),
check_match_flag_label: program.allocate_label(),
on_match_jump_to_label: program.allocate_label(),
};
m.left_joins.insert(*id, lj_metadata);
}
left.step(program, m, referenced_tables)?;
right.step(program, m, referenced_tables)?;
Ok(OpStepResult::Continue)
}
JOIN_DO_JOIN => {
left.step(program, m, referenced_tables)?;
let mut jump_target_when_false = *m
.next_row_labels
.get(&right.id())
.or(m.next_row_labels.get(&left.id()))
.unwrap_or(&m.termination_labels.last().unwrap());
if *outer {
let lj_meta = m.left_joins.get(id).unwrap();
program.emit_insn(Insn::Integer {
value: 0,
dest: lj_meta.match_flag_register,
});
jump_target_when_false = lj_meta.check_match_flag_label;
}
m.next_row_labels.insert(right.id(), jump_target_when_false);
right.step(program, m, referenced_tables)?;
if let Some(predicates) = predicates {
let jump_target_when_true = program.allocate_label();
let condition_metadata = ConditionMetadata {
jump_if_condition_is_true: false,
jump_target_when_true,
jump_target_when_false,
};
for predicate in predicates.iter() {
translate_condition_expr(
program,
referenced_tables,
predicate,
None,
condition_metadata,
)?;
}
program.resolve_label(jump_target_when_true, program.offset());
}
if *outer {
let lj_meta = m.left_joins.get(id).unwrap();
program.defer_label_resolution(
lj_meta.set_match_flag_true_label,
program.offset() as usize,
);
program.emit_insn(Insn::Integer {
value: 1,
dest: lj_meta.match_flag_register,
});
}
Ok(OpStepResult::ReadyToEmit)
}
JOIN_END => {
right.step(program, m, referenced_tables)?;
if *outer {
let lj_meta = m.left_joins.get(id).unwrap();
// If the left join match flag has been set to 1, we jump to the next row on the outer table (result row has been emitted already)
program.resolve_label(lj_meta.check_match_flag_label, program.offset());
program.emit_insn_with_label_dependency(
Insn::IfPos {
reg: lj_meta.match_flag_register,
target_pc: lj_meta.on_match_jump_to_label,
decrement_by: 0,
},
lj_meta.on_match_jump_to_label,
);
// If not, we set the right table cursor's "pseudo null bit" on, which means any Insn::Column will return NULL
let right_cursor_id = match right.as_ref() {
Operator::Scan {
table_identifier, ..
} => program.resolve_cursor_id(table_identifier, None),
Operator::SeekRowid {
table_identifier, ..
} => program.resolve_cursor_id(table_identifier, None),
_ => unreachable!(),
};
program.emit_insn(Insn::NullRow {
cursor_id: right_cursor_id,
});
// Jump to setting the left join match flag to 1 again, but this time the right table cursor will set everything to null
program.emit_insn_with_label_dependency(
Insn::Goto {
target_pc: lj_meta.set_match_flag_true_label,
},
lj_meta.set_match_flag_true_label,
);
// This points to the NextAsync instruction of the left table
program.resolve_label(lj_meta.on_match_jump_to_label, program.offset());
}
left.step(program, m, referenced_tables)?;
Ok(OpStepResult::Done)
}
_ => Ok(OpStepResult::Done),
}
}
Operator::Aggregate {
id,
source,
aggregates,
step,
} => {
*step += 1;
const AGGREGATE_INIT: usize = 1;
const AGGREGATE_WAIT_UNTIL_SOURCE_READY: usize = 2;
match *step {
AGGREGATE_INIT => {
let agg_final_label = program.allocate_label();
m.termination_labels.push(agg_final_label);
let num_aggs = aggregates.len();
let start_reg = program.alloc_registers(num_aggs);
m.aggregation_start_registers.insert(*id, start_reg);
Ok(OpStepResult::Continue)
}
AGGREGATE_WAIT_UNTIL_SOURCE_READY => loop {
match source.step(program, m, referenced_tables)? {
OpStepResult::Continue => {}
OpStepResult::ReadyToEmit => {
let start_reg = m.aggregation_start_registers.get(id).unwrap();
for (i, agg) in aggregates.iter().enumerate() {
let agg_result_reg = start_reg + i;
translate_aggregation(
program,
referenced_tables,
agg,
agg_result_reg,
None,
)?;
}
}
OpStepResult::Done => {
return Ok(OpStepResult::ReadyToEmit);
}
}
},
_ => Ok(OpStepResult::Done),
}
}
Operator::Filter { .. } => unreachable!("predicates have been pushed down"),
Operator::Limit { source, step, .. } => {
*step += 1;
loop {
match source.step(program, m, referenced_tables)? {
OpStepResult::Continue => continue,
OpStepResult::ReadyToEmit => {
return Ok(OpStepResult::ReadyToEmit);
}
OpStepResult::Done => return Ok(OpStepResult::Done),
}
}
}
Operator::Order {
id,
source,
key,
step,
} => {
*step += 1;
const ORDER_INIT: usize = 1;
const ORDER_INSERT_INTO_SORTER: usize = 2;
const ORDER_SORT_AND_OPEN_LOOP: usize = 3;
const ORDER_NEXT: usize = 4;
match *step {
ORDER_INIT => {
let sort_cursor = program.alloc_cursor_id(None, None);
m.sorts.insert(
*id,
SortMetadata {
sort_cursor,
pseudo_table_cursor: usize::MAX, // will be set later
sorter_data_label: program.allocate_label(),
done_label: program.allocate_label(),
},
);
let mut order = Vec::new();
for (_, direction) in key.iter() {
order.push(OwnedValue::Integer(*direction as i64));
}
program.emit_insn(Insn::SorterOpen {
cursor_id: sort_cursor,
columns: key.len(),
order: OwnedRecord::new(order),
});
loop {
match source.step(program, m, referenced_tables)? {
OpStepResult::Continue => continue,
OpStepResult::ReadyToEmit => {
return Ok(OpStepResult::Continue);
}
OpStepResult::Done => {
return Ok(OpStepResult::Done);
}
}
}
}
ORDER_INSERT_INTO_SORTER => {
let sort_keys_count = key.len();
let source_cols_count = source.column_count(referenced_tables);
let start_reg = program.alloc_registers(sort_keys_count);
for (i, (expr, _)) in key.iter().enumerate() {
let key_reg = start_reg + i;
translate_expr(program, Some(referenced_tables), expr, key_reg, None)?;
}
source.result_columns(program, referenced_tables, m, None)?;
let dest = program.alloc_register();
program.emit_insn(Insn::MakeRecord {
start_reg,
count: sort_keys_count + source_cols_count,
dest_reg: dest,
});
let sort_metadata = m.sorts.get_mut(id).unwrap();
program.emit_insn(Insn::SorterInsert {
cursor_id: sort_metadata.sort_cursor,
record_reg: dest,
});
Ok(OpStepResult::Continue)
}
ORDER_SORT_AND_OPEN_LOOP => {
loop {
match source.step(program, m, referenced_tables)? {
OpStepResult::Done => {
break;
}
_ => unreachable!(),
}
}
let column_names = source.column_names();
let pseudo_columns = column_names
.iter()
.map(|name| Column {
name: name.clone(),
primary_key: false,
ty: crate::schema::Type::Null,
})
.collect::<Vec<_>>();
let pseudo_cursor = program.alloc_cursor_id(
None,
Some(Table::Pseudo(Rc::new(PseudoTable {
columns: pseudo_columns,
}))),
);
let pseudo_content_reg = program.alloc_register();
program.emit_insn(Insn::OpenPseudo {
cursor_id: pseudo_cursor,
content_reg: pseudo_content_reg,
num_fields: key.len() + source.column_count(referenced_tables),
});
let sort_metadata = m.sorts.get(id).unwrap();
program.emit_insn_with_label_dependency(
Insn::SorterSort {
cursor_id: sort_metadata.sort_cursor,
pc_if_empty: sort_metadata.done_label,
},
sort_metadata.done_label,
);
program.defer_label_resolution(
sort_metadata.sorter_data_label,
program.offset() as usize,
);
program.emit_insn(Insn::SorterData {
cursor_id: sort_metadata.sort_cursor,
dest_reg: pseudo_content_reg,
pseudo_cursor,
});
let sort_metadata = m.sorts.get_mut(id).unwrap();
sort_metadata.pseudo_table_cursor = pseudo_cursor;
Ok(OpStepResult::ReadyToEmit)
}
ORDER_NEXT => {
let sort_metadata = m.sorts.get(id).unwrap();
program.emit_insn_with_label_dependency(
Insn::SorterNext {
cursor_id: sort_metadata.sort_cursor,
pc_if_next: sort_metadata.sorter_data_label,
},
sort_metadata.sorter_data_label,
);
program.resolve_label(sort_metadata.done_label, program.offset());
Ok(OpStepResult::Done)
}
_ => unreachable!(),
}
}
Operator::Projection { source, step, .. } => {
*step += 1;
const PROJECTION_WAIT_UNTIL_SOURCE_READY: usize = 1;
const PROJECTION_FINALIZE_SOURCE: usize = 2;
match *step {
PROJECTION_WAIT_UNTIL_SOURCE_READY => loop {
match source.step(program, m, referenced_tables)? {
OpStepResult::Continue => continue,
OpStepResult::ReadyToEmit | OpStepResult::Done => {
return Ok(OpStepResult::ReadyToEmit);
}
}
},
PROJECTION_FINALIZE_SOURCE => {
match source.step(program, m, referenced_tables)? {
OpStepResult::Done => {
return Ok(OpStepResult::Done);
}
_ => unreachable!(),
}
}
_ => Ok(OpStepResult::Done),
}
}
Operator::Nothing => Ok(OpStepResult::Done),
}
}
fn result_columns(
&self,
program: &mut ProgramBuilder,
referenced_tables: &[(Rc<BTreeTable>, String)],
m: &mut Metadata,
cursor_override: Option<usize>,
) -> Result<usize> {
let col_count = self.column_count(referenced_tables);
match self {
Operator::Scan {
table,
table_identifier,
..
} => {
let start_reg = program.alloc_registers(col_count);
translate_table_columns(
program,
table,
table_identifier,
cursor_override,
start_reg,
);
Ok(start_reg)
}
Operator::Join { left, right, .. } => {
let left_start_reg =
left.result_columns(program, referenced_tables, m, cursor_override)?;
right.result_columns(program, referenced_tables, m, cursor_override)?;
Ok(left_start_reg)
}
Operator::Aggregate { id, aggregates, .. } => {
let start_reg = m.aggregation_start_registers.get(id).unwrap();
for (i, agg) in aggregates.iter().enumerate() {
let agg_result_reg = *start_reg + i;
program.emit_insn(Insn::AggFinal {
register: agg_result_reg,
func: agg.func.clone(),
});
}
Ok(*start_reg)
}
Operator::Filter { .. } => unreachable!("predicates have been pushed down"),
Operator::SeekRowid {
table_identifier,
table,
..
} => {
let start_reg = program.alloc_registers(col_count);
translate_table_columns(
program,
table,
table_identifier,
cursor_override,
start_reg,
);
Ok(start_reg)
}
Operator::Limit { .. } => {
unimplemented!()
}
Operator::Order {
id, source, key, ..
} => {
let sort_metadata = m.sorts.get(id).unwrap();
let cursor_override = Some(sort_metadata.sort_cursor);
let sort_keys_count = key.len();
let start_reg = program.alloc_registers(sort_keys_count);
for (i, (expr, _)) in key.iter().enumerate() {
let key_reg = start_reg + i;
translate_expr(
program,
Some(referenced_tables),
expr,
key_reg,
cursor_override,
)?;
}
source.result_columns(program, referenced_tables, m, cursor_override)?;
Ok(start_reg)
}
Operator::Projection { expressions, .. } => {
let expr_count = expressions
.iter()
.map(|e| e.column_count(referenced_tables))
.sum();
let start_reg = program.alloc_registers(expr_count);
let mut cur_reg = start_reg;
for expr in expressions {
match expr {
ProjectionColumn::Column(expr) => {
translate_expr(
program,
Some(referenced_tables),
expr,
cur_reg,
cursor_override,
)?;
cur_reg += 1;
}
ProjectionColumn::Star => {
for (table, table_identifier) in referenced_tables.iter() {
cur_reg = translate_table_columns(
program,
table,
table_identifier,
cursor_override,
cur_reg,
);
}
}
ProjectionColumn::TableStar(_, table_identifier) => {
let (table, table_identifier) = referenced_tables
.iter()
.find(|(_, id)| id == table_identifier)
.unwrap();
cur_reg = translate_table_columns(
program,
table,
table_identifier,
cursor_override,
cur_reg,
);
}
}
}
Ok(start_reg)
}
Operator::Nothing => unimplemented!(),
}
}
fn result_row(
&mut self,
program: &mut ProgramBuilder,
referenced_tables: &[(Rc<BTreeTable>, String)],
m: &mut Metadata,
cursor_override: Option<usize>,
) -> Result<()> {
match self {
Operator::Order { id, source, .. } => {
let sort_metadata = m.sorts.get(id).unwrap();
source.result_row(
program,
referenced_tables,
m,
Some(sort_metadata.pseudo_table_cursor),
)?;
Ok(())
}
Operator::Limit { source, limit, .. } => {
source.result_row(program, referenced_tables, m, cursor_override)?;
let limit_reg = program.alloc_register();
program.emit_insn(Insn::Integer {
value: *limit as i64,
dest: limit_reg,
});
program.mark_last_insn_constant();
let jump_label = m.termination_labels.last().unwrap();
program.emit_insn_with_label_dependency(
Insn::DecrJumpZero {
reg: limit_reg,
target_pc: *jump_label,
},
*jump_label,
);
Ok(())
}
operator => {
let start_reg =
operator.result_columns(program, referenced_tables, m, cursor_override)?;
program.emit_insn(Insn::ResultRow {
start_reg,
count: operator.column_count(referenced_tables),
});
Ok(())
}
}
}
}
fn prologue() -> Result<(
ProgramBuilder,
Metadata,
BranchOffset,
BranchOffset,
BranchOffset,
)> {
let mut program = ProgramBuilder::new();
let init_label = program.allocate_label();
let halt_label = program.allocate_label();
program.emit_insn_with_label_dependency(
Insn::Init {
target_pc: init_label,
},
init_label,
);
let start_offset = program.offset();
let metadata = Metadata {
termination_labels: vec![halt_label],
..Default::default()
};
Ok((program, metadata, init_label, halt_label, start_offset))
}
fn epilogue(
program: &mut ProgramBuilder,
init_label: BranchOffset,
halt_label: BranchOffset,
start_offset: BranchOffset,
) -> Result<()> {
program.resolve_label(halt_label, program.offset());
program.emit_insn(Insn::Halt);
program.resolve_label(init_label, program.offset());
program.emit_insn(Insn::Transaction);
program.emit_constant_insns();
program.emit_insn(Insn::Goto {
target_pc: start_offset,
});
program.resolve_deferred_labels();
Ok(())
}
pub fn emit_program(
database_header: Rc<RefCell<DatabaseHeader>>,
mut plan: Plan,
) -> Result<Program> {
let (mut program, mut metadata, init_label, halt_label, start_offset) = prologue()?;
loop {
match plan
.root_operator
.step(&mut program, &mut metadata, &plan.referenced_tables)?
{
OpStepResult::Continue => {}
OpStepResult::ReadyToEmit => {
plan.root_operator.result_row(
&mut program,
&plan.referenced_tables,
&mut metadata,
None,
)?;
}
OpStepResult::Done => {
epilogue(&mut program, init_label, halt_label, start_offset)?;
return Ok(program.build(database_header));
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -7,10 +7,13 @@
//! a SELECT statement will be translated into a sequence of instructions that
//! will read rows from the database and filter them according to a WHERE clause.
pub(crate) mod emitter;
pub(crate) mod expr;
pub(crate) mod insert;
pub(crate) mod optimizer;
pub(crate) mod plan;
pub(crate) mod planner;
pub(crate) mod select;
pub(crate) mod where_clause;
use std::cell::RefCell;
use std::rc::Rc;
@@ -18,11 +21,10 @@ use std::rc::Rc;
use crate::schema::Schema;
use crate::storage::pager::Pager;
use crate::storage::sqlite3_ondisk::{DatabaseHeader, MIN_PAGE_CACHE_SIZE};
use crate::util::normalize_ident;
use crate::vdbe::{builder::ProgramBuilder, Insn, Program};
use crate::{bail_parse_error, Result};
use insert::translate_insert;
use select::{prepare_select, translate_select};
use select::translate_select;
use sqlite3_parser::ast;
/// Translate SQL statement into bytecode program.
@@ -56,10 +58,7 @@ pub fn translate(
ast::Stmt::Release(_) => bail_parse_error!("RELEASE not supported yet"),
ast::Stmt::Rollback { .. } => bail_parse_error!("ROLLBACK not supported yet"),
ast::Stmt::Savepoint(_) => bail_parse_error!("SAVEPOINT not supported yet"),
ast::Stmt::Select(select) => {
let select = prepare_select(schema, &select)?;
translate_select(select, database_header)
}
ast::Stmt::Select(select) => translate_select(schema, select, database_header),
ast::Stmt::Update { .. } => bail_parse_error!("UPDATE not supported yet"),
ast::Stmt::Vacuum(_, _) => bail_parse_error!("VACUUM not supported yet"),
ast::Stmt::Insert {

742
core/translate/optimizer.rs Normal file
View File

@@ -0,0 +1,742 @@
use std::rc::Rc;
use sqlite3_parser::ast;
use crate::{schema::BTreeTable, util::normalize_ident, Result};
use super::plan::{
get_table_ref_bitmask_for_ast_expr, get_table_ref_bitmask_for_operator, Operator, Plan,
};
/**
* Make a few passes over the plan to optimize it.
*/
pub fn optimize_plan(mut select_plan: Plan) -> Result<Plan> {
push_predicates(
&mut select_plan.root_operator,
&select_plan.referenced_tables,
)?;
eliminate_constants(&mut select_plan.root_operator)?;
use_indexes(
&mut select_plan.root_operator,
&select_plan.referenced_tables,
)?;
Ok(select_plan)
}
/**
* Use indexes where possible (currently just primary key lookups)
*/
fn use_indexes(
operator: &mut Operator,
referenced_tables: &[(Rc<BTreeTable>, String)],
) -> Result<()> {
match operator {
Operator::Scan {
table,
predicates: filter,
table_identifier,
id,
..
} => {
if filter.is_none() {
return Ok(());
}
let fs = filter.as_mut().unwrap();
let mut i = 0;
let mut maybe_rowid_predicate = None;
while i < fs.len() {
let f = fs[i].take_ownership();
let table_index = referenced_tables
.iter()
.position(|(t, t_id)| Rc::ptr_eq(t, table) && t_id == table_identifier)
.unwrap();
let (can_use, expr) =
try_extract_rowid_comparison_expression(f, table_index, referenced_tables)?;
if can_use {
maybe_rowid_predicate = Some(expr);
fs.remove(i);
break;
} else {
fs[i] = expr;
i += 1;
}
}
if let Some(rowid_predicate) = maybe_rowid_predicate {
let predicates_owned = if fs.is_empty() {
None
} else {
Some(fs.drain(..).collect())
};
*operator = Operator::SeekRowid {
table: table.clone(),
table_identifier: table_identifier.clone(),
rowid_predicate,
predicates: predicates_owned,
id: *id,
step: 0,
}
}
return Ok(());
}
Operator::Aggregate { source, .. } => {
use_indexes(source, referenced_tables)?;
return Ok(());
}
Operator::Filter { source, .. } => {
use_indexes(source, referenced_tables)?;
return Ok(());
}
Operator::SeekRowid { .. } => {
return Ok(());
}
Operator::Limit { source, .. } => {
use_indexes(source, referenced_tables)?;
return Ok(());
}
Operator::Join { left, right, .. } => {
use_indexes(left, referenced_tables)?;
use_indexes(right, referenced_tables)?;
return Ok(());
}
Operator::Order { source, .. } => {
use_indexes(source, referenced_tables)?;
return Ok(());
}
Operator::Projection { source, .. } => {
use_indexes(source, referenced_tables)?;
return Ok(());
}
Operator::Nothing => {
return Ok(());
}
}
}
// removes predicates that are always true
// returns false if there is an impossible predicate that is always false
fn eliminate_constants(operator: &mut Operator) -> Result<bool> {
match operator {
Operator::Filter {
source, predicates, ..
} => {
let mut i = 0;
while i < predicates.len() {
let predicate = &predicates[i];
if predicate.is_always_true()? {
predicates.remove(i);
} else if predicate.is_always_false()? {
return Ok(false);
} else {
i += 1;
}
}
if predicates.is_empty() {
*operator = source.take_ownership();
eliminate_constants(operator)?;
} else {
eliminate_constants(source)?;
}
return Ok(true);
}
Operator::Join {
left,
right,
predicates,
outer,
..
} => {
if !eliminate_constants(left)? {
return Ok(false);
}
if !eliminate_constants(right)? && !*outer {
return Ok(false);
}
if predicates.is_none() {
return Ok(true);
}
let predicates = predicates.as_mut().unwrap();
let mut i = 0;
while i < predicates.len() {
let predicate = &predicates[i];
if predicate.is_always_true()? {
predicates.remove(i);
} else if predicate.is_always_false()? && !*outer {
return Ok(false);
} else {
i += 1;
}
}
return Ok(true);
}
Operator::Aggregate { source, .. } => {
let ok = eliminate_constants(source)?;
if !ok {
*source = Box::new(Operator::Nothing);
}
return Ok(ok);
}
Operator::SeekRowid {
rowid_predicate,
predicates,
..
} => {
if let Some(predicates) = predicates {
let mut i = 0;
while i < predicates.len() {
let predicate = &predicates[i];
if predicate.is_always_true()? {
predicates.remove(i);
} else if predicate.is_always_false()? {
return Ok(false);
} else {
i += 1;
}
}
}
if rowid_predicate.is_always_false()? {
return Ok(false);
}
return Ok(true);
}
Operator::Limit { source, .. } => {
let ok = eliminate_constants(source)?;
if !ok {
*operator = Operator::Nothing;
}
return Ok(ok);
}
Operator::Order { source, .. } => {
let ok = eliminate_constants(source)?;
if !ok {
*operator = Operator::Nothing;
}
return Ok(true);
}
Operator::Projection { source, .. } => {
let ok = eliminate_constants(source)?;
if !ok {
*operator = Operator::Nothing;
}
return Ok(ok);
}
Operator::Scan { predicates, .. } => {
if let Some(ps) = predicates {
let mut i = 0;
while i < ps.len() {
let predicate = &ps[i];
if predicate.is_always_true()? {
ps.remove(i);
} else if predicate.is_always_false()? {
return Ok(false);
} else {
i += 1;
}
}
if ps.is_empty() {
*predicates = None;
}
}
return Ok(true);
}
Operator::Nothing => return Ok(true),
}
}
/**
Recursively pushes predicates down the tree, as far as possible.
*/
fn push_predicates(
operator: &mut Operator,
referenced_tables: &Vec<(Rc<BTreeTable>, String)>,
) -> Result<()> {
match operator {
Operator::Filter {
source, predicates, ..
} => {
let mut i = 0;
while i < predicates.len() {
// try to push the predicate to the source
// if it succeeds, remove the predicate from the filter
let predicate_owned = predicates[i].take_ownership();
let Some(predicate) = push_predicate(source, predicate_owned, referenced_tables)?
else {
predicates.remove(i);
continue;
};
predicates[i] = predicate;
i += 1;
}
if predicates.is_empty() {
*operator = source.take_ownership();
}
return Ok(());
}
Operator::Join {
left,
right,
predicates,
outer,
..
} => {
push_predicates(left, referenced_tables)?;
push_predicates(right, referenced_tables)?;
if predicates.is_none() {
return Ok(());
}
let predicates = predicates.as_mut().unwrap();
let mut i = 0;
while i < predicates.len() {
// try to push the predicate to the left side first, then to the right side
// temporarily take ownership of the predicate
let predicate_owned = predicates[i].take_ownership();
// left join predicates cant be pushed to the left side
let push_result = if *outer {
Some(predicate_owned)
} else {
push_predicate(left, predicate_owned, referenced_tables)?
};
// if the predicate was pushed to a child, remove it from the list
let Some(predicate) = push_result else {
predicates.remove(i);
continue;
};
// otherwise try to push it to the right side
// if it was pushed to the right side, remove it from the list
let Some(predicate) = push_predicate(right, predicate, referenced_tables)? else {
predicates.remove(i);
continue;
};
// otherwise keep the predicate in the list
predicates[i] = predicate;
i += 1;
}
return Ok(());
}
Operator::Aggregate { source, .. } => {
push_predicates(source, referenced_tables)?;
return Ok(());
}
Operator::SeekRowid { .. } => {
return Ok(());
}
Operator::Limit { source, .. } => {
push_predicates(source, referenced_tables)?;
return Ok(());
}
Operator::Order { source, .. } => {
push_predicates(source, referenced_tables)?;
return Ok(());
}
Operator::Projection { source, .. } => {
push_predicates(source, referenced_tables)?;
return Ok(());
}
Operator::Scan { .. } => {
return Ok(());
}
Operator::Nothing => {
return Ok(());
}
}
}
/**
Push a single predicate down the tree, as far as possible.
Returns Ok(None) if the predicate was pushed, otherwise returns itself as Ok(Some(predicate))
*/
fn push_predicate(
operator: &mut Operator,
predicate: ast::Expr,
referenced_tables: &Vec<(Rc<BTreeTable>, String)>,
) -> Result<Option<ast::Expr>> {
match operator {
Operator::Scan {
predicates,
table_identifier,
..
} => {
let table_index = referenced_tables
.iter()
.position(|(_, t_id)| t_id == table_identifier)
.unwrap();
let predicate_bitmask =
get_table_ref_bitmask_for_ast_expr(referenced_tables, &predicate)?;
// the expression is allowed to refer to tables on its left, i.e. the righter bits in the mask
// e.g. if this table is 0010, and the table on its right in the join is 0100:
// if predicate_bitmask is 0011, the predicate can be pushed (refers to this table and the table on its left)
// if predicate_bitmask is 0001, the predicate can be pushed (refers to the table on its left)
// if predicate_bitmask is 0101, the predicate can't be pushed (refers to this table and a table on its right)
let next_table_on_the_right_in_join_bitmask = 1 << (table_index + 1);
if predicate_bitmask >= next_table_on_the_right_in_join_bitmask {
return Ok(Some(predicate));
}
if predicates.is_none() {
predicates.replace(vec![predicate]);
} else {
predicates.as_mut().unwrap().push(predicate);
}
return Ok(None);
}
Operator::Filter {
source,
predicates: ps,
..
} => {
let push_result = push_predicate(source, predicate, referenced_tables)?;
if push_result.is_none() {
return Ok(None);
}
ps.push(push_result.unwrap());
return Ok(None);
}
Operator::Join {
left,
right,
predicates: join_on_preds,
outer,
..
} => {
let push_result_left = push_predicate(left, predicate, referenced_tables)?;
if push_result_left.is_none() {
return Ok(None);
}
let push_result_right =
push_predicate(right, push_result_left.unwrap(), referenced_tables)?;
if push_result_right.is_none() {
return Ok(None);
}
if *outer {
return Ok(Some(push_result_right.unwrap()));
}
let pred = push_result_right.unwrap();
let table_refs_bitmask = get_table_ref_bitmask_for_ast_expr(referenced_tables, &pred)?;
let left_bitmask = get_table_ref_bitmask_for_operator(referenced_tables, left)?;
let right_bitmask = get_table_ref_bitmask_for_operator(referenced_tables, right)?;
if table_refs_bitmask & left_bitmask == 0 || table_refs_bitmask & right_bitmask == 0 {
return Ok(Some(pred));
}
if join_on_preds.is_none() {
join_on_preds.replace(vec![pred]);
} else {
join_on_preds.as_mut().unwrap().push(pred);
}
return Ok(None);
}
Operator::Aggregate { source, .. } => {
let push_result = push_predicate(source, predicate, referenced_tables)?;
if push_result.is_none() {
return Ok(None);
}
return Ok(Some(push_result.unwrap()));
}
Operator::SeekRowid { .. } => {
return Ok(Some(predicate));
}
Operator::Limit { source, .. } => {
let push_result = push_predicate(source, predicate, referenced_tables)?;
if push_result.is_none() {
return Ok(None);
}
return Ok(Some(push_result.unwrap()));
}
Operator::Order { source, .. } => {
let push_result = push_predicate(source, predicate, referenced_tables)?;
if push_result.is_none() {
return Ok(None);
}
return Ok(Some(push_result.unwrap()));
}
Operator::Projection { source, .. } => {
let push_result = push_predicate(source, predicate, referenced_tables)?;
if push_result.is_none() {
return Ok(None);
}
return Ok(Some(push_result.unwrap()));
}
Operator::Nothing => {
return Ok(Some(predicate));
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConstantPredicate {
AlwaysTrue,
AlwaysFalse,
}
/**
Helper trait for expressions that can be optimized
Implemented for ast::Expr
*/
pub trait Optimizable {
// if the expression is a constant expression e.g. '1', returns the constant condition
fn check_constant(&self) -> Result<Option<ConstantPredicate>>;
fn is_always_true(&self) -> Result<bool> {
Ok(self
.check_constant()?
.map_or(false, |c| c == ConstantPredicate::AlwaysTrue))
}
fn is_always_false(&self) -> Result<bool> {
Ok(self
.check_constant()?
.map_or(false, |c| c == ConstantPredicate::AlwaysFalse))
}
// if the expression is the primary key of a table, returns the index of the table
fn check_primary_key(
&self,
referenced_tables: &[(Rc<BTreeTable>, String)],
) -> Result<Option<usize>>;
}
impl Optimizable for ast::Expr {
fn check_primary_key(
&self,
referenced_tables: &[(Rc<BTreeTable>, String)],
) -> Result<Option<usize>> {
match self {
ast::Expr::Id(ident) => {
let ident = normalize_ident(&ident.0);
let tables = referenced_tables
.iter()
.enumerate()
.filter_map(|(i, (t, _))| {
if t.get_column(&ident).map_or(false, |(_, c)| c.primary_key) {
Some(i)
} else {
None
}
});
let mut matches = 0;
let mut matching_tbl = None;
for tbl in tables {
matching_tbl = Some(tbl);
matches += 1;
if matches > 1 {
crate::bail_parse_error!("ambiguous column name {}", ident)
}
}
Ok(matching_tbl)
}
ast::Expr::Qualified(tbl, ident) => {
let tbl = normalize_ident(&tbl.0);
let ident = normalize_ident(&ident.0);
let table = referenced_tables.iter().enumerate().find(|(_, (t, t_id))| {
*t_id == tbl && t.get_column(&ident).map_or(false, |(_, c)| c.primary_key)
});
if table.is_none() {
return Ok(None);
}
let table = table.unwrap();
Ok(Some(table.0))
}
_ => Ok(None),
}
}
fn check_constant(&self) -> Result<Option<ConstantPredicate>> {
match self {
ast::Expr::Literal(lit) => match lit {
ast::Literal::Null => Ok(Some(ConstantPredicate::AlwaysFalse)),
ast::Literal::Numeric(b) => {
if let Ok(int_value) = b.parse::<i64>() {
return Ok(Some(if int_value == 0 {
ConstantPredicate::AlwaysFalse
} else {
ConstantPredicate::AlwaysTrue
}));
}
if let Ok(float_value) = b.parse::<f64>() {
return Ok(Some(if float_value == 0.0 {
ConstantPredicate::AlwaysFalse
} else {
ConstantPredicate::AlwaysTrue
}));
}
Ok(None)
}
ast::Literal::String(s) => {
let without_quotes = s.trim_matches('\'');
if let Ok(int_value) = without_quotes.parse::<i64>() {
return Ok(Some(if int_value == 0 {
ConstantPredicate::AlwaysFalse
} else {
ConstantPredicate::AlwaysTrue
}));
}
if let Ok(float_value) = without_quotes.parse::<f64>() {
return Ok(Some(if float_value == 0.0 {
ConstantPredicate::AlwaysFalse
} else {
ConstantPredicate::AlwaysTrue
}));
}
Ok(Some(ConstantPredicate::AlwaysFalse))
}
_ => Ok(None),
},
ast::Expr::Unary(op, expr) => {
if *op == ast::UnaryOperator::Not {
let trivial = expr.check_constant()?;
return Ok(trivial.map(|t| match t {
ConstantPredicate::AlwaysTrue => ConstantPredicate::AlwaysFalse,
ConstantPredicate::AlwaysFalse => ConstantPredicate::AlwaysTrue,
}));
}
if *op == ast::UnaryOperator::Negative {
let trivial = expr.check_constant()?;
return Ok(trivial);
}
Ok(None)
}
ast::Expr::InList { lhs: _, not, rhs } => {
if rhs.is_none() {
return Ok(Some(if *not {
ConstantPredicate::AlwaysTrue
} else {
ConstantPredicate::AlwaysFalse
}));
}
let rhs = rhs.as_ref().unwrap();
if rhs.is_empty() {
return Ok(Some(if *not {
ConstantPredicate::AlwaysTrue
} else {
ConstantPredicate::AlwaysFalse
}));
}
Ok(None)
}
ast::Expr::Binary(lhs, op, rhs) => {
let lhs_trivial = lhs.check_constant()?;
let rhs_trivial = rhs.check_constant()?;
match op {
ast::Operator::And => {
if lhs_trivial == Some(ConstantPredicate::AlwaysFalse)
|| rhs_trivial == Some(ConstantPredicate::AlwaysFalse)
{
return Ok(Some(ConstantPredicate::AlwaysFalse));
}
if lhs_trivial == Some(ConstantPredicate::AlwaysTrue)
&& rhs_trivial == Some(ConstantPredicate::AlwaysTrue)
{
return Ok(Some(ConstantPredicate::AlwaysTrue));
}
Ok(None)
}
ast::Operator::Or => {
if lhs_trivial == Some(ConstantPredicate::AlwaysTrue)
|| rhs_trivial == Some(ConstantPredicate::AlwaysTrue)
{
return Ok(Some(ConstantPredicate::AlwaysTrue));
}
if lhs_trivial == Some(ConstantPredicate::AlwaysFalse)
&& rhs_trivial == Some(ConstantPredicate::AlwaysFalse)
{
return Ok(Some(ConstantPredicate::AlwaysFalse));
}
Ok(None)
}
_ => Ok(None),
}
}
_ => Ok(None),
}
}
}
pub fn try_extract_rowid_comparison_expression(
expr: ast::Expr,
table_index: usize,
referenced_tables: &[(Rc<BTreeTable>, String)],
) -> Result<(bool, ast::Expr)> {
match expr {
ast::Expr::Binary(lhs, ast::Operator::Equals, rhs) => {
if let Some(lhs_table_index) = lhs.check_primary_key(referenced_tables)? {
if lhs_table_index == table_index {
return Ok((true, *rhs));
}
}
if let Some(rhs_table_index) = rhs.check_primary_key(referenced_tables)? {
if rhs_table_index == table_index {
return Ok((true, *lhs));
}
}
Ok((false, ast::Expr::Binary(lhs, ast::Operator::Equals, rhs)))
}
_ => Ok((false, expr)),
}
}
trait TakeOwnership {
fn take_ownership(&mut self) -> Self;
}
impl TakeOwnership for ast::Expr {
fn take_ownership(&mut self) -> Self {
std::mem::replace(self, ast::Expr::Literal(ast::Literal::Null))
}
}
impl TakeOwnership for Operator {
fn take_ownership(&mut self) -> Self {
std::mem::replace(self, Operator::Nothing)
}
}
fn replace_with<T: TakeOwnership>(expr: &mut T, mut replacement: T) {
*expr = replacement.take_ownership();
}

534
core/translate/plan.rs Normal file
View File

@@ -0,0 +1,534 @@
use core::fmt;
use std::{
fmt::{Display, Formatter},
rc::Rc,
};
use sqlite3_parser::ast;
use crate::{function::AggFunc, schema::BTreeTable, util::normalize_ident, Result};
pub struct Plan {
pub root_operator: Operator,
pub referenced_tables: Vec<(Rc<BTreeTable>, String)>,
}
impl Display for Plan {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.root_operator)
}
}
/**
An Operator is a Node in the query plan.
Operators form a tree structure, with each having zero or more children.
For example, a query like `SELECT t1.foo FROM t1 ORDER BY t1.foo LIMIT 1` would have the following structure:
Limit
Order
Project
Scan
Operators also have a unique ID, which is used to identify them in the query plan and attach metadata.
They also have a step counter, which is used to track the current step in the operator's execution.
TODO: perhaps 'step' shouldn't be in this struct, since it's an execution time concept, not a plan time concept.
*/
#[derive(Clone, Debug)]
pub enum Operator {
// Aggregate operator
// This operator is used to compute aggregate functions like SUM, AVG, COUNT, etc.
// It takes a source operator and a list of aggregate functions to compute.
// GROUP BY is not supported yet.
Aggregate {
id: usize,
source: Box<Operator>,
aggregates: Vec<Aggregate>,
step: usize,
},
// Filter operator
// This operator is used to filter rows from the source operator.
// It takes a source operator and a list of predicates to evaluate.
// Only rows for which all predicates evaluate to true are passed to the next operator.
// Generally filter operators will only exist in unoptimized plans,
// as the optimizer will try to push filters down to the lowest possible level,
// e.g. a table scan.
Filter {
id: usize,
source: Box<Operator>,
predicates: Vec<ast::Expr>,
},
// SeekRowid operator
// This operator is used to retrieve a single row from a table by its rowid.
// rowid_predicate is an expression that produces the comparison value for the rowid.
// e.g. rowid = 5, or rowid = other_table.foo
// predicates is an optional list of additional predicates to evaluate.
SeekRowid {
id: usize,
table: Rc<BTreeTable>,
table_identifier: String,
rowid_predicate: ast::Expr,
predicates: Option<Vec<ast::Expr>>,
step: usize,
},
// Limit operator
// This operator is used to limit the number of rows returned by the source operator.
Limit {
id: usize,
source: Box<Operator>,
limit: usize,
step: usize,
},
// Join operator
// This operator is used to join two source operators.
// It takes a left and right source operator, a list of predicates to evaluate,
// and a boolean indicating whether it is an outer join.
Join {
id: usize,
left: Box<Operator>,
right: Box<Operator>,
predicates: Option<Vec<ast::Expr>>,
outer: bool,
step: usize,
},
// Order operator
// This operator is used to sort the rows returned by the source operator.
Order {
id: usize,
source: Box<Operator>,
key: Vec<(ast::Expr, Direction)>,
step: usize,
},
// Projection operator
// This operator is used to project columns from the source operator.
// It takes a source operator and a list of expressions to evaluate.
// e.g. SELECT foo, bar FROM t1
// In this example, the expressions would be [foo, bar]
// and the source operator would be a Scan operator for table t1.
Projection {
id: usize,
source: Box<Operator>,
expressions: Vec<ProjectionColumn>,
step: usize,
},
// Scan operator
// This operator is used to scan a table.
// It takes a table to scan and an optional list of predicates to evaluate.
// The predicates are used to filter rows from the table.
// e.g. SELECT * FROM t1 WHERE t1.foo = 5
Scan {
id: usize,
table: Rc<BTreeTable>,
table_identifier: String,
predicates: Option<Vec<ast::Expr>>,
step: usize,
},
// Nothing operator
// This operator is used to represent an empty query.
// e.g. SELECT * from foo WHERE 0 will eventually be optimized to Nothing.
Nothing,
}
#[derive(Clone, Debug)]
pub enum ProjectionColumn {
Column(ast::Expr),
Star,
TableStar(Rc<BTreeTable>, String),
}
impl ProjectionColumn {
pub fn column_count(&self, referenced_tables: &[(Rc<BTreeTable>, String)]) -> usize {
match self {
ProjectionColumn::Column(_) => 1,
ProjectionColumn::Star => {
let mut count = 0;
for (table, _) in referenced_tables {
count += table.columns.len();
}
count
}
ProjectionColumn::TableStar(table, _) => table.columns.len(),
}
}
}
impl Operator {
pub fn column_count(&self, referenced_tables: &[(Rc<BTreeTable>, String)]) -> usize {
match self {
Operator::Aggregate { aggregates, .. } => aggregates.len(),
Operator::Filter { source, .. } => source.column_count(referenced_tables),
Operator::SeekRowid { table, .. } => table.columns.len(),
Operator::Limit { source, .. } => source.column_count(referenced_tables),
Operator::Join { left, right, .. } => {
left.column_count(referenced_tables) + right.column_count(referenced_tables)
}
Operator::Order { source, .. } => source.column_count(referenced_tables),
Operator::Projection { expressions, .. } => expressions
.iter()
.map(|e| e.column_count(referenced_tables))
.sum(),
Operator::Scan { table, .. } => table.columns.len(),
Operator::Nothing => 0,
}
}
pub fn column_names(&self) -> Vec<String> {
match self {
Operator::Aggregate { .. } => {
todo!();
}
Operator::Filter { source, .. } => source.column_names(),
Operator::SeekRowid { table, .. } => {
table.columns.iter().map(|c| c.name.clone()).collect()
}
Operator::Limit { source, .. } => source.column_names(),
Operator::Join { left, right, .. } => {
let mut names = left.column_names();
names.extend(right.column_names());
names
}
Operator::Order { source, .. } => source.column_names(),
Operator::Projection { expressions, .. } => expressions
.iter()
.map(|e| match e {
ProjectionColumn::Column(expr) => match expr {
ast::Expr::Id(ident) => ident.0.clone(),
ast::Expr::Qualified(tbl, ident) => format!("{}.{}", tbl.0, ident.0),
_ => "expr".to_string(),
},
ProjectionColumn::Star => "*".to_string(),
ProjectionColumn::TableStar(_, tbl) => format!("{}.{}", tbl, "*"),
})
.collect(),
Operator::Scan { table, .. } => table.columns.iter().map(|c| c.name.clone()).collect(),
Operator::Nothing => vec![],
}
}
pub fn id(&self) -> usize {
match self {
Operator::Aggregate { id, .. } => *id,
Operator::Filter { id, .. } => *id,
Operator::SeekRowid { id, .. } => *id,
Operator::Limit { id, .. } => *id,
Operator::Join { id, .. } => *id,
Operator::Order { id, .. } => *id,
Operator::Projection { id, .. } => *id,
Operator::Scan { id, .. } => *id,
Operator::Nothing => unreachable!(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Direction {
Ascending,
Descending,
}
impl Display for Direction {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Direction::Ascending => write!(f, "ASC"),
Direction::Descending => write!(f, "DESC"),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Aggregate {
pub func: AggFunc,
pub args: Vec<ast::Expr>,
}
impl Display for Aggregate {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let args_str = self
.args
.iter()
.map(|arg| arg.to_string())
.collect::<Vec<String>>()
.join(", ");
write!(f, "{:?}({})", self.func, args_str)
}
}
// For EXPLAIN QUERY PLAN
impl Display for Operator {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
fn fmt_operator(operator: &Operator, f: &mut Formatter, level: usize) -> fmt::Result {
let indent = " ".repeat(level);
match operator {
Operator::Aggregate {
source, aggregates, ..
} => {
// e.g. Aggregate count(*), sum(x)
let aggregates_display_string = aggregates
.iter()
.map(|agg| agg.to_string())
.collect::<Vec<String>>()
.join(", ");
writeln!(f, "{}AGGREGATE {}", indent, aggregates_display_string)?;
fmt_operator(source, f, level + 1)
}
Operator::Filter {
source, predicates, ..
} => {
let predicates_string = predicates
.iter()
.map(|p| p.to_string())
.collect::<Vec<String>>()
.join(" AND ");
writeln!(f, "{}FILTER {}", indent, predicates_string)?;
fmt_operator(source, f, level + 1)
}
Operator::SeekRowid {
table,
rowid_predicate,
predicates,
..
} => {
match predicates {
Some(ps) => {
let predicates_string = ps
.iter()
.map(|p| p.to_string())
.collect::<Vec<String>>()
.join(" AND ");
writeln!(
f,
"{}SEEK {}.rowid ON rowid={} FILTER {}",
indent, &table.name, rowid_predicate, predicates_string
)?;
}
None => writeln!(
f,
"{}SEEK {}.rowid ON rowid={}",
indent, &table.name, rowid_predicate
)?,
}
Ok(())
}
Operator::Limit { source, limit, .. } => {
writeln!(f, "{}TAKE {}", indent, limit)?;
fmt_operator(source, f, level + 1)
}
Operator::Join {
left,
right,
predicates,
outer,
..
} => {
let join_name = if *outer { "OUTER JOIN" } else { "JOIN" };
match predicates
.as_ref()
.and_then(|ps| if ps.is_empty() { None } else { Some(ps) })
{
Some(ps) => {
let predicates_string = ps
.iter()
.map(|p| p.to_string())
.collect::<Vec<String>>()
.join(" AND ");
writeln!(f, "{}{} ON {}", indent, join_name, predicates_string)?;
}
None => writeln!(f, "{}{}", indent, join_name)?,
}
fmt_operator(left, f, level + 1)?;
fmt_operator(right, f, level + 1)
}
Operator::Order { source, key, .. } => {
let sort_keys_string = key
.iter()
.map(|(expr, dir)| format!("{} {}", expr, dir))
.collect::<Vec<String>>()
.join(", ");
writeln!(f, "{}SORT {}", indent, sort_keys_string)?;
fmt_operator(source, f, level + 1)
}
Operator::Projection {
source,
expressions,
..
} => {
let expressions = expressions
.iter()
.map(|expr| match expr {
ProjectionColumn::Column(c) => c.to_string(),
ProjectionColumn::Star => "*".to_string(),
ProjectionColumn::TableStar(_, a) => format!("{}.{}", a, "*"),
})
.collect::<Vec<String>>()
.join(", ");
writeln!(f, "{}PROJECT {}", indent, expressions)?;
fmt_operator(source, f, level + 1)
}
Operator::Scan {
table,
predicates: filter,
table_identifier,
..
} => {
let table_name = format!("{} AS {}", &table.name, &table_identifier);
let filter_string = filter.as_ref().map(|f| {
let filters_string = f
.iter()
.map(|p| p.to_string())
.collect::<Vec<String>>()
.join(" AND ");
format!("FILTER {}", filters_string)
});
match filter_string {
Some(fs) => writeln!(f, "{}SCAN {} {}", indent, table_name, fs),
None => writeln!(f, "{}SCAN {}", indent, table_name),
}?;
Ok(())
}
Operator::Nothing => Ok(()),
}
}
fmt_operator(self, f, 0)
}
}
/**
Returns a bitmask where each bit corresponds to a table in the `tables` vector.
If a table is referenced in the given Operator, the corresponding bit is set to 1.
Example:
if tables = [(table1, "t1"), (table2, "t2"), (table3, "t3")],
and the Operator is a join between table2 and table3,
then the return value will be (in bits): 110
*/
pub fn get_table_ref_bitmask_for_operator<'a>(
tables: &'a Vec<(Rc<BTreeTable>, String)>,
operator: &'a Operator,
) -> Result<usize> {
let mut table_refs_mask = 0;
match operator {
Operator::Aggregate { source, .. } => {
table_refs_mask |= get_table_ref_bitmask_for_operator(tables, source)?;
}
Operator::Filter {
source, predicates, ..
} => {
table_refs_mask |= get_table_ref_bitmask_for_operator(tables, source)?;
for predicate in predicates {
table_refs_mask |= get_table_ref_bitmask_for_ast_expr(tables, predicate)?;
}
}
Operator::SeekRowid { table, .. } => {
table_refs_mask |= 1
<< tables
.iter()
.position(|(t, _)| Rc::ptr_eq(t, table))
.unwrap();
}
Operator::Limit { source, .. } => {
table_refs_mask |= get_table_ref_bitmask_for_operator(tables, source)?;
}
Operator::Join { left, right, .. } => {
table_refs_mask |= get_table_ref_bitmask_for_operator(tables, left)?;
table_refs_mask |= get_table_ref_bitmask_for_operator(tables, right)?;
}
Operator::Order { source, .. } => {
table_refs_mask |= get_table_ref_bitmask_for_operator(tables, source)?;
}
Operator::Projection { source, .. } => {
table_refs_mask |= get_table_ref_bitmask_for_operator(tables, source)?;
}
Operator::Scan { table, .. } => {
table_refs_mask |= 1
<< tables
.iter()
.position(|(t, _)| Rc::ptr_eq(t, table))
.unwrap();
}
Operator::Nothing => {}
}
Ok(table_refs_mask)
}
/**
Returns a bitmask where each bit corresponds to a table in the `tables` vector.
If a table is referenced in the given AST expression, the corresponding bit is set to 1.
Example:
if tables = [(table1, "t1"), (table2, "t2"), (table3, "t3")],
and predicate = "t1.a = t2.b"
then the return value will be (in bits): 011
*/
pub fn get_table_ref_bitmask_for_ast_expr<'a>(
tables: &'a Vec<(Rc<BTreeTable>, String)>,
predicate: &'a ast::Expr,
) -> Result<usize> {
let mut table_refs_mask = 0;
match predicate {
ast::Expr::Binary(e1, _, e2) => {
table_refs_mask |= get_table_ref_bitmask_for_ast_expr(tables, e1)?;
table_refs_mask |= get_table_ref_bitmask_for_ast_expr(tables, e2)?;
}
ast::Expr::Id(ident) => {
let ident = normalize_ident(&ident.0);
let matching_tables = tables
.iter()
.enumerate()
.filter(|(_, (table, _))| table.get_column(&ident).is_some());
let mut matches = 0;
let mut matching_tbl = None;
for table in matching_tables {
matching_tbl = Some(table);
matches += 1;
if matches > 1 {
crate::bail_parse_error!("ambiguous column name {}", &ident)
}
}
if let Some((tbl_index, _)) = matching_tbl {
table_refs_mask |= 1 << tbl_index;
} else {
crate::bail_parse_error!("column not found: {}", &ident)
}
}
ast::Expr::Qualified(tbl, ident) => {
let tbl = normalize_ident(&tbl.0);
let ident = normalize_ident(&ident.0);
let matching_table = tables
.iter()
.enumerate()
.find(|(_, (_, t_id))| *t_id == tbl);
if matching_table.is_none() {
crate::bail_parse_error!("introspect: table not found: {}", &tbl)
}
let matching_table = matching_table.unwrap();
if matching_table.1 .0.get_column(&ident).is_none() {
crate::bail_parse_error!("column with qualified name {}.{} not found", &tbl, &ident)
}
table_refs_mask |= 1 << matching_table.0;
}
ast::Expr::Literal(_) => {}
ast::Expr::Like { lhs, rhs, .. } => {
table_refs_mask |= get_table_ref_bitmask_for_ast_expr(tables, lhs)?;
table_refs_mask |= get_table_ref_bitmask_for_ast_expr(tables, rhs)?;
}
ast::Expr::FunctionCall {
args: Some(args), ..
} => {
for arg in args {
table_refs_mask |= get_table_ref_bitmask_for_ast_expr(tables, arg)?;
}
}
ast::Expr::InList { lhs, rhs, .. } => {
table_refs_mask |= get_table_ref_bitmask_for_ast_expr(tables, lhs)?;
if let Some(rhs_list) = rhs {
for rhs_expr in rhs_list {
table_refs_mask |= get_table_ref_bitmask_for_ast_expr(tables, rhs_expr)?;
}
}
}
_ => {}
}
Ok(table_refs_mask)
}

369
core/translate/planner.rs Normal file
View File

@@ -0,0 +1,369 @@
use super::plan::{Aggregate, Direction, Operator, Plan, ProjectionColumn};
use crate::{
function::Func,
schema::{BTreeTable, Schema},
util::normalize_ident,
Result,
};
use sqlite3_parser::ast::{self, FromClause, JoinType, ResultColumn};
use std::rc::Rc;
pub struct OperatorIdCounter {
id: usize,
}
impl OperatorIdCounter {
pub fn new() -> Self {
Self { id: 0 }
}
pub fn get_next_id(&mut self) -> usize {
let id = self.id;
self.id += 1;
id
}
}
pub fn prepare_select_plan<'a>(schema: &Schema, select: ast::Select) -> Result<Plan> {
match select.body.select {
ast::OneSelect::Select {
columns,
from,
where_clause,
..
} => {
let col_count = columns.len();
if col_count == 0 {
crate::bail_parse_error!("SELECT without columns is not allowed");
}
let mut operator_id_counter = OperatorIdCounter::new();
// Parse the FROM clause
let (mut operator, referenced_tables) =
parse_from(schema, from, &mut operator_id_counter)?;
// Parse the WHERE clause
if let Some(w) = where_clause {
let mut predicates = vec![];
break_predicate_at_and_boundaries(w, &mut predicates);
operator = Operator::Filter {
source: Box::new(operator),
predicates,
id: operator_id_counter.get_next_id(),
};
}
// Parse the SELECT clause to either a projection or an aggregation
// depending on the presence of aggregate functions.
// Since GROUP BY is not supported yet, mixing aggregate and non-aggregate
// columns is not allowed.
//
// If there are no aggregate functions, we can simply project the columns.
// For a simple SELECT *, the projection operator is skipped.
let is_select_star = col_count == 1 && matches!(columns[0], ast::ResultColumn::Star);
if !is_select_star {
let mut aggregate_expressions = Vec::new();
let mut scalar_expressions = Vec::with_capacity(col_count);
for column in columns.clone() {
match column {
ast::ResultColumn::Star => {
scalar_expressions.push(ProjectionColumn::Star);
}
ast::ResultColumn::TableStar(name) => {
let name_normalized = normalize_ident(name.0.as_str());
let referenced_table = referenced_tables
.iter()
.find(|(t, t_id)| *t_id == name_normalized);
if referenced_table.is_none() {
crate::bail_parse_error!("Table {} not found", name.0);
}
let (table, identifier) = referenced_table.unwrap();
scalar_expressions.push(ProjectionColumn::TableStar(
table.clone(),
identifier.clone(),
));
}
ast::ResultColumn::Expr(expr, _) => match expr {
ast::Expr::FunctionCall {
name,
distinctness,
args,
filter_over,
order_by,
} => {
let args_count = if let Some(args) = &args {
args.len()
} else {
0
};
match Func::resolve_function(
normalize_ident(name.0.as_str()).as_str(),
args_count,
) {
Ok(Func::Agg(f)) => aggregate_expressions.push(Aggregate {
func: f,
args: args.unwrap(),
}),
Ok(_) => {
scalar_expressions.push(ProjectionColumn::Column(
ast::Expr::FunctionCall {
name,
distinctness,
args,
filter_over,
order_by,
},
));
}
_ => {}
}
}
ast::Expr::FunctionCallStar { name, filter_over } => {
match Func::resolve_function(
normalize_ident(name.0.as_str()).as_str(),
0,
) {
Ok(Func::Agg(f)) => aggregate_expressions.push(Aggregate {
func: f,
args: vec![],
}),
Ok(Func::Scalar(_)) => {
scalar_expressions.push(ProjectionColumn::Column(
ast::Expr::FunctionCallStar { name, filter_over },
));
}
_ => {}
}
}
_ => {
scalar_expressions.push(ProjectionColumn::Column(expr));
}
},
}
}
let mixing_aggregate_and_non_aggregate_columns =
!aggregate_expressions.is_empty() && aggregate_expressions.len() != col_count;
if mixing_aggregate_and_non_aggregate_columns {
crate::bail_parse_error!(
"mixing aggregate and non-aggregate columns is not allowed (GROUP BY is not supported)"
);
}
if !aggregate_expressions.is_empty() {
operator = Operator::Aggregate {
source: Box::new(operator),
aggregates: aggregate_expressions,
id: operator_id_counter.get_next_id(),
step: 0,
}
} else if !scalar_expressions.is_empty() {
operator = Operator::Projection {
source: Box::new(operator),
expressions: scalar_expressions,
id: operator_id_counter.get_next_id(),
step: 0,
};
}
}
// Parse the ORDER BY clause
if let Some(order_by) = select.order_by {
let mut key = Vec::new();
for o in order_by {
// if the ORDER BY expression is a number, interpret it as an 1-indexed column number
// otherwise, interpret it normally as an expression
let expr = if let ast::Expr::Literal(ast::Literal::Numeric(num)) = o.expr {
let column_number = num.parse::<usize>()?;
if column_number == 0 {
crate::bail_parse_error!("invalid column index: {}", column_number);
}
let maybe_result_column = columns.get(column_number - 1);
match maybe_result_column {
Some(ResultColumn::Expr(expr, _)) => expr.clone(),
None => {
crate::bail_parse_error!("invalid column index: {}", column_number)
}
_ => todo!(),
}
} else {
o.expr
};
key.push((
expr,
o.order.map_or(Direction::Ascending, |o| match o {
ast::SortOrder::Asc => Direction::Ascending,
ast::SortOrder::Desc => Direction::Descending,
}),
));
}
operator = Operator::Order {
source: Box::new(operator),
key,
id: operator_id_counter.get_next_id(),
step: 0,
};
}
// Parse the LIMIT clause
if let Some(limit) = &select.limit {
operator = match &limit.expr {
ast::Expr::Literal(ast::Literal::Numeric(n)) => {
let l = n.parse()?;
if l == 0 {
Operator::Nothing
} else {
Operator::Limit {
source: Box::new(operator),
limit: l,
id: operator_id_counter.get_next_id(),
step: 0,
}
}
}
_ => todo!(),
}
}
// Return the unoptimized query plan
return Ok(Plan {
root_operator: operator,
referenced_tables,
});
}
_ => todo!(),
};
}
fn parse_from(
schema: &Schema,
from: Option<FromClause>,
operator_id_counter: &mut OperatorIdCounter,
) -> Result<(Operator, Vec<(Rc<BTreeTable>, String)>)> {
if from.as_ref().and_then(|f| f.select.as_ref()).is_none() {
return Ok((Operator::Nothing, vec![]));
}
let from = from.unwrap();
let first_table = match *from.select.unwrap() {
ast::SelectTable::Table(qualified_name, maybe_alias, _) => {
let Some(table) = schema.get_table(&qualified_name.name.0) else {
crate::bail_parse_error!("Table {} not found", qualified_name.name.0);
};
let alias = maybe_alias
.map(|a| match a {
ast::As::As(id) => id,
ast::As::Elided(id) => id,
})
.map(|a| a.0);
(table, alias.unwrap_or(qualified_name.name.0))
}
_ => todo!(),
};
let mut operator = Operator::Scan {
table: first_table.0.clone(),
predicates: None,
table_identifier: first_table.1.clone(),
id: operator_id_counter.get_next_id(),
step: 0,
};
let mut tables = vec![first_table];
for join in from.joins.unwrap_or_default().into_iter() {
let (right, outer, predicates) =
parse_join(schema, join, operator_id_counter, &mut tables)?;
operator = Operator::Join {
left: Box::new(operator),
right: Box::new(right),
predicates,
outer,
id: operator_id_counter.get_next_id(),
step: 0,
}
}
return Ok((operator, tables));
}
fn parse_join(
schema: &Schema,
join: ast::JoinedSelectTable,
operator_id_counter: &mut OperatorIdCounter,
tables: &mut Vec<(Rc<BTreeTable>, String)>,
) -> Result<(Operator, bool, Option<Vec<ast::Expr>>)> {
let ast::JoinedSelectTable {
operator,
table,
constraint,
} = join;
let table = match table {
ast::SelectTable::Table(qualified_name, maybe_alias, _) => {
let Some(table) = schema.get_table(&qualified_name.name.0) else {
crate::bail_parse_error!("Table {} not found", qualified_name.name.0);
};
let alias = maybe_alias
.map(|a| match a {
ast::As::As(id) => id,
ast::As::Elided(id) => id,
})
.map(|a| a.0);
(table, alias.unwrap_or(qualified_name.name.0))
}
_ => todo!(),
};
tables.push(table.clone());
let outer = match operator {
ast::JoinOperator::TypedJoin(Some(join_type)) => {
if join_type == JoinType::LEFT | JoinType::OUTER {
true
} else if join_type == JoinType::RIGHT | JoinType::OUTER {
true
} else {
false
}
}
_ => false,
};
let predicates = constraint.map(|c| match c {
ast::JoinConstraint::On(expr) => {
let mut predicates = vec![];
break_predicate_at_and_boundaries(expr, &mut predicates);
predicates
}
ast::JoinConstraint::Using(_) => todo!("USING joins not supported yet"),
});
Ok((
Operator::Scan {
table: table.0.clone(),
predicates: None,
table_identifier: table.1.clone(),
id: operator_id_counter.get_next_id(),
step: 0,
},
outer,
predicates,
))
}
fn break_predicate_at_and_boundaries(predicate: ast::Expr, out_predicates: &mut Vec<ast::Expr>) {
match predicate {
ast::Expr::Binary(left, ast::Operator::And, right) => {
break_predicate_at_and_boundaries(*left, out_predicates);
break_predicate_at_and_boundaries(*right, out_predicates);
}
_ => {
out_predicates.push(predicate);
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -70,18 +70,7 @@ impl ProgramBuilder {
cursor
}
pub fn has_cursor_emitted_seekrowid(&self, cursor_id: CursorID) -> bool {
(self.seekrowid_emitted_bitmask & (1 << cursor_id)) != 0
}
fn set_cursor_emitted_seekrowid(&mut self, cursor_id: CursorID) {
self.seekrowid_emitted_bitmask |= 1 << cursor_id;
}
fn _emit_insn(&mut self, insn: Insn) {
if let Insn::SeekRowid { cursor_id, .. } = insn {
self.set_cursor_emitted_seekrowid(cursor_id);
}
self.insns.push(insn);
}

View File

@@ -1120,7 +1120,8 @@ impl Program {
let record = match *cursor.record()? {
Some(ref record) => record.clone(),
None => {
todo!();
state.pc += 1;
continue;
}
};
state.registers[*dest_reg] = OwnedValue::Record(record.clone());

View File

@@ -106,7 +106,7 @@ impl Cursor for Sorter {
}
fn get_null_flag(&self) -> bool {
todo!();
false
}
fn exists(&mut self, key: &OwnedValue) -> Result<CursorResult<bool>> {