mirror of
https://github.com/tursodatabase/limbo.git
synced 2025-08-04 18:18:03 +00:00
Merge 'refactor: replace Operation::Subquery with Table::FromClauseSubquery' from Jussi Saurio
Previously the Operation enum consisted of: - Operation::Scan - Operation::Search - Operation::Subquery Which was always a dumb hack because what we really are doing is an Operation::Scan on a "virtual"/"pseudo" table (overloaded names...) derived from a subquery appearing in the FROM clause. Hence, refactor the relevant data structures so that the Table enum now contains a new variant: Table::FromClauseSubquery And the Operation enum only consists of Scan and Search. ``` SELECT * FROM (SELECT ...) sub; -- the subquery here was previously interpreted as Operation::Subquery on a Table::Pseudo, -- with a lot of special handling for Operation::Subquery in different code paths -- now it's an Operation::Scan on a Table::FromClauseSubquery ``` No functional changes (intended, at least!) Reviewed-by: Pere Diaz Bou <pere-altea@homail.com> Closes #1529
This commit is contained in:
commit
14058357ad
9 changed files with 326 additions and 324 deletions
|
@ -1,4 +1,5 @@
|
|||
use crate::translate::collate::CollationSeq;
|
||||
use crate::translate::plan::SelectPlan;
|
||||
use crate::{util::normalize_ident, Result};
|
||||
use crate::{LimboError, VirtualTable};
|
||||
use core::fmt;
|
||||
|
@ -109,6 +110,7 @@ pub enum Table {
|
|||
BTree(Rc<BTreeTable>),
|
||||
Pseudo(Rc<PseudoTable>),
|
||||
Virtual(Rc<VirtualTable>),
|
||||
FromClauseSubquery(FromClauseSubquery),
|
||||
}
|
||||
|
||||
impl Table {
|
||||
|
@ -117,6 +119,7 @@ impl Table {
|
|||
Table::BTree(table) => table.root_page,
|
||||
Table::Pseudo(_) => unimplemented!(),
|
||||
Table::Virtual(_) => unimplemented!(),
|
||||
Table::FromClauseSubquery(_) => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -125,6 +128,7 @@ impl Table {
|
|||
Self::BTree(table) => &table.name,
|
||||
Self::Pseudo(_) => "",
|
||||
Self::Virtual(table) => &table.name,
|
||||
Self::FromClauseSubquery(from_clause_subquery) => &from_clause_subquery.name,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -133,6 +137,9 @@ impl Table {
|
|||
Self::BTree(table) => table.columns.get(index),
|
||||
Self::Pseudo(table) => table.columns.get(index),
|
||||
Self::Virtual(table) => table.columns.get(index),
|
||||
Self::FromClauseSubquery(from_clause_subquery) => {
|
||||
from_clause_subquery.columns.get(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -141,6 +148,7 @@ impl Table {
|
|||
Self::BTree(table) => &table.columns,
|
||||
Self::Pseudo(table) => &table.columns,
|
||||
Self::Virtual(table) => &table.columns,
|
||||
Self::FromClauseSubquery(from_clause_subquery) => &from_clause_subquery.columns,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -149,6 +157,7 @@ impl Table {
|
|||
Self::BTree(table) => Some(table.clone()),
|
||||
Self::Pseudo(_) => None,
|
||||
Self::Virtual(_) => None,
|
||||
Self::FromClauseSubquery(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -280,6 +289,20 @@ impl PseudoTable {
|
|||
}
|
||||
}
|
||||
|
||||
/// A derived table from a FROM clause subquery.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FromClauseSubquery {
|
||||
/// The name of the derived table; uses the alias if available.
|
||||
pub name: String,
|
||||
/// The query plan for the derived table.
|
||||
pub plan: Box<SelectPlan>,
|
||||
/// The columns of the derived table.
|
||||
pub columns: Vec<Column>,
|
||||
/// The start register for the result columns of the derived table;
|
||||
/// must be set before data is read from it.
|
||||
pub result_columns_start_reg: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq)]
|
||||
struct UniqueColumnProps {
|
||||
column_name: String,
|
||||
|
|
|
@ -451,7 +451,6 @@ fn emit_delete_insns(
|
|||
index: Some(index), ..
|
||||
} => program.resolve_cursor_id(&index.name),
|
||||
},
|
||||
_ => return Ok(()),
|
||||
};
|
||||
let main_table_cursor_id = program.resolve_cursor_id(table_reference.table.get_name());
|
||||
|
||||
|
@ -674,7 +673,6 @@ fn emit_update_insns(
|
|||
false,
|
||||
),
|
||||
},
|
||||
_ => return Ok(()),
|
||||
};
|
||||
|
||||
for cond in plan
|
||||
|
|
|
@ -2,7 +2,7 @@ use limbo_sqlite3_parser::ast::{self, UnaryOperator};
|
|||
|
||||
use super::emitter::Resolver;
|
||||
use super::optimizer::Optimizable;
|
||||
use super::plan::{Operation, TableReference};
|
||||
use super::plan::TableReference;
|
||||
#[cfg(feature = "json")]
|
||||
use crate::function::JsonFunc;
|
||||
use crate::function::{Func, FuncCtx, MathFuncArity, ScalarFunc, VectorFunc};
|
||||
|
@ -1799,84 +1799,87 @@ pub fn translate_expr(
|
|||
};
|
||||
// Counter intuitive but a column always needs to have a collation
|
||||
program.set_collation(Some((table_column.collation.unwrap_or_default(), false)));
|
||||
match table_reference.op {
|
||||
// If we are reading a column from a table, we find the cursor that corresponds to
|
||||
// the table and read the column from the cursor.
|
||||
// If we have a covering index, we don't have an open table cursor so we read from the index cursor.
|
||||
Operation::Scan { .. } | Operation::Search(_) => {
|
||||
match &table_reference.table {
|
||||
Table::BTree(_) => {
|
||||
let table_cursor_id = if use_covering_index {
|
||||
None
|
||||
} else {
|
||||
Some(program.resolve_cursor_id(&table_reference.identifier))
|
||||
};
|
||||
let index_cursor_id =
|
||||
index.map(|index| program.resolve_cursor_id(&index.name));
|
||||
if *is_rowid_alias {
|
||||
if let Some(index_cursor_id) = index_cursor_id {
|
||||
program.emit_insn(Insn::IdxRowId {
|
||||
cursor_id: index_cursor_id,
|
||||
dest: target_register,
|
||||
});
|
||||
} else if let Some(table_cursor_id) = table_cursor_id {
|
||||
program.emit_insn(Insn::RowId {
|
||||
cursor_id: table_cursor_id,
|
||||
dest: target_register,
|
||||
});
|
||||
} else {
|
||||
unreachable!("Either index or table cursor must be opened");
|
||||
}
|
||||
} else {
|
||||
let read_cursor = if use_covering_index {
|
||||
index_cursor_id
|
||||
.expect("index cursor should be opened when use_covering_index=true")
|
||||
} else {
|
||||
table_cursor_id
|
||||
.expect("table cursor should be opened when use_covering_index=false")
|
||||
};
|
||||
let column = if use_covering_index {
|
||||
let index = index.expect("index cursor should be opened when use_covering_index=true");
|
||||
index.column_table_pos_to_index_pos(*column).unwrap_or_else(|| {
|
||||
panic!("covering index {} does not contain column number {} of table {}", index.name, column, table_reference.identifier)
|
||||
})
|
||||
} else {
|
||||
*column
|
||||
};
|
||||
program.emit_insn(Insn::Column {
|
||||
cursor_id: read_cursor,
|
||||
column,
|
||||
dest: target_register,
|
||||
});
|
||||
}
|
||||
maybe_apply_affinity(table_column.ty, target_register, program);
|
||||
Ok(target_register)
|
||||
}
|
||||
Table::Virtual(_) => {
|
||||
let cursor_id = program.resolve_cursor_id(&table_reference.identifier);
|
||||
program.emit_insn(Insn::VColumn {
|
||||
cursor_id,
|
||||
column: *column,
|
||||
|
||||
// If we are reading a column from a table, we find the cursor that corresponds to
|
||||
// the table and read the column from the cursor.
|
||||
// If we have a covering index, we don't have an open table cursor so we read from the index cursor.
|
||||
match &table_reference.table {
|
||||
Table::BTree(_) => {
|
||||
let table_cursor_id = if use_covering_index {
|
||||
None
|
||||
} else {
|
||||
Some(program.resolve_cursor_id(&table_reference.identifier))
|
||||
};
|
||||
let index_cursor_id = index.map(|index| program.resolve_cursor_id(&index.name));
|
||||
if *is_rowid_alias {
|
||||
if let Some(index_cursor_id) = index_cursor_id {
|
||||
program.emit_insn(Insn::IdxRowId {
|
||||
cursor_id: index_cursor_id,
|
||||
dest: target_register,
|
||||
});
|
||||
Ok(target_register)
|
||||
} else if let Some(table_cursor_id) = table_cursor_id {
|
||||
program.emit_insn(Insn::RowId {
|
||||
cursor_id: table_cursor_id,
|
||||
dest: target_register,
|
||||
});
|
||||
} else {
|
||||
unreachable!("Either index or table cursor must be opened");
|
||||
}
|
||||
_ => unreachable!(),
|
||||
} else {
|
||||
let read_cursor = if use_covering_index {
|
||||
index_cursor_id.expect(
|
||||
"index cursor should be opened when use_covering_index=true",
|
||||
)
|
||||
} else {
|
||||
table_cursor_id.expect(
|
||||
"table cursor should be opened when use_covering_index=false",
|
||||
)
|
||||
};
|
||||
let column = if use_covering_index {
|
||||
let index = index.expect(
|
||||
"index cursor should be opened when use_covering_index=true",
|
||||
);
|
||||
index.column_table_pos_to_index_pos(*column).unwrap_or_else(|| {
|
||||
panic!("covering index {} does not contain column number {} of table {}", index.name, column, table_reference.identifier)
|
||||
})
|
||||
} else {
|
||||
*column
|
||||
};
|
||||
program.emit_insn(Insn::Column {
|
||||
cursor_id: read_cursor,
|
||||
column,
|
||||
dest: target_register,
|
||||
});
|
||||
}
|
||||
let Some(column) = table_reference.table.get_column_at(*column) else {
|
||||
crate::bail_parse_error!("column index out of bounds");
|
||||
};
|
||||
maybe_apply_affinity(column.ty, target_register, program);
|
||||
Ok(target_register)
|
||||
}
|
||||
// If we are reading a column from a subquery, we instead copy the column from the
|
||||
// subquery's result registers.
|
||||
Operation::Subquery {
|
||||
result_columns_start_reg,
|
||||
..
|
||||
} => {
|
||||
Table::FromClauseSubquery(from_clause_subquery) => {
|
||||
// If we are reading a column from a subquery, we instead copy the column from the
|
||||
// subquery's result registers.
|
||||
program.emit_insn(Insn::Copy {
|
||||
src_reg: result_columns_start_reg + *column,
|
||||
src_reg: from_clause_subquery
|
||||
.result_columns_start_reg
|
||||
.expect("Subquery result_columns_start_reg must be set")
|
||||
+ *column,
|
||||
dst_reg: target_register,
|
||||
amount: 0,
|
||||
});
|
||||
Ok(target_register)
|
||||
}
|
||||
Table::Virtual(_) => {
|
||||
let cursor_id = program.resolve_cursor_id(&table_reference.identifier);
|
||||
program.emit_insn(Insn::VColumn {
|
||||
cursor_id,
|
||||
column: *column,
|
||||
dest: target_register,
|
||||
});
|
||||
Ok(target_register)
|
||||
}
|
||||
Table::Pseudo(_) => panic!("Column access on pseudo table"),
|
||||
}
|
||||
}
|
||||
ast::Expr::RowId { database: _, table } => {
|
||||
|
|
|
@ -237,7 +237,6 @@ pub fn init_loop(
|
|||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -282,175 +281,164 @@ pub fn open_loop(
|
|||
let (table_cursor_id, index_cursor_id) = table.resolve_cursors(program)?;
|
||||
|
||||
match &table.op {
|
||||
Operation::Subquery { plan, .. } => {
|
||||
let (yield_reg, coroutine_implementation_start) = match &plan.query_type {
|
||||
SelectQueryType::Subquery {
|
||||
yield_reg,
|
||||
coroutine_implementation_start,
|
||||
} => (*yield_reg, *coroutine_implementation_start),
|
||||
_ => unreachable!("Subquery operator with non-subquery query type"),
|
||||
};
|
||||
// In case the subquery is an inner loop, it needs to be reinitialized on each iteration of the outer loop.
|
||||
program.emit_insn(Insn::InitCoroutine {
|
||||
yield_reg,
|
||||
jump_on_definition: BranchOffset::Offset(0),
|
||||
start_offset: coroutine_implementation_start,
|
||||
});
|
||||
program.preassign_label_to_next_insn(loop_start);
|
||||
// A subquery within the main loop of a parent query has no cursor, so instead of advancing the cursor,
|
||||
// it emits a Yield which jumps back to the main loop of the subquery itself to retrieve the next row.
|
||||
// When the subquery coroutine completes, this instruction jumps to the label at the top of the termination_label_stack,
|
||||
// which in this case is the end of the Yield-Goto loop in the parent query.
|
||||
program.emit_insn(Insn::Yield {
|
||||
yield_reg,
|
||||
end_offset: loop_end,
|
||||
});
|
||||
|
||||
for cond in predicates
|
||||
.iter()
|
||||
.filter(|cond| cond.should_eval_at_loop(join_index, join_order))
|
||||
{
|
||||
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: next,
|
||||
};
|
||||
translate_condition_expr(
|
||||
program,
|
||||
tables,
|
||||
&cond.expr,
|
||||
condition_metadata,
|
||||
&t_ctx.resolver,
|
||||
)?;
|
||||
program.preassign_label_to_next_insn(jump_target_when_true);
|
||||
}
|
||||
}
|
||||
Operation::Scan { iter_dir, .. } => {
|
||||
let iteration_cursor_id = index_cursor_id.unwrap_or_else(|| {
|
||||
table_cursor_id.expect("Either index or table cursor must be opened")
|
||||
});
|
||||
if !matches!(&table.table, Table::Virtual(_)) {
|
||||
if *iter_dir == IterationDirection::Backwards {
|
||||
program.emit_insn(Insn::Last {
|
||||
cursor_id: iteration_cursor_id,
|
||||
pc_if_empty: loop_end,
|
||||
});
|
||||
} else {
|
||||
program.emit_insn(Insn::Rewind {
|
||||
cursor_id: iteration_cursor_id,
|
||||
pc_if_empty: loop_end,
|
||||
match &table.table {
|
||||
Table::BTree(_) => {
|
||||
let iteration_cursor_id = index_cursor_id.unwrap_or_else(|| {
|
||||
table_cursor_id.expect("Either index or table cursor must be opened")
|
||||
});
|
||||
if *iter_dir == IterationDirection::Backwards {
|
||||
program.emit_insn(Insn::Last {
|
||||
cursor_id: iteration_cursor_id,
|
||||
pc_if_empty: loop_end,
|
||||
});
|
||||
} else {
|
||||
program.emit_insn(Insn::Rewind {
|
||||
cursor_id: iteration_cursor_id,
|
||||
pc_if_empty: loop_end,
|
||||
});
|
||||
}
|
||||
program.preassign_label_to_next_insn(loop_start);
|
||||
}
|
||||
program.preassign_label_to_next_insn(loop_start);
|
||||
} else if let Some(vtab) = table.virtual_table() {
|
||||
let (start_reg, count, maybe_idx_str, maybe_idx_int) = if vtab
|
||||
.kind
|
||||
.eq(&VTabKind::VirtualTable)
|
||||
{
|
||||
// Virtual‑table (non‑TVF) modules can receive constraints via xBestIndex.
|
||||
// They return information with which to pass to VFilter operation.
|
||||
// We forward every predicate that touches vtab columns.
|
||||
//
|
||||
// vtab.col = literal (always usable)
|
||||
// vtab.col = outer_table.col (usable, because outer_table is already positioned)
|
||||
// vtab.col = later_table.col (forwarded with usable = false)
|
||||
//
|
||||
// xBestIndex decides which ones it wants by setting argvIndex and whether the
|
||||
// core layer may omit them (omit = true).
|
||||
// We then materialise the RHS/LHS into registers before issuing VFilter.
|
||||
let converted_constraints = predicates
|
||||
.iter()
|
||||
.filter(|p| p.should_eval_at_loop(join_index, join_order))
|
||||
.enumerate()
|
||||
.filter_map(|(i, p)| {
|
||||
// Build ConstraintInfo from the predicates
|
||||
convert_where_to_vtab_constraint(p, table_index, i)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
// TODO: get proper order_by information to pass to the vtab.
|
||||
// maybe encode more info on t_ctx? we need: [col_idx, is_descending]
|
||||
let index_info = vtab.best_index(&converted_constraints, &[]);
|
||||
Table::Virtual(vtab) => {
|
||||
let (start_reg, count, maybe_idx_str, maybe_idx_int) =
|
||||
if vtab.kind.eq(&VTabKind::VirtualTable) {
|
||||
// Virtual‑table (non‑TVF) modules can receive constraints via xBestIndex.
|
||||
// They return information with which to pass to VFilter operation.
|
||||
// We forward every predicate that touches vtab columns.
|
||||
//
|
||||
// vtab.col = literal (always usable)
|
||||
// vtab.col = outer_table.col (usable, because outer_table is already positioned)
|
||||
// vtab.col = later_table.col (forwarded with usable = false)
|
||||
//
|
||||
// xBestIndex decides which ones it wants by setting argvIndex and whether the
|
||||
// core layer may omit them (omit = true).
|
||||
// We then materialise the RHS/LHS into registers before issuing VFilter.
|
||||
let converted_constraints = predicates
|
||||
.iter()
|
||||
.filter(|p| p.should_eval_at_loop(join_index, join_order))
|
||||
.enumerate()
|
||||
.filter_map(|(i, p)| {
|
||||
// Build ConstraintInfo from the predicates
|
||||
convert_where_to_vtab_constraint(p, table_index, i)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
// TODO: get proper order_by information to pass to the vtab.
|
||||
// maybe encode more info on t_ctx? we need: [col_idx, is_descending]
|
||||
let index_info = vtab.best_index(&converted_constraints, &[]);
|
||||
|
||||
// Determine the number of VFilter arguments (constraints with an argv_index).
|
||||
let args_needed = index_info
|
||||
.constraint_usages
|
||||
.iter()
|
||||
.filter(|u| u.argv_index.is_some())
|
||||
.count();
|
||||
let start_reg = program.alloc_registers(args_needed);
|
||||
// Determine the number of VFilter arguments (constraints with an argv_index).
|
||||
let args_needed = index_info
|
||||
.constraint_usages
|
||||
.iter()
|
||||
.filter(|u| u.argv_index.is_some())
|
||||
.count();
|
||||
let start_reg = program.alloc_registers(args_needed);
|
||||
|
||||
// For each constraint used by best_index, translate the opposite side.
|
||||
for (i, usage) in index_info.constraint_usages.iter().enumerate() {
|
||||
if let Some(argv_index) = usage.argv_index {
|
||||
if let Some(cinfo) = converted_constraints.get(i) {
|
||||
let (pred_idx, is_rhs) = cinfo.unpack_plan_info();
|
||||
if let ast::Expr::Binary(lhs, _, rhs) =
|
||||
&predicates[pred_idx].expr
|
||||
{
|
||||
// translate the opposite side of the referenced vtab column
|
||||
let expr = if is_rhs { lhs } else { rhs };
|
||||
// argv_index is 1-based; adjust to get the proper register offset.
|
||||
let target_reg = start_reg + (argv_index - 1) as usize;
|
||||
translate_expr(
|
||||
program,
|
||||
Some(tables),
|
||||
expr,
|
||||
target_reg,
|
||||
&t_ctx.resolver,
|
||||
)?;
|
||||
if cinfo.usable && usage.omit {
|
||||
predicates[pred_idx].consumed = true;
|
||||
// For each constraint used by best_index, translate the opposite side.
|
||||
for (i, usage) in index_info.constraint_usages.iter().enumerate() {
|
||||
if let Some(argv_index) = usage.argv_index {
|
||||
if let Some(cinfo) = converted_constraints.get(i) {
|
||||
let (pred_idx, is_rhs) = cinfo.unpack_plan_info();
|
||||
if let ast::Expr::Binary(lhs, _, rhs) =
|
||||
&predicates[pred_idx].expr
|
||||
{
|
||||
// translate the opposite side of the referenced vtab column
|
||||
let expr = if is_rhs { lhs } else { rhs };
|
||||
// argv_index is 1-based; adjust to get the proper register offset.
|
||||
let target_reg =
|
||||
start_reg + (argv_index - 1) as usize;
|
||||
translate_expr(
|
||||
program,
|
||||
Some(tables),
|
||||
expr,
|
||||
target_reg,
|
||||
&t_ctx.resolver,
|
||||
)?;
|
||||
if cinfo.usable && usage.omit {
|
||||
predicates[pred_idx].consumed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// If best_index provided an idx_str, translate it.
|
||||
let maybe_idx_str = if let Some(idx_str) = index_info.idx_str {
|
||||
let reg = program.alloc_register();
|
||||
program.emit_insn(Insn::String8 {
|
||||
dest: reg,
|
||||
value: idx_str,
|
||||
});
|
||||
Some(reg)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
(
|
||||
start_reg,
|
||||
args_needed,
|
||||
maybe_idx_str,
|
||||
Some(index_info.idx_num),
|
||||
)
|
||||
} else {
|
||||
// For table-valued functions: translate the table args.
|
||||
let args = match vtab.args.as_ref() {
|
||||
Some(args) => args,
|
||||
None => &vec![],
|
||||
};
|
||||
let start_reg = program.alloc_registers(args.len());
|
||||
let mut cur_reg = start_reg;
|
||||
for arg in args {
|
||||
let reg = cur_reg;
|
||||
cur_reg += 1;
|
||||
let _ =
|
||||
translate_expr(program, Some(tables), arg, reg, &t_ctx.resolver)?;
|
||||
}
|
||||
(start_reg, args.len(), None, None)
|
||||
};
|
||||
// If best_index provided an idx_str, translate it.
|
||||
let maybe_idx_str = if let Some(idx_str) = index_info.idx_str {
|
||||
let reg = program.alloc_register();
|
||||
program.emit_insn(Insn::String8 {
|
||||
dest: reg,
|
||||
value: idx_str,
|
||||
});
|
||||
Some(reg)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
(
|
||||
start_reg,
|
||||
args_needed,
|
||||
maybe_idx_str,
|
||||
Some(index_info.idx_num),
|
||||
)
|
||||
} else {
|
||||
// For table-valued functions: translate the table args.
|
||||
let args = match vtab.args.as_ref() {
|
||||
Some(args) => args,
|
||||
None => &vec![],
|
||||
};
|
||||
let start_reg = program.alloc_registers(args.len());
|
||||
let mut cur_reg = start_reg;
|
||||
for arg in args {
|
||||
let reg = cur_reg;
|
||||
cur_reg += 1;
|
||||
let _ = translate_expr(
|
||||
program,
|
||||
Some(tables),
|
||||
arg,
|
||||
reg,
|
||||
&t_ctx.resolver,
|
||||
)?;
|
||||
}
|
||||
(start_reg, args.len(), None, None)
|
||||
};
|
||||
|
||||
// Emit VFilter with the computed arguments.
|
||||
program.emit_insn(Insn::VFilter {
|
||||
cursor_id: table_cursor_id
|
||||
.expect("Virtual tables do not support covering indexes"),
|
||||
arg_count: count,
|
||||
args_reg: start_reg,
|
||||
idx_str: maybe_idx_str,
|
||||
idx_num: maybe_idx_int.unwrap_or(0) as usize,
|
||||
pc_if_empty: loop_end,
|
||||
});
|
||||
program.preassign_label_to_next_insn(loop_start);
|
||||
// Emit VFilter with the computed arguments.
|
||||
program.emit_insn(Insn::VFilter {
|
||||
cursor_id: table_cursor_id
|
||||
.expect("Virtual tables do not support covering indexes"),
|
||||
arg_count: count,
|
||||
args_reg: start_reg,
|
||||
idx_str: maybe_idx_str,
|
||||
idx_num: maybe_idx_int.unwrap_or(0) as usize,
|
||||
pc_if_empty: loop_end,
|
||||
});
|
||||
program.preassign_label_to_next_insn(loop_start);
|
||||
}
|
||||
Table::FromClauseSubquery(from_clause_subquery) => {
|
||||
let (yield_reg, coroutine_implementation_start) =
|
||||
match &from_clause_subquery.plan.query_type {
|
||||
SelectQueryType::Subquery {
|
||||
yield_reg,
|
||||
coroutine_implementation_start,
|
||||
} => (*yield_reg, *coroutine_implementation_start),
|
||||
_ => unreachable!("Subquery table with non-subquery query type"),
|
||||
};
|
||||
// In case the subquery is an inner loop, it needs to be reinitialized on each iteration of the outer loop.
|
||||
program.emit_insn(Insn::InitCoroutine {
|
||||
yield_reg,
|
||||
jump_on_definition: BranchOffset::Offset(0),
|
||||
start_offset: coroutine_implementation_start,
|
||||
});
|
||||
program.preassign_label_to_next_insn(loop_start);
|
||||
// A subquery within the main loop of a parent query has no cursor, so instead of advancing the cursor,
|
||||
// it emits a Yield which jumps back to the main loop of the subquery itself to retrieve the next row.
|
||||
// When the subquery coroutine completes, this instruction jumps to the label at the top of the termination_label_stack,
|
||||
// which in this case is the end of the Yield-Goto loop in the parent query.
|
||||
program.emit_insn(Insn::Yield {
|
||||
yield_reg,
|
||||
end_offset: loop_end,
|
||||
});
|
||||
}
|
||||
Table::Pseudo(_) => panic!("Pseudo tables should not loop"),
|
||||
}
|
||||
|
||||
if let Some(table_cursor_id) = table_cursor_id {
|
||||
|
@ -483,6 +471,10 @@ pub fn open_loop(
|
|||
}
|
||||
}
|
||||
Operation::Search(search) => {
|
||||
assert!(
|
||||
!matches!(table.table, Table::FromClauseSubquery(_)),
|
||||
"Subqueries do not support index seeks"
|
||||
);
|
||||
// Open the loop for the index search.
|
||||
// Rowid equality point lookups are handled with a SeekRowid instruction which does not loop, since it is a single row lookup.
|
||||
if let Search::RowidEq { cmp_expr } = search {
|
||||
|
@ -874,23 +866,13 @@ pub fn close_loop(
|
|||
let (table_cursor_id, index_cursor_id) = table.resolve_cursors(program)?;
|
||||
|
||||
match &table.op {
|
||||
Operation::Subquery { .. } => {
|
||||
program.resolve_label(loop_labels.next, program.offset());
|
||||
// A subquery has no cursor to call Next on, so it just emits a Goto
|
||||
// to the Yield instruction, which in turn jumps back to the main loop of the subquery,
|
||||
// so that the next row from the subquery can be read.
|
||||
program.emit_insn(Insn::Goto {
|
||||
target_pc: loop_labels.loop_start,
|
||||
});
|
||||
program.preassign_label_to_next_insn(loop_labels.loop_end);
|
||||
}
|
||||
Operation::Scan { iter_dir, .. } => {
|
||||
program.resolve_label(loop_labels.next, program.offset());
|
||||
let iteration_cursor_id = index_cursor_id.unwrap_or_else(|| {
|
||||
table_cursor_id.expect("Either index or table cursor must be opened")
|
||||
});
|
||||
match &table.table {
|
||||
Table::BTree(_) => {
|
||||
let iteration_cursor_id = index_cursor_id.unwrap_or_else(|| {
|
||||
table_cursor_id.expect("Either index or table cursor must be opened")
|
||||
});
|
||||
if *iter_dir == IterationDirection::Backwards {
|
||||
program.emit_insn(Insn::Prev {
|
||||
cursor_id: iteration_cursor_id,
|
||||
|
@ -910,11 +892,23 @@ pub fn close_loop(
|
|||
pc_if_next: loop_labels.loop_start,
|
||||
});
|
||||
}
|
||||
Table::FromClauseSubquery(_) => {
|
||||
// A subquery has no cursor to call Next on, so it just emits a Goto
|
||||
// to the Yield instruction, which in turn jumps back to the main loop of the subquery,
|
||||
// so that the next row from the subquery can be read.
|
||||
program.emit_insn(Insn::Goto {
|
||||
target_pc: loop_labels.loop_start,
|
||||
});
|
||||
}
|
||||
other => unreachable!("Unsupported table reference type: {:?}", other),
|
||||
}
|
||||
program.preassign_label_to_next_insn(loop_labels.loop_end);
|
||||
}
|
||||
Operation::Search(search) => {
|
||||
assert!(
|
||||
!matches!(table.table, Table::FromClauseSubquery(_)),
|
||||
"Subqueries do not support index seeks"
|
||||
);
|
||||
program.resolve_label(loop_labels.next, program.offset());
|
||||
let iteration_cursor_id = index_cursor_id.unwrap_or_else(|| {
|
||||
table_cursor_id.expect("Either index or table cursor must be opened")
|
||||
|
|
|
@ -10,7 +10,7 @@ use order::{compute_order_target, plan_satisfies_order_target, EliminatesSort};
|
|||
|
||||
use crate::{
|
||||
parameters::PARAM_PREFIX,
|
||||
schema::{Index, IndexColumn, Schema},
|
||||
schema::{Index, IndexColumn, Schema, Table},
|
||||
translate::plan::TerminationKey,
|
||||
types::SeekOp,
|
||||
Result,
|
||||
|
@ -108,8 +108,8 @@ fn optimize_update_plan(plan: &mut UpdatePlan, schema: &Schema) -> Result<()> {
|
|||
|
||||
fn optimize_subqueries(plan: &mut SelectPlan, schema: &Schema) -> Result<()> {
|
||||
for table in plan.table_references.iter_mut() {
|
||||
if let Operation::Subquery { plan, .. } = &mut table.op {
|
||||
optimize_select_plan(&mut *plan, schema)?;
|
||||
if let Table::FromClauseSubquery(from_clause_subquery) = &mut table.table {
|
||||
optimize_select_plan(&mut from_clause_subquery.plan, schema)?;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -213,21 +213,17 @@ fn optimize_table_access(
|
|||
for (i, join_order_member) in best_join_order.iter().enumerate() {
|
||||
let table_number = join_order_member.table_no;
|
||||
let access_method = &access_methods_arena.borrow()[best_access_methods[i]];
|
||||
if matches!(
|
||||
table_references[table_number].op,
|
||||
Operation::Subquery { .. }
|
||||
) {
|
||||
// FIXME: Operation::Subquery shouldn't exist. It's not an operation, it's a kind of temporary table.
|
||||
assert!(
|
||||
access_method.is_scan(),
|
||||
"nothing in the current optimizer should be able to optimize subqueries, but got {:?} for table {}",
|
||||
access_method,
|
||||
table_references[table_number].table.get_name()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if access_method.is_scan() {
|
||||
if access_method.index.is_some() || i == 0 {
|
||||
let is_leftmost_table = i == 0;
|
||||
let uses_index = access_method.index.is_some();
|
||||
let source_table_is_from_clause_subquery = matches!(
|
||||
&table_references[table_number].table,
|
||||
Table::FromClauseSubquery(_)
|
||||
);
|
||||
|
||||
let try_to_build_ephemeral_index =
|
||||
!is_leftmost_table && !uses_index && !source_table_is_from_clause_subquery;
|
||||
if !try_to_build_ephemeral_index {
|
||||
table_references[table_number].op = Operation::Scan {
|
||||
iter_dir: access_method.iter_dir,
|
||||
index: access_method.index.clone(),
|
||||
|
|
|
@ -10,7 +10,7 @@ use std::{
|
|||
|
||||
use crate::{
|
||||
function::AggFunc,
|
||||
schema::{BTreeTable, Column, Index, Table},
|
||||
schema::{BTreeTable, Column, FromClauseSubquery, Index, Table},
|
||||
util::exprs_are_equivalent,
|
||||
vdbe::{
|
||||
builder::{CursorType, ProgramBuilder},
|
||||
|
@ -18,11 +18,7 @@ use crate::{
|
|||
},
|
||||
Result, VirtualTable,
|
||||
};
|
||||
use crate::{
|
||||
schema::{PseudoTable, Type},
|
||||
types::SeekOp,
|
||||
util::can_pushdown_predicate,
|
||||
};
|
||||
use crate::{schema::Type, types::SeekOp, util::can_pushdown_predicate};
|
||||
|
||||
use super::{emitter::OperationMode, planner::determine_where_to_eval_term, schema::ParseSchema};
|
||||
|
||||
|
@ -561,13 +557,6 @@ pub enum Operation {
|
|||
// This operation is used to search for a row in a table using an index
|
||||
// (i.e. a primary key or a secondary index)
|
||||
Search(Search),
|
||||
/// Subquery operation
|
||||
/// This operation is used to represent a subquery in the query plan.
|
||||
/// The subquery itself (recursively) contains an arbitrary SelectPlan.
|
||||
Subquery {
|
||||
plan: Box<SelectPlan>,
|
||||
result_columns_start_reg: usize,
|
||||
},
|
||||
}
|
||||
|
||||
impl Operation {
|
||||
|
@ -576,7 +565,6 @@ impl Operation {
|
|||
Operation::Scan { index, .. } => index.as_ref(),
|
||||
Operation::Search(Search::RowidEq { .. }) => None,
|
||||
Operation::Search(Search::Seek { index, .. }) => index.as_ref(),
|
||||
Operation::Subquery { .. } => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -598,29 +586,35 @@ impl TableReference {
|
|||
|
||||
/// Creates a new TableReference for a subquery.
|
||||
pub fn new_subquery(identifier: String, plan: SelectPlan, join_info: Option<JoinInfo>) -> Self {
|
||||
let table = Table::Pseudo(Rc::new(PseudoTable::new_with_columns(
|
||||
plan.result_columns
|
||||
.iter()
|
||||
.map(|rc| Column {
|
||||
name: rc.name(&plan.table_references).map(String::from),
|
||||
ty: Type::Text, // FIXME: infer proper type
|
||||
ty_str: "TEXT".to_string(),
|
||||
is_rowid_alias: false,
|
||||
primary_key: false,
|
||||
notnull: false,
|
||||
default: None,
|
||||
unique: false,
|
||||
collation: None,
|
||||
})
|
||||
.collect(),
|
||||
)));
|
||||
let columns = plan
|
||||
.result_columns
|
||||
.iter()
|
||||
.map(|rc| Column {
|
||||
name: rc.name(&plan.table_references).map(String::from),
|
||||
ty: Type::Blob, // FIXME: infer proper type
|
||||
ty_str: "BLOB".to_string(),
|
||||
is_rowid_alias: false,
|
||||
primary_key: false,
|
||||
notnull: false,
|
||||
default: None,
|
||||
unique: false,
|
||||
collation: None, // FIXME: infer collation from subquery
|
||||
})
|
||||
.collect();
|
||||
|
||||
let table = Table::FromClauseSubquery(FromClauseSubquery {
|
||||
name: identifier.clone(),
|
||||
plan: Box::new(plan),
|
||||
columns,
|
||||
result_columns_start_reg: None,
|
||||
});
|
||||
Self {
|
||||
op: Operation::Subquery {
|
||||
plan: Box::new(plan),
|
||||
result_columns_start_reg: 0, // Will be set in the bytecode emission phase
|
||||
op: Operation::Scan {
|
||||
iter_dir: IterationDirection::Forwards,
|
||||
index: None,
|
||||
},
|
||||
table,
|
||||
identifier: identifier.clone(),
|
||||
identifier,
|
||||
join_info,
|
||||
col_used_mask: ColumnUsedMask::new(),
|
||||
}
|
||||
|
@ -678,6 +672,7 @@ impl TableReference {
|
|||
Ok((table_cursor_id, index_cursor_id))
|
||||
}
|
||||
Table::Pseudo(_) => Ok((None, None)),
|
||||
Table::FromClauseSubquery(..) => Ok((None, None)),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -921,13 +916,6 @@ impl Display for SelectPlan {
|
|||
)?;
|
||||
}
|
||||
},
|
||||
Operation::Subquery { plan, .. } => {
|
||||
writeln!(f, "{}SUBQUERY {}", indent, reference.identifier)?;
|
||||
// Indent and format the subquery plan
|
||||
for line in format!("{}", plan).lines() {
|
||||
writeln!(f, "{} {}", indent, line)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
@ -955,9 +943,6 @@ impl Display for DeletePlan {
|
|||
Operation::Search { .. } => {
|
||||
panic!("DELETE plans should not contain search operations");
|
||||
}
|
||||
Operation::Subquery { .. } => {
|
||||
panic!("DELETE plans should not contain subqueries");
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
@ -1012,12 +997,6 @@ impl fmt::Display for UpdatePlan {
|
|||
)?;
|
||||
}
|
||||
},
|
||||
Operation::Subquery { plan, .. } => {
|
||||
writeln!(f, "{}SUBQUERY {}", indent, reference.identifier)?;
|
||||
for line in format!("{}", plan).lines() {
|
||||
writeln!(f, "{} {}", indent, line)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(order_by) = &self.order_by {
|
||||
|
|
|
@ -793,6 +793,7 @@ pub fn translate_drop_table(
|
|||
});
|
||||
}
|
||||
Table::Pseudo(..) => unimplemented!(),
|
||||
Table::FromClauseSubquery(..) => panic!("FromClauseSubquery can't be dropped"),
|
||||
};
|
||||
|
||||
let r6 = program.alloc_register();
|
||||
|
|
|
@ -4,6 +4,7 @@ use super::plan::{
|
|||
};
|
||||
use super::planner::Scope;
|
||||
use crate::function::{AggFunc, ExtFunc, Func};
|
||||
use crate::schema::Table;
|
||||
use crate::translate::optimizer::optimize_plan;
|
||||
use crate::translate::plan::{Aggregate, GroupBy, Plan, ResultSetColumn, SelectPlan};
|
||||
use crate::translate::planner::{
|
||||
|
@ -437,8 +438,11 @@ fn count_plan_required_cursors(plan: &SelectPlan) -> usize {
|
|||
Operation::Search(search) => match search {
|
||||
Search::RowidEq { .. } => 1,
|
||||
Search::Seek { index, .. } => 1 + index.is_some() as usize,
|
||||
},
|
||||
Operation::Subquery { plan, .. } => count_plan_required_cursors(plan),
|
||||
}
|
||||
} + if let Table::FromClauseSubquery(from_clause_subquery) = &t.table {
|
||||
count_plan_required_cursors(&from_clause_subquery.plan)
|
||||
} else {
|
||||
0
|
||||
})
|
||||
.sum();
|
||||
let num_sorter_cursors = plan.group_by.is_some() as usize + plan.order_by.is_some() as usize;
|
||||
|
@ -454,7 +458,10 @@ fn estimate_num_instructions(select: &SelectPlan) -> usize {
|
|||
.map(|t| match &t.op {
|
||||
Operation::Scan { .. } => 10,
|
||||
Operation::Search(_) => 15,
|
||||
Operation::Subquery { plan, .. } => 10 + estimate_num_instructions(plan),
|
||||
} + if let Table::FromClauseSubquery(from_clause_subquery) = &t.table {
|
||||
10 + estimate_num_instructions(&from_clause_subquery.plan)
|
||||
} else {
|
||||
0
|
||||
})
|
||||
.sum();
|
||||
|
||||
|
@ -480,7 +487,10 @@ fn estimate_num_labels(select: &SelectPlan) -> usize {
|
|||
.map(|t| match &t.op {
|
||||
Operation::Scan { .. } => 3,
|
||||
Operation::Search(_) => 3,
|
||||
Operation::Subquery { plan, .. } => 3 + estimate_num_labels(plan),
|
||||
} + if let Table::FromClauseSubquery(from_clause_subquery) = &t.table {
|
||||
3 + estimate_num_labels(&from_clause_subquery.plan)
|
||||
} else {
|
||||
0
|
||||
})
|
||||
.sum::<usize>()
|
||||
+ 1;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use crate::{
|
||||
schema::Table,
|
||||
vdbe::{builder::ProgramBuilder, insn::Insn},
|
||||
Result,
|
||||
};
|
||||
|
@ -6,7 +7,7 @@ use crate::{
|
|||
use super::{
|
||||
emitter::{emit_query, Resolver, TranslateCtx},
|
||||
main_loop::LoopLabels,
|
||||
plan::{Operation, SelectPlan, SelectQueryType, TableReference},
|
||||
plan::{SelectPlan, SelectQueryType, TableReference},
|
||||
};
|
||||
|
||||
/// Emit the subqueries contained in the FROM clause.
|
||||
|
@ -16,18 +17,15 @@ pub fn emit_subqueries(
|
|||
t_ctx: &mut TranslateCtx,
|
||||
tables: &mut [TableReference],
|
||||
) -> Result<()> {
|
||||
for table in tables.iter_mut() {
|
||||
if let Operation::Subquery {
|
||||
plan,
|
||||
result_columns_start_reg,
|
||||
} = &mut table.op
|
||||
{
|
||||
for table_reference in tables.iter_mut() {
|
||||
if let Table::FromClauseSubquery(from_clause_subquery) = &mut table_reference.table {
|
||||
// Emit the subquery and get the start register of the result columns.
|
||||
let result_columns_start = emit_subquery(program, plan, t_ctx)?;
|
||||
let result_columns_start =
|
||||
emit_subquery(program, &mut from_clause_subquery.plan, t_ctx)?;
|
||||
// Set the start register of the subquery's result columns.
|
||||
// This is done so that translate_expr() can read the result columns of the subquery,
|
||||
// as if it were reading from a regular table.
|
||||
*result_columns_start_reg = result_columns_start;
|
||||
from_clause_subquery.result_columns_start_reg = Some(result_columns_start);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue