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:
Jussi Saurio 2025-05-20 14:31:42 +03:00
commit 14058357ad
9 changed files with 326 additions and 324 deletions

View file

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

View file

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

View file

@ -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 } => {

View file

@ -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)
{
// Virtualtable (nonTVF) 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) {
// Virtualtable (nonTVF) 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")

View file

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

View file

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

View file

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

View file

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

View file

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