mirror of
https://github.com/tursodatabase/limbo.git
synced 2025-08-04 18:18:03 +00:00
feat: subqueries in FROM clause
This commit is contained in:
parent
b1ca2b050b
commit
2066475e03
12 changed files with 1092 additions and 310 deletions
|
@ -298,8 +298,8 @@ impl Connection {
|
|||
Cmd::ExplainQueryPlan(stmt) => {
|
||||
match stmt {
|
||||
ast::Stmt::Select(select) => {
|
||||
let plan = prepare_select_plan(&self.schema.borrow(), select)?;
|
||||
let plan = optimize_plan(plan)?;
|
||||
let mut plan = prepare_select_plan(&self.schema.borrow(), select)?;
|
||||
optimize_plan(&mut plan)?;
|
||||
println!("{}", plan);
|
||||
}
|
||||
_ => todo!(),
|
||||
|
|
|
@ -71,6 +71,14 @@ impl Table {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn get_root_page(&self) -> usize {
|
||||
match self {
|
||||
Table::BTree(table) => table.root_page,
|
||||
Table::Index(_) => unimplemented!(),
|
||||
Table::Pseudo(_) => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_name(&self) -> &str {
|
||||
match self {
|
||||
Self::BTree(table) => &table.name,
|
||||
|
@ -211,6 +219,10 @@ impl PseudoTable {
|
|||
Self { columns: vec![] }
|
||||
}
|
||||
|
||||
pub fn new_with_columns(columns: Vec<Column>) -> Self {
|
||||
Self { columns }
|
||||
}
|
||||
|
||||
pub fn add_column(&mut self, name: &str, ty: Type, primary_key: bool) {
|
||||
self.columns.push(Column {
|
||||
name: normalize_ident(name),
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use crate::schema::Table;
|
||||
use crate::translate::emitter::emit_program;
|
||||
use crate::translate::optimizer::optimize_plan;
|
||||
use crate::translate::plan::{BTreeTableReference, DeletePlan, Plan, SourceOperator};
|
||||
use crate::translate::plan::{DeletePlan, Plan, SourceOperator};
|
||||
use crate::translate::planner::{parse_limit, parse_where};
|
||||
use crate::{schema::Schema, storage::sqlite3_ondisk::DatabaseHeader, vdbe::Program};
|
||||
use crate::{Connection, Result, SymbolTable};
|
||||
|
@ -8,6 +9,8 @@ use sqlite3_parser::ast::{Expr, Limit, QualifiedName};
|
|||
use std::rc::Weak;
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use super::plan::{TableReference, TableReferenceType};
|
||||
|
||||
pub fn translate_delete(
|
||||
schema: &Schema,
|
||||
tbl_name: &QualifiedName,
|
||||
|
@ -17,9 +20,9 @@ pub fn translate_delete(
|
|||
connection: Weak<Connection>,
|
||||
syms: &SymbolTable,
|
||||
) -> Result<Program> {
|
||||
let delete_plan = prepare_delete_plan(schema, tbl_name, where_clause, limit)?;
|
||||
let optimized_plan = optimize_plan(delete_plan)?;
|
||||
emit_program(database_header, optimized_plan, connection, syms)
|
||||
let mut delete_plan = prepare_delete_plan(schema, tbl_name, where_clause, limit)?;
|
||||
optimize_plan(&mut delete_plan)?;
|
||||
emit_program(database_header, delete_plan, connection, syms)
|
||||
}
|
||||
|
||||
pub fn prepare_delete_plan(
|
||||
|
@ -33,15 +36,16 @@ pub fn prepare_delete_plan(
|
|||
None => crate::bail_corrupt_error!("Parse error: no such table: {}", tbl_name),
|
||||
};
|
||||
|
||||
let table_ref = BTreeTableReference {
|
||||
table: table.clone(),
|
||||
let btree_table_ref = TableReference {
|
||||
table: Table::BTree(table.clone()),
|
||||
table_identifier: table.name.clone(),
|
||||
table_index: 0,
|
||||
reference_type: TableReferenceType::BTreeTable,
|
||||
};
|
||||
let referenced_tables = vec![table_ref.clone()];
|
||||
let referenced_tables = vec![btree_table_ref.clone()];
|
||||
|
||||
// Parse the WHERE clause
|
||||
let resolved_where_clauses = parse_where(where_clause, &[table_ref.clone()])?;
|
||||
let resolved_where_clauses = parse_where(where_clause, &referenced_tables)?;
|
||||
|
||||
// Parse the LIMIT clause
|
||||
let resolved_limit = limit.and_then(parse_limit);
|
||||
|
@ -49,7 +53,7 @@ pub fn prepare_delete_plan(
|
|||
let plan = DeletePlan {
|
||||
source: SourceOperator::Scan {
|
||||
id: 0,
|
||||
table_reference: table_ref.clone(),
|
||||
table_reference: btree_table_ref,
|
||||
predicates: resolved_where_clauses.clone(),
|
||||
iter_dir: None,
|
||||
},
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -10,7 +10,7 @@ use crate::util::{exprs_are_equivalent, normalize_ident};
|
|||
use crate::vdbe::{builder::ProgramBuilder, insn::Insn, BranchOffset};
|
||||
use crate::{Result, SymbolTable};
|
||||
|
||||
use super::plan::{Aggregate, BTreeTableReference};
|
||||
use super::plan::{Aggregate, TableReference, TableReferenceType};
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy)]
|
||||
pub struct ConditionMetadata {
|
||||
|
@ -21,7 +21,7 @@ pub struct ConditionMetadata {
|
|||
|
||||
pub fn translate_condition_expr(
|
||||
program: &mut ProgramBuilder,
|
||||
referenced_tables: &[BTreeTableReference],
|
||||
referenced_tables: &[TableReference],
|
||||
expr: &ast::Expr,
|
||||
condition_metadata: ConditionMetadata,
|
||||
precomputed_exprs_to_registers: Option<&Vec<(&ast::Expr, usize)>>,
|
||||
|
@ -562,7 +562,7 @@ pub fn translate_condition_expr(
|
|||
|
||||
pub fn translate_expr(
|
||||
program: &mut ProgramBuilder,
|
||||
referenced_tables: Option<&[BTreeTableReference]>,
|
||||
referenced_tables: Option<&[TableReference]>,
|
||||
expr: &ast::Expr,
|
||||
target_register: usize,
|
||||
precomputed_exprs_to_registers: Option<&Vec<(&ast::Expr, usize)>>,
|
||||
|
@ -1962,22 +1962,41 @@ pub fn translate_expr(
|
|||
is_rowid_alias,
|
||||
} => {
|
||||
let tbl_ref = referenced_tables.as_ref().unwrap().get(*table).unwrap();
|
||||
let cursor_id = program.resolve_cursor_id(&tbl_ref.table_identifier);
|
||||
if *is_rowid_alias {
|
||||
program.emit_insn(Insn::RowId {
|
||||
cursor_id,
|
||||
dest: target_register,
|
||||
});
|
||||
} else {
|
||||
program.emit_insn(Insn::Column {
|
||||
cursor_id,
|
||||
column: *column,
|
||||
dest: target_register,
|
||||
});
|
||||
match tbl_ref.reference_type {
|
||||
// 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.
|
||||
TableReferenceType::BTreeTable => {
|
||||
let cursor_id = program.resolve_cursor_id(&tbl_ref.table_identifier);
|
||||
if *is_rowid_alias {
|
||||
program.emit_insn(Insn::RowId {
|
||||
cursor_id,
|
||||
dest: target_register,
|
||||
});
|
||||
} else {
|
||||
program.emit_insn(Insn::Column {
|
||||
cursor_id,
|
||||
column: *column,
|
||||
dest: target_register,
|
||||
});
|
||||
}
|
||||
let column = tbl_ref.table.get_column_at(*column);
|
||||
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.
|
||||
TableReferenceType::Subquery {
|
||||
result_columns_start_reg,
|
||||
..
|
||||
} => {
|
||||
program.emit_insn(Insn::Copy {
|
||||
src_reg: result_columns_start_reg + *column,
|
||||
dst_reg: target_register,
|
||||
amount: 0,
|
||||
});
|
||||
Ok(target_register)
|
||||
}
|
||||
}
|
||||
let column = &tbl_ref.table.columns[*column];
|
||||
maybe_apply_affinity(column.ty, target_register, program);
|
||||
Ok(target_register)
|
||||
}
|
||||
ast::Expr::InList { .. } => todo!(),
|
||||
ast::Expr::InSelect { .. } => todo!(),
|
||||
|
@ -2170,7 +2189,7 @@ pub fn translate_expr(
|
|||
fn translate_variable_sized_function_parameter_list(
|
||||
program: &mut ProgramBuilder,
|
||||
args: &Option<Vec<ast::Expr>>,
|
||||
referenced_tables: Option<&[BTreeTableReference]>,
|
||||
referenced_tables: Option<&[TableReference]>,
|
||||
precomputed_exprs_to_registers: Option<&Vec<(&ast::Expr, usize)>>,
|
||||
syms: &SymbolTable,
|
||||
) -> Result<usize> {
|
||||
|
@ -2223,7 +2242,7 @@ pub fn maybe_apply_affinity(col_type: Type, target_register: usize, program: &mu
|
|||
|
||||
pub fn translate_aggregation(
|
||||
program: &mut ProgramBuilder,
|
||||
referenced_tables: &[BTreeTableReference],
|
||||
referenced_tables: &[TableReference],
|
||||
agg: &Aggregate,
|
||||
target_register: usize,
|
||||
syms: &SymbolTable,
|
||||
|
@ -2408,7 +2427,7 @@ pub fn translate_aggregation(
|
|||
|
||||
pub fn translate_aggregation_groupby(
|
||||
program: &mut ProgramBuilder,
|
||||
referenced_tables: &[BTreeTableReference],
|
||||
referenced_tables: &[TableReference],
|
||||
group_by_sorter_cursor_id: usize,
|
||||
cursor_index: usize,
|
||||
agg: &Aggregate,
|
||||
|
@ -2585,3 +2604,29 @@ pub fn translate_aggregation_groupby(
|
|||
};
|
||||
Ok(dest)
|
||||
}
|
||||
|
||||
/// Get an appropriate name for an expression.
|
||||
/// If the query provides an alias (e.g. `SELECT a AS b FROM t`), use that (e.g. `b`).
|
||||
/// If the expression is a column from a table, use the column name (e.g. `a`).
|
||||
/// Otherwise we just use a generic fallback name (e.g. `expr_<index>`).
|
||||
pub fn get_name(
|
||||
maybe_alias: Option<&ast::As>,
|
||||
expr: &ast::Expr,
|
||||
referenced_tables: &[TableReference],
|
||||
fallback: impl Fn() -> String,
|
||||
) -> String {
|
||||
let alias = maybe_alias.map(|a| match a {
|
||||
ast::As::As(id) => id.0.clone(),
|
||||
ast::As::Elided(id) => id.0.clone(),
|
||||
});
|
||||
if let Some(alias) = alias {
|
||||
return alias;
|
||||
}
|
||||
match expr {
|
||||
ast::Expr::Column { table, column, .. } => {
|
||||
let table_ref = referenced_tables.get(*table).unwrap();
|
||||
table_ref.table.get_column_at(*column).name.clone()
|
||||
}
|
||||
_ => fallback(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,14 +5,15 @@ use sqlite3_parser::ast;
|
|||
use crate::{schema::Index, Result};
|
||||
|
||||
use super::plan::{
|
||||
get_table_ref_bitmask_for_ast_expr, get_table_ref_bitmask_for_operator, BTreeTableReference,
|
||||
DeletePlan, Direction, IterationDirection, Plan, Search, SelectPlan, SourceOperator,
|
||||
get_table_ref_bitmask_for_ast_expr, get_table_ref_bitmask_for_operator, DeletePlan, Direction,
|
||||
IterationDirection, Plan, Search, SelectPlan, SourceOperator, TableReference,
|
||||
TableReferenceType,
|
||||
};
|
||||
|
||||
pub fn optimize_plan(plan: Plan) -> Result<Plan> {
|
||||
pub fn optimize_plan(plan: &mut Plan) -> Result<()> {
|
||||
match plan {
|
||||
Plan::Select(plan) => optimize_select_plan(plan).map(Plan::Select),
|
||||
Plan::Delete(plan) => optimize_delete_plan(plan).map(Plan::Delete),
|
||||
Plan::Select(plan) => optimize_select_plan(plan),
|
||||
Plan::Delete(plan) => optimize_delete_plan(plan),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -21,13 +22,14 @@ pub fn optimize_plan(plan: Plan) -> Result<Plan> {
|
|||
* TODO: these could probably be done in less passes,
|
||||
* but having them separate makes them easier to understand
|
||||
*/
|
||||
fn optimize_select_plan(mut plan: SelectPlan) -> Result<SelectPlan> {
|
||||
fn optimize_select_plan(plan: &mut SelectPlan) -> Result<()> {
|
||||
optimize_subqueries(&mut plan.source)?;
|
||||
eliminate_between(&mut plan.source, &mut plan.where_clause)?;
|
||||
if let ConstantConditionEliminationResult::ImpossibleCondition =
|
||||
eliminate_constants(&mut plan.source, &mut plan.where_clause)?
|
||||
{
|
||||
plan.contains_constant_false_condition = true;
|
||||
return Ok(plan);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
push_predicates(
|
||||
|
@ -49,16 +51,16 @@ fn optimize_select_plan(mut plan: SelectPlan) -> Result<SelectPlan> {
|
|||
&plan.available_indexes,
|
||||
)?;
|
||||
|
||||
Ok(plan)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn optimize_delete_plan(mut plan: DeletePlan) -> Result<DeletePlan> {
|
||||
fn optimize_delete_plan(plan: &mut DeletePlan) -> Result<()> {
|
||||
eliminate_between(&mut plan.source, &mut plan.where_clause)?;
|
||||
if let ConstantConditionEliminationResult::ImpossibleCondition =
|
||||
eliminate_constants(&mut plan.source, &mut plan.where_clause)?
|
||||
{
|
||||
plan.contains_constant_false_condition = true;
|
||||
return Ok(plan);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
use_indexes(
|
||||
|
@ -67,13 +69,28 @@ fn optimize_delete_plan(mut plan: DeletePlan) -> Result<DeletePlan> {
|
|||
&plan.available_indexes,
|
||||
)?;
|
||||
|
||||
Ok(plan)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn optimize_subqueries(operator: &mut SourceOperator) -> Result<()> {
|
||||
match operator {
|
||||
SourceOperator::Subquery { plan, .. } => {
|
||||
optimize_select_plan(&mut *plan)?;
|
||||
Ok(())
|
||||
}
|
||||
SourceOperator::Join { left, right, .. } => {
|
||||
optimize_subqueries(left)?;
|
||||
optimize_subqueries(right)?;
|
||||
Ok(())
|
||||
}
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
fn _operator_is_already_ordered_by(
|
||||
operator: &mut SourceOperator,
|
||||
key: &mut ast::Expr,
|
||||
referenced_tables: &[BTreeTableReference],
|
||||
referenced_tables: &[TableReference],
|
||||
available_indexes: &Vec<Rc<Index>>,
|
||||
) -> Result<bool> {
|
||||
match operator {
|
||||
|
@ -109,7 +126,7 @@ fn _operator_is_already_ordered_by(
|
|||
fn eliminate_unnecessary_orderby(
|
||||
operator: &mut SourceOperator,
|
||||
order_by: &mut Option<Vec<(ast::Expr, Direction)>>,
|
||||
referenced_tables: &[BTreeTableReference],
|
||||
referenced_tables: &[TableReference],
|
||||
available_indexes: &Vec<Rc<Index>>,
|
||||
) -> Result<()> {
|
||||
if order_by.is_none() {
|
||||
|
@ -141,10 +158,11 @@ fn eliminate_unnecessary_orderby(
|
|||
*/
|
||||
fn use_indexes(
|
||||
operator: &mut SourceOperator,
|
||||
referenced_tables: &[BTreeTableReference],
|
||||
referenced_tables: &[TableReference],
|
||||
available_indexes: &[Rc<Index>],
|
||||
) -> Result<()> {
|
||||
match operator {
|
||||
SourceOperator::Subquery { .. } => Ok(()),
|
||||
SourceOperator::Search { .. } => Ok(()),
|
||||
SourceOperator::Scan {
|
||||
table_reference,
|
||||
|
@ -161,10 +179,7 @@ fn use_indexes(
|
|||
let f = fs[i].take_ownership();
|
||||
let table_index = referenced_tables
|
||||
.iter()
|
||||
.position(|t| {
|
||||
Rc::ptr_eq(&t.table, &table_reference.table)
|
||||
&& t.table_identifier == table_reference.table_identifier
|
||||
})
|
||||
.position(|t| t.table_identifier == table_reference.table_identifier)
|
||||
.unwrap();
|
||||
match try_extract_index_search_expression(
|
||||
f,
|
||||
|
@ -229,6 +244,7 @@ fn eliminate_constants(
|
|||
}
|
||||
}
|
||||
match operator {
|
||||
SourceOperator::Subquery { .. } => Ok(ConstantConditionEliminationResult::Continue),
|
||||
SourceOperator::Join {
|
||||
left,
|
||||
right,
|
||||
|
@ -334,7 +350,7 @@ fn eliminate_constants(
|
|||
fn push_predicates(
|
||||
operator: &mut SourceOperator,
|
||||
where_clause: &mut Option<Vec<ast::Expr>>,
|
||||
referenced_tables: &Vec<BTreeTableReference>,
|
||||
referenced_tables: &Vec<TableReference>,
|
||||
) -> Result<()> {
|
||||
// First try to push down any predicates from the WHERE clause
|
||||
if let Some(predicates) = where_clause {
|
||||
|
@ -357,6 +373,7 @@ fn push_predicates(
|
|||
}
|
||||
|
||||
match operator {
|
||||
SourceOperator::Subquery { .. } => Ok(()),
|
||||
SourceOperator::Join {
|
||||
left,
|
||||
right,
|
||||
|
@ -424,9 +441,64 @@ fn push_predicates(
|
|||
fn push_predicate(
|
||||
operator: &mut SourceOperator,
|
||||
predicate: ast::Expr,
|
||||
referenced_tables: &Vec<BTreeTableReference>,
|
||||
referenced_tables: &Vec<TableReference>,
|
||||
) -> Result<Option<ast::Expr>> {
|
||||
match operator {
|
||||
SourceOperator::Subquery {
|
||||
predicates,
|
||||
table_reference,
|
||||
..
|
||||
} => {
|
||||
// **TODO**: we are currently just evaluating the predicate after the subquery yields,
|
||||
// and not trying to do anythign more sophisticated.
|
||||
// E.g. literally: SELECT * FROM (SELECT * FROM t1) sub WHERE sub.col = 'foo'
|
||||
//
|
||||
// It is possible, and not overly difficult, to determine that we can also push the
|
||||
// predicate into the subquery coroutine itself before it yields. The above query would
|
||||
// effectively become: SELECT * FROM (SELECT * FROM t1 WHERE col = 'foo') sub
|
||||
//
|
||||
// This matters more in cases where the subquery builds some kind of sorter/index in memory
|
||||
// (or on disk) and in those cases pushing the predicate down to the coroutine will make the
|
||||
// subquery produce less intermediate data. In cases where no intermediate data structures are
|
||||
// built, it doesn't matter.
|
||||
//
|
||||
// Moreover, in many cases the subquery can even be completely eliminated, e.g. the above original
|
||||
// query would become: SELECT * FROM t1 WHERE col = 'foo' without the subquery.
|
||||
// **END TODO**
|
||||
|
||||
// Find position of this subquery in referenced_tables array
|
||||
let subquery_index = referenced_tables
|
||||
.iter()
|
||||
.position(|t| {
|
||||
t.table_identifier == table_reference.table_identifier
|
||||
&& matches!(t.reference_type, TableReferenceType::Subquery { .. })
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// Get bitmask showing which tables this predicate references
|
||||
let predicate_bitmask =
|
||||
get_table_ref_bitmask_for_ast_expr(referenced_tables, &predicate)?;
|
||||
|
||||
// Each table has a bit position based on join order from left to right
|
||||
// e.g. in SELECT * FROM t1 JOIN t2 JOIN t3
|
||||
// t1 is position 0 (001), t2 is position 1 (010), t3 is position 2 (100)
|
||||
// To push a predicate to a given table, it can only reference that table and tables to its left
|
||||
// Example: For table t2 at position 1 (bit 010):
|
||||
// - Can push: 011 (t2 + t1), 001 (just t1), 010 (just t2)
|
||||
// - Can't push: 110 (t2 + t3)
|
||||
let next_table_on_the_right_in_join_bitmask = 1 << (subquery_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);
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
SourceOperator::Scan {
|
||||
predicates,
|
||||
table_reference,
|
||||
|
@ -435,7 +507,10 @@ fn push_predicate(
|
|||
// Find position of this table in referenced_tables array
|
||||
let table_index = referenced_tables
|
||||
.iter()
|
||||
.position(|t| t.table_identifier == table_reference.table_identifier)
|
||||
.position(|t| {
|
||||
t.table_identifier == table_reference.table_identifier
|
||||
&& t.reference_type == TableReferenceType::BTreeTable
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// Get bitmask showing which tables this predicate references
|
||||
|
@ -595,7 +670,7 @@ pub trait Optimizable {
|
|||
fn check_index_scan(
|
||||
&mut self,
|
||||
table_index: usize,
|
||||
referenced_tables: &[BTreeTableReference],
|
||||
referenced_tables: &[TableReference],
|
||||
available_indexes: &[Rc<Index>],
|
||||
) -> Result<Option<usize>>;
|
||||
}
|
||||
|
@ -614,7 +689,7 @@ impl Optimizable for ast::Expr {
|
|||
fn check_index_scan(
|
||||
&mut self,
|
||||
table_index: usize,
|
||||
referenced_tables: &[BTreeTableReference],
|
||||
referenced_tables: &[TableReference],
|
||||
available_indexes: &[Rc<Index>],
|
||||
) -> Result<Option<usize>> {
|
||||
match self {
|
||||
|
@ -623,12 +698,9 @@ impl Optimizable for ast::Expr {
|
|||
return Ok(None);
|
||||
}
|
||||
for (idx, index) in available_indexes.iter().enumerate() {
|
||||
if index.table_name == referenced_tables[*table].table.name {
|
||||
let column = referenced_tables[*table]
|
||||
.table
|
||||
.columns
|
||||
.get(*column)
|
||||
.unwrap();
|
||||
let table_ref = &referenced_tables[*table];
|
||||
if index.table_name == table_ref.table.get_name() {
|
||||
let column = table_ref.table.get_column_at(*column);
|
||||
if index.columns.first().unwrap().name == column.name {
|
||||
return Ok(Some(idx));
|
||||
}
|
||||
|
@ -793,7 +865,7 @@ pub enum Either<T, U> {
|
|||
pub fn try_extract_index_search_expression(
|
||||
expr: ast::Expr,
|
||||
table_index: usize,
|
||||
referenced_tables: &[BTreeTableReference],
|
||||
referenced_tables: &[TableReference],
|
||||
available_indexes: &[Rc<Index>],
|
||||
) -> Result<Either<ast::Expr, Search>> {
|
||||
match expr {
|
||||
|
|
|
@ -5,34 +5,51 @@ use std::{
|
|||
rc::Rc,
|
||||
};
|
||||
|
||||
use crate::translate::plan::Plan::{Delete, Select};
|
||||
use crate::{
|
||||
function::AggFunc,
|
||||
schema::{BTreeTable, Column, Index},
|
||||
schema::{Column, Index, Table},
|
||||
vdbe::BranchOffset,
|
||||
Result,
|
||||
};
|
||||
use crate::{
|
||||
schema::{PseudoTable, Type},
|
||||
translate::plan::Plan::{Delete, Select},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ResultSetColumn {
|
||||
pub expr: ast::Expr,
|
||||
pub name: String,
|
||||
// TODO: encode which aggregates (e.g. index bitmask of plan.aggregates) are present in this column
|
||||
pub contains_aggregates: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GroupBy {
|
||||
pub exprs: Vec<ast::Expr>,
|
||||
/// having clause split into a vec at 'AND' boundaries.
|
||||
pub having: Option<Vec<ast::Expr>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Plan {
|
||||
Select(SelectPlan),
|
||||
Delete(DeletePlan),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// The type of the query, either top level or subquery
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum SelectQueryType {
|
||||
TopLevel,
|
||||
Subquery {
|
||||
/// The register that holds the program offset that handles jumping to/from the subquery.
|
||||
yield_reg: usize,
|
||||
/// The index of the first instruction in the bytecode that implements the subquery.
|
||||
coroutine_implementation_start: BranchOffset,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SelectPlan {
|
||||
/// A tree of sources (tables).
|
||||
pub source: SourceOperator,
|
||||
|
@ -49,15 +66,17 @@ pub struct SelectPlan {
|
|||
/// limit clause
|
||||
pub limit: Option<usize>,
|
||||
/// all the tables referenced in the query
|
||||
pub referenced_tables: Vec<BTreeTableReference>,
|
||||
pub referenced_tables: Vec<TableReference>,
|
||||
/// all the indexes available
|
||||
pub available_indexes: Vec<Rc<Index>>,
|
||||
/// query contains a constant condition that is always false
|
||||
pub contains_constant_false_condition: bool,
|
||||
/// query type (top level or subquery)
|
||||
pub query_type: SelectQueryType,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DeletePlan {
|
||||
/// A tree of sources (tables).
|
||||
pub source: SourceOperator,
|
||||
|
@ -70,7 +89,7 @@ pub struct DeletePlan {
|
|||
/// limit clause
|
||||
pub limit: Option<usize>,
|
||||
/// all the tables referenced in the query
|
||||
pub referenced_tables: Vec<BTreeTableReference>,
|
||||
pub referenced_tables: Vec<TableReference>,
|
||||
/// all the indexes available
|
||||
pub available_indexes: Vec<Rc<Index>>,
|
||||
/// query contains a constant condition that is always false
|
||||
|
@ -94,11 +113,12 @@ pub enum IterationDirection {
|
|||
|
||||
impl SourceOperator {
|
||||
pub fn select_star(&self, out_columns: &mut Vec<ResultSetColumn>) {
|
||||
for (table_ref, col, idx) in self.select_star_helper() {
|
||||
for (table_index, col, idx) in self.select_star_helper() {
|
||||
out_columns.push(ResultSetColumn {
|
||||
name: col.name.clone(),
|
||||
expr: ast::Expr::Column {
|
||||
database: None,
|
||||
table: table_ref.table_index,
|
||||
table: table_index,
|
||||
column: idx,
|
||||
is_rowid_alias: col.is_rowid_alias,
|
||||
},
|
||||
|
@ -108,7 +128,7 @@ impl SourceOperator {
|
|||
}
|
||||
|
||||
/// All this ceremony is required to deduplicate columns when joining with USING
|
||||
fn select_star_helper(&self) -> Vec<(&BTreeTableReference, &Column, usize)> {
|
||||
fn select_star_helper(&self) -> Vec<(usize, &Column, usize)> {
|
||||
match self {
|
||||
SourceOperator::Join {
|
||||
left, right, using, ..
|
||||
|
@ -120,12 +140,12 @@ impl SourceOperator {
|
|||
if let Some(using_cols) = using {
|
||||
let right_columns = right.select_star_helper();
|
||||
|
||||
for (table_ref, col, idx) in right_columns {
|
||||
for (table_index, col, idx) in right_columns {
|
||||
if !using_cols
|
||||
.iter()
|
||||
.any(|using_col| col.name.eq_ignore_ascii_case(&using_col.0))
|
||||
{
|
||||
columns.push((table_ref, col, idx));
|
||||
columns.push((table_index, col, idx));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -138,12 +158,14 @@ impl SourceOperator {
|
|||
}
|
||||
| SourceOperator::Search {
|
||||
table_reference, ..
|
||||
}
|
||||
| SourceOperator::Subquery {
|
||||
table_reference, ..
|
||||
} => table_reference
|
||||
.table
|
||||
.columns
|
||||
.columns()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, col)| (table_reference, col, i))
|
||||
.map(|(i, col)| (table_reference.table_index, col, i))
|
||||
.collect(),
|
||||
SourceOperator::Nothing => Vec::new(),
|
||||
}
|
||||
|
@ -178,7 +200,7 @@ pub enum SourceOperator {
|
|||
// assignments. for more detailed discussions, please refer to https://github.com/penberg/limbo/pull/376
|
||||
Scan {
|
||||
id: usize,
|
||||
table_reference: BTreeTableReference,
|
||||
table_reference: TableReference,
|
||||
predicates: Option<Vec<ast::Expr>>,
|
||||
iter_dir: Option<IterationDirection>,
|
||||
},
|
||||
|
@ -187,21 +209,74 @@ pub enum SourceOperator {
|
|||
// (i.e. a primary key or a secondary index)
|
||||
Search {
|
||||
id: usize,
|
||||
table_reference: BTreeTableReference,
|
||||
table_reference: TableReference,
|
||||
search: Search,
|
||||
predicates: Option<Vec<ast::Expr>>,
|
||||
},
|
||||
Subquery {
|
||||
id: usize,
|
||||
table_reference: TableReference,
|
||||
plan: Box<SelectPlan>,
|
||||
predicates: Option<Vec<ast::Expr>>,
|
||||
},
|
||||
// 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,
|
||||
}
|
||||
|
||||
/// The type of the table reference, either BTreeTable or Subquery
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum TableReferenceType {
|
||||
/// A BTreeTable is a table that is stored on disk in a B-tree index.
|
||||
BTreeTable,
|
||||
/// A subquery.
|
||||
Subquery {
|
||||
/// The index of the first register in the query plan that contains the result columns of the subquery.
|
||||
result_columns_start_reg: usize,
|
||||
},
|
||||
}
|
||||
|
||||
/// A query plan has a list of TableReference objects, each of which represents a table or subquery.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct BTreeTableReference {
|
||||
pub table: Rc<BTreeTable>,
|
||||
pub struct TableReference {
|
||||
/// Table object, which contains metadata about the table, e.g. columns.
|
||||
pub table: Table,
|
||||
/// The name of the table as referred to in the query, either the literal name or an alias e.g. "users" or "u"
|
||||
pub table_identifier: String,
|
||||
/// The index of this reference in the list of TableReference objects in the query plan
|
||||
/// The reference at index 0 is the first table in the FROM clause, the reference at index 1 is the second table in the FROM clause, etc.
|
||||
/// So, the index is relevant for determining when predicates (WHERE, ON filters etc.) should be evaluated.
|
||||
pub table_index: usize,
|
||||
/// The type of the table reference, either BTreeTable or Subquery
|
||||
pub reference_type: TableReferenceType,
|
||||
}
|
||||
|
||||
impl TableReference {
|
||||
pub fn new_subquery(identifier: String, table_index: usize, plan: &SelectPlan) -> Self {
|
||||
Self {
|
||||
table: Table::Pseudo(Rc::new(PseudoTable::new_with_columns(
|
||||
plan.result_columns
|
||||
.iter()
|
||||
.map(|rc| Column {
|
||||
name: rc.name.clone(),
|
||||
ty: Type::Text, // FIXME: infer proper type
|
||||
is_rowid_alias: false,
|
||||
primary_key: false,
|
||||
})
|
||||
.collect(),
|
||||
))),
|
||||
table_identifier: identifier.clone(),
|
||||
table_index,
|
||||
reference_type: TableReferenceType::Subquery {
|
||||
result_columns_start_reg: 0, // Will be set in the bytecode emission phase
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn columns(&self) -> &[Column] {
|
||||
self.table.columns()
|
||||
}
|
||||
}
|
||||
|
||||
/// An enum that represents a search operation that can be used to search for a row in a table using an index
|
||||
|
@ -230,6 +305,7 @@ impl SourceOperator {
|
|||
SourceOperator::Join { id, .. } => *id,
|
||||
SourceOperator::Scan { id, .. } => *id,
|
||||
SourceOperator::Search { id, .. } => *id,
|
||||
SourceOperator::Subquery { id, .. } => *id,
|
||||
SourceOperator::Nothing => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
@ -320,12 +396,13 @@ impl Display for SourceOperator {
|
|||
..
|
||||
} => {
|
||||
let table_name =
|
||||
if table_reference.table.name == table_reference.table_identifier {
|
||||
if table_reference.table.get_name() == table_reference.table_identifier {
|
||||
table_reference.table_identifier.clone()
|
||||
} else {
|
||||
format!(
|
||||
"{} AS {}",
|
||||
&table_reference.table.name, &table_reference.table_identifier
|
||||
&table_reference.table.get_name(),
|
||||
&table_reference.table_identifier
|
||||
)
|
||||
};
|
||||
let filter_string = filter.as_ref().map(|f| {
|
||||
|
@ -365,6 +442,9 @@ impl Display for SourceOperator {
|
|||
}
|
||||
Ok(())
|
||||
}
|
||||
SourceOperator::Subquery { plan, .. } => {
|
||||
fmt_operator(&plan.source, f, level + 1, last)
|
||||
}
|
||||
SourceOperator::Nothing => Ok(()),
|
||||
}
|
||||
}
|
||||
|
@ -382,7 +462,7 @@ impl Display for SourceOperator {
|
|||
then the return value will be (in bits): 110
|
||||
*/
|
||||
pub fn get_table_ref_bitmask_for_operator<'a>(
|
||||
tables: &'a Vec<BTreeTableReference>,
|
||||
tables: &'a Vec<TableReference>,
|
||||
operator: &'a SourceOperator,
|
||||
) -> Result<usize> {
|
||||
let mut table_refs_mask = 0;
|
||||
|
@ -409,6 +489,7 @@ pub fn get_table_ref_bitmask_for_operator<'a>(
|
|||
.position(|t| t.table_identifier == table_reference.table_identifier)
|
||||
.unwrap();
|
||||
}
|
||||
SourceOperator::Subquery { .. } => {}
|
||||
SourceOperator::Nothing => {}
|
||||
}
|
||||
Ok(table_refs_mask)
|
||||
|
@ -424,7 +505,7 @@ pub fn get_table_ref_bitmask_for_operator<'a>(
|
|||
*/
|
||||
#[allow(clippy::only_used_in_recursion)]
|
||||
pub fn get_table_ref_bitmask_for_ast_expr<'a>(
|
||||
tables: &'a Vec<BTreeTableReference>,
|
||||
tables: &'a Vec<TableReference>,
|
||||
predicate: &'a ast::Expr,
|
||||
) -> Result<usize> {
|
||||
let mut table_refs_mask = 0;
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
use super::plan::{Aggregate, BTreeTableReference, SourceOperator};
|
||||
use super::{
|
||||
plan::{Aggregate, Plan, SelectQueryType, SourceOperator, TableReference, TableReferenceType},
|
||||
select::prepare_select_plan,
|
||||
};
|
||||
use crate::{
|
||||
function::Func,
|
||||
schema::Schema,
|
||||
schema::{Schema, Table},
|
||||
util::{exprs_are_equivalent, normalize_ident},
|
||||
vdbe::BranchOffset,
|
||||
Result,
|
||||
};
|
||||
use sqlite3_parser::ast::{self, Expr, FromClause, JoinType, Limit};
|
||||
|
@ -88,7 +92,7 @@ pub fn resolve_aggregates(expr: &ast::Expr, aggs: &mut Vec<Aggregate>) -> bool {
|
|||
|
||||
pub fn bind_column_references(
|
||||
expr: &mut ast::Expr,
|
||||
referenced_tables: &[BTreeTableReference],
|
||||
referenced_tables: &[TableReference],
|
||||
) -> Result<()> {
|
||||
match expr {
|
||||
ast::Expr::Id(id) => {
|
||||
|
@ -101,15 +105,14 @@ pub fn bind_column_references(
|
|||
let normalized_id = normalize_ident(id.0.as_str());
|
||||
for (tbl_idx, table) in referenced_tables.iter().enumerate() {
|
||||
let col_idx = table
|
||||
.table
|
||||
.columns
|
||||
.columns()
|
||||
.iter()
|
||||
.position(|c| c.name.eq_ignore_ascii_case(&normalized_id));
|
||||
if col_idx.is_some() {
|
||||
if match_result.is_some() {
|
||||
crate::bail_parse_error!("Column {} is ambiguous", id.0);
|
||||
}
|
||||
let col = table.table.columns.get(col_idx.unwrap()).unwrap();
|
||||
let col = table.columns().get(col_idx.unwrap()).unwrap();
|
||||
match_result = Some((tbl_idx, col_idx.unwrap(), col.primary_key));
|
||||
}
|
||||
}
|
||||
|
@ -137,16 +140,14 @@ pub fn bind_column_references(
|
|||
let tbl_idx = matching_tbl_idx.unwrap();
|
||||
let normalized_id = normalize_ident(id.0.as_str());
|
||||
let col_idx = referenced_tables[tbl_idx]
|
||||
.table
|
||||
.columns
|
||||
.columns()
|
||||
.iter()
|
||||
.position(|c| c.name.eq_ignore_ascii_case(&normalized_id));
|
||||
if col_idx.is_none() {
|
||||
crate::bail_parse_error!("Column {} not found", normalized_id);
|
||||
}
|
||||
let col = referenced_tables[tbl_idx]
|
||||
.table
|
||||
.columns
|
||||
.columns()
|
||||
.get(col_idx.unwrap())
|
||||
.unwrap();
|
||||
*expr = ast::Expr::Column {
|
||||
|
@ -206,8 +207,8 @@ pub fn bind_column_references(
|
|||
}
|
||||
Ok(())
|
||||
}
|
||||
// Column references cannot exist before binding
|
||||
ast::Expr::Column { .. } => unreachable!(),
|
||||
// Already bound earlier
|
||||
ast::Expr::Column { .. } => Ok(()),
|
||||
ast::Expr::DoublyQualified(_, _, _) => todo!(),
|
||||
ast::Expr::Exists(_) => todo!(),
|
||||
ast::Expr::FunctionCallStar { .. } => Ok(()),
|
||||
|
@ -253,18 +254,13 @@ pub fn bind_column_references(
|
|||
}
|
||||
}
|
||||
|
||||
pub fn parse_from(
|
||||
fn parse_from_clause_table(
|
||||
schema: &Schema,
|
||||
from: Option<FromClause>,
|
||||
table: ast::SelectTable,
|
||||
operator_id_counter: &mut OperatorIdCounter,
|
||||
) -> Result<(SourceOperator, Vec<BTreeTableReference>)> {
|
||||
if from.as_ref().and_then(|f| f.select.as_ref()).is_none() {
|
||||
return Ok((SourceOperator::Nothing, vec![]));
|
||||
}
|
||||
|
||||
let from = from.unwrap();
|
||||
|
||||
let first_table = match *from.select.unwrap() {
|
||||
cur_table_index: usize,
|
||||
) -> Result<(TableReference, SourceOperator)> {
|
||||
match table {
|
||||
ast::SelectTable::Table(qualified_name, maybe_alias, _) => {
|
||||
let normalized_qualified_name = normalize_ident(qualified_name.name.0.as_str());
|
||||
let Some(table) = schema.get_table(&normalized_qualified_name) else {
|
||||
|
@ -276,29 +272,80 @@ pub fn parse_from(
|
|||
ast::As::Elided(id) => id,
|
||||
})
|
||||
.map(|a| a.0);
|
||||
|
||||
BTreeTableReference {
|
||||
table: table.clone(),
|
||||
let table_reference = TableReference {
|
||||
table: Table::BTree(table.clone()),
|
||||
table_identifier: alias.unwrap_or(normalized_qualified_name),
|
||||
table_index: 0,
|
||||
}
|
||||
table_index: cur_table_index,
|
||||
reference_type: TableReferenceType::BTreeTable,
|
||||
};
|
||||
Ok((
|
||||
table_reference.clone(),
|
||||
SourceOperator::Scan {
|
||||
table_reference,
|
||||
predicates: None,
|
||||
id: operator_id_counter.get_next_id(),
|
||||
iter_dir: None,
|
||||
},
|
||||
))
|
||||
}
|
||||
ast::SelectTable::Select(subselect, maybe_alias) => {
|
||||
let Plan::Select(mut subplan) = prepare_select_plan(schema, subselect)? else {
|
||||
unreachable!();
|
||||
};
|
||||
subplan.query_type = SelectQueryType::Subquery {
|
||||
yield_reg: usize::MAX, // will be set later in bytecode emission
|
||||
coroutine_implementation_start: BranchOffset::MAX, // will be set later in bytecode emission
|
||||
};
|
||||
let identifier = maybe_alias
|
||||
.map(|a| match a {
|
||||
ast::As::As(id) => id.0.clone(),
|
||||
ast::As::Elided(id) => id.0.clone(),
|
||||
})
|
||||
.unwrap_or(format!("subquery_{}", cur_table_index));
|
||||
let table_reference =
|
||||
TableReference::new_subquery(identifier.clone(), cur_table_index, &subplan);
|
||||
Ok((
|
||||
table_reference.clone(),
|
||||
SourceOperator::Subquery {
|
||||
id: operator_id_counter.get_next_id(),
|
||||
table_reference,
|
||||
plan: Box::new(subplan),
|
||||
predicates: None,
|
||||
},
|
||||
))
|
||||
}
|
||||
_ => todo!(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let mut operator = SourceOperator::Scan {
|
||||
table_reference: first_table.clone(),
|
||||
predicates: None,
|
||||
id: operator_id_counter.get_next_id(),
|
||||
iter_dir: None,
|
||||
};
|
||||
pub fn parse_from(
|
||||
schema: &Schema,
|
||||
mut from: Option<FromClause>,
|
||||
operator_id_counter: &mut OperatorIdCounter,
|
||||
) -> Result<(SourceOperator, Vec<TableReference>)> {
|
||||
if from.as_ref().and_then(|f| f.select.as_ref()).is_none() {
|
||||
return Ok((SourceOperator::Nothing, vec![]));
|
||||
}
|
||||
|
||||
let mut tables = vec![first_table];
|
||||
let mut table_index = 0;
|
||||
let mut tables = vec![];
|
||||
|
||||
let mut table_index = 1;
|
||||
for join in from.joins.unwrap_or_default().into_iter() {
|
||||
let (right, outer, using, predicates) =
|
||||
parse_join(schema, join, operator_id_counter, &mut tables, table_index)?;
|
||||
let mut from_owned = std::mem::take(&mut from).unwrap();
|
||||
let select_owned = *std::mem::take(&mut from_owned.select).unwrap();
|
||||
let joins_owned = std::mem::take(&mut from_owned.joins).unwrap_or_default();
|
||||
let (table_reference, mut operator) =
|
||||
parse_from_clause_table(schema, select_owned, operator_id_counter, table_index)?;
|
||||
|
||||
tables.push(table_reference);
|
||||
table_index += 1;
|
||||
|
||||
for join in joins_owned.into_iter() {
|
||||
let JoinParseResult {
|
||||
source_operator: right,
|
||||
is_outer_join: outer,
|
||||
using,
|
||||
predicates,
|
||||
} = parse_join(schema, join, operator_id_counter, &mut tables, table_index)?;
|
||||
operator = SourceOperator::Join {
|
||||
left: Box::new(operator),
|
||||
right: Box::new(right),
|
||||
|
@ -315,7 +362,7 @@ pub fn parse_from(
|
|||
|
||||
pub fn parse_where(
|
||||
where_clause: Option<Expr>,
|
||||
referenced_tables: &[BTreeTableReference],
|
||||
referenced_tables: &[TableReference],
|
||||
) -> Result<Option<Vec<Expr>>> {
|
||||
if let Some(where_expr) = where_clause {
|
||||
let mut predicates = vec![];
|
||||
|
@ -329,48 +376,32 @@ pub fn parse_where(
|
|||
}
|
||||
}
|
||||
|
||||
struct JoinParseResult {
|
||||
source_operator: SourceOperator,
|
||||
is_outer_join: bool,
|
||||
using: Option<ast::DistinctNames>,
|
||||
predicates: Option<Vec<ast::Expr>>,
|
||||
}
|
||||
|
||||
fn parse_join(
|
||||
schema: &Schema,
|
||||
join: ast::JoinedSelectTable,
|
||||
operator_id_counter: &mut OperatorIdCounter,
|
||||
tables: &mut Vec<BTreeTableReference>,
|
||||
tables: &mut Vec<TableReference>,
|
||||
table_index: usize,
|
||||
) -> Result<(
|
||||
SourceOperator,
|
||||
bool,
|
||||
Option<ast::DistinctNames>,
|
||||
Option<Vec<ast::Expr>>,
|
||||
)> {
|
||||
) -> Result<JoinParseResult> {
|
||||
let ast::JoinedSelectTable {
|
||||
operator,
|
||||
operator: join_operator,
|
||||
table,
|
||||
constraint,
|
||||
} = join;
|
||||
|
||||
let table = match table {
|
||||
ast::SelectTable::Table(qualified_name, maybe_alias, _) => {
|
||||
let normalized_name = normalize_ident(qualified_name.name.0.as_str());
|
||||
let Some(table) = schema.get_table(&normalized_name) else {
|
||||
crate::bail_parse_error!("Table {} not found", normalized_name);
|
||||
};
|
||||
let alias = maybe_alias
|
||||
.map(|a| match a {
|
||||
ast::As::As(id) => id,
|
||||
ast::As::Elided(id) => id,
|
||||
})
|
||||
.map(|a| a.0);
|
||||
BTreeTableReference {
|
||||
table: table.clone(),
|
||||
table_identifier: alias.unwrap_or(normalized_name),
|
||||
table_index,
|
||||
}
|
||||
}
|
||||
_ => todo!(),
|
||||
};
|
||||
let (table_reference, source_operator) =
|
||||
parse_from_clause_table(schema, table, operator_id_counter, table_index)?;
|
||||
|
||||
tables.push(table.clone());
|
||||
tables.push(table_reference);
|
||||
|
||||
let (outer, natural) = match operator {
|
||||
let (outer, natural) = match join_operator {
|
||||
ast::JoinOperator::TypedJoin(Some(join_type)) => {
|
||||
let is_outer = join_type.contains(JoinType::OUTER);
|
||||
let is_natural = join_type.contains(JoinType::NATURAL);
|
||||
|
@ -391,13 +422,13 @@ fn parse_join(
|
|||
let left_tables = &tables[..table_index];
|
||||
assert!(!left_tables.is_empty());
|
||||
let right_table = &tables[table_index];
|
||||
let right_cols = &right_table.table.columns;
|
||||
let right_cols = &right_table.columns();
|
||||
let mut distinct_names = None;
|
||||
// TODO: O(n^2) maybe not great for large tables or big multiway joins
|
||||
for right_col in right_cols.iter() {
|
||||
let mut found_match = false;
|
||||
for left_table in left_tables.iter() {
|
||||
for left_col in left_table.table.columns.iter() {
|
||||
for left_col in left_table.columns().iter() {
|
||||
if left_col.name == right_col.name {
|
||||
if distinct_names.is_none() {
|
||||
distinct_names =
|
||||
|
@ -447,8 +478,7 @@ fn parse_join(
|
|||
let mut left_col = None;
|
||||
for (left_table_idx, left_table) in left_tables.iter().enumerate() {
|
||||
left_col = left_table
|
||||
.table
|
||||
.columns
|
||||
.columns()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, col)| col.name == name_normalized)
|
||||
|
@ -464,8 +494,7 @@ fn parse_join(
|
|||
);
|
||||
}
|
||||
let right_col = right_table
|
||||
.table
|
||||
.columns
|
||||
.columns()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, col)| col.name == name_normalized);
|
||||
|
@ -499,17 +528,12 @@ fn parse_join(
|
|||
}
|
||||
}
|
||||
|
||||
Ok((
|
||||
SourceOperator::Scan {
|
||||
table_reference: table.clone(),
|
||||
predicates: None,
|
||||
id: operator_id_counter.get_next_id(),
|
||||
iter_dir: None,
|
||||
},
|
||||
outer,
|
||||
Ok(JoinParseResult {
|
||||
source_operator,
|
||||
is_outer_join: outer,
|
||||
using,
|
||||
predicates,
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn parse_limit(limit: Limit) -> Option<usize> {
|
||||
|
|
|
@ -2,6 +2,8 @@ use std::rc::Weak;
|
|||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use super::emitter::emit_program;
|
||||
use super::expr::get_name;
|
||||
use super::plan::SelectQueryType;
|
||||
use crate::function::Func;
|
||||
use crate::storage::sqlite3_ondisk::DatabaseHeader;
|
||||
use crate::translate::optimizer::optimize_plan;
|
||||
|
@ -23,15 +25,15 @@ pub fn translate_select(
|
|||
connection: Weak<Connection>,
|
||||
syms: &SymbolTable,
|
||||
) -> Result<Program> {
|
||||
let select_plan = prepare_select_plan(schema, select)?;
|
||||
let optimized_plan = optimize_plan(select_plan)?;
|
||||
emit_program(database_header, optimized_plan, connection, syms)
|
||||
let mut select_plan = prepare_select_plan(schema, select)?;
|
||||
optimize_plan(&mut select_plan)?;
|
||||
emit_program(database_header, select_plan, connection, syms)
|
||||
}
|
||||
|
||||
pub fn prepare_select_plan(schema: &Schema, select: ast::Select) -> Result<Plan> {
|
||||
match select.body.select {
|
||||
ast::OneSelect::Select {
|
||||
columns,
|
||||
mut columns,
|
||||
from,
|
||||
where_clause,
|
||||
group_by,
|
||||
|
@ -58,13 +60,14 @@ pub fn prepare_select_plan(schema: &Schema, select: ast::Select) -> Result<Plan>
|
|||
referenced_tables,
|
||||
available_indexes: schema.indexes.clone().into_values().flatten().collect(),
|
||||
contains_constant_false_condition: false,
|
||||
query_type: SelectQueryType::TopLevel,
|
||||
};
|
||||
|
||||
// Parse the WHERE clause
|
||||
plan.where_clause = parse_where(where_clause, &plan.referenced_tables)?;
|
||||
|
||||
let mut aggregate_expressions = Vec::new();
|
||||
for column in columns.clone() {
|
||||
for (result_column_idx, column) in columns.iter_mut().enumerate() {
|
||||
match column {
|
||||
ast::ResultColumn::Star => {
|
||||
plan.source.select_star(&mut plan.result_columns);
|
||||
|
@ -80,7 +83,7 @@ pub fn prepare_select_plan(schema: &Schema, select: ast::Select) -> Result<Plan>
|
|||
crate::bail_parse_error!("Table {} not found", name.0);
|
||||
}
|
||||
let table_reference = referenced_table.unwrap();
|
||||
for (idx, col) in table_reference.table.columns.iter().enumerate() {
|
||||
for (idx, col) in table_reference.columns().iter().enumerate() {
|
||||
plan.result_columns.push(ResultSetColumn {
|
||||
expr: ast::Expr::Column {
|
||||
database: None, // TODO: support different databases
|
||||
|
@ -88,13 +91,14 @@ pub fn prepare_select_plan(schema: &Schema, select: ast::Select) -> Result<Plan>
|
|||
column: idx,
|
||||
is_rowid_alias: col.is_rowid_alias,
|
||||
},
|
||||
name: col.name.clone(),
|
||||
contains_aggregates: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
ast::ResultColumn::Expr(mut expr, _) => {
|
||||
bind_column_references(&mut expr, &plan.referenced_tables)?;
|
||||
match &expr {
|
||||
ast::ResultColumn::Expr(ref mut expr, maybe_alias) => {
|
||||
bind_column_references(expr, &plan.referenced_tables)?;
|
||||
match expr {
|
||||
ast::Expr::FunctionCall {
|
||||
name,
|
||||
distinctness: _,
|
||||
|
@ -119,6 +123,12 @@ pub fn prepare_select_plan(schema: &Schema, select: ast::Select) -> Result<Plan>
|
|||
};
|
||||
aggregate_expressions.push(agg.clone());
|
||||
plan.result_columns.push(ResultSetColumn {
|
||||
name: get_name(
|
||||
maybe_alias.as_ref(),
|
||||
&expr,
|
||||
&plan.referenced_tables,
|
||||
|| format!("expr_{}", result_column_idx),
|
||||
),
|
||||
expr: expr.clone(),
|
||||
contains_aggregates: true,
|
||||
});
|
||||
|
@ -127,6 +137,12 @@ pub fn prepare_select_plan(schema: &Schema, select: ast::Select) -> Result<Plan>
|
|||
let contains_aggregates =
|
||||
resolve_aggregates(&expr, &mut aggregate_expressions);
|
||||
plan.result_columns.push(ResultSetColumn {
|
||||
name: get_name(
|
||||
maybe_alias.as_ref(),
|
||||
&expr,
|
||||
&plan.referenced_tables,
|
||||
|| format!("expr_{}", result_column_idx),
|
||||
),
|
||||
expr: expr.clone(),
|
||||
contains_aggregates,
|
||||
});
|
||||
|
@ -151,6 +167,12 @@ pub fn prepare_select_plan(schema: &Schema, select: ast::Select) -> Result<Plan>
|
|||
};
|
||||
aggregate_expressions.push(agg.clone());
|
||||
plan.result_columns.push(ResultSetColumn {
|
||||
name: get_name(
|
||||
maybe_alias.as_ref(),
|
||||
&expr,
|
||||
&plan.referenced_tables,
|
||||
|| format!("expr_{}", result_column_idx),
|
||||
),
|
||||
expr: expr.clone(),
|
||||
contains_aggregates: true,
|
||||
});
|
||||
|
@ -163,8 +185,14 @@ pub fn prepare_select_plan(schema: &Schema, select: ast::Select) -> Result<Plan>
|
|||
}
|
||||
expr => {
|
||||
let contains_aggregates =
|
||||
resolve_aggregates(expr, &mut aggregate_expressions);
|
||||
resolve_aggregates(&expr, &mut aggregate_expressions);
|
||||
plan.result_columns.push(ResultSetColumn {
|
||||
name: get_name(
|
||||
maybe_alias.as_ref(),
|
||||
&expr,
|
||||
&plan.referenced_tables,
|
||||
|| format!("expr_{}", result_column_idx),
|
||||
),
|
||||
expr: expr.clone(),
|
||||
contains_aggregates,
|
||||
});
|
||||
|
|
|
@ -99,7 +99,7 @@ pub struct ProgramState {
|
|||
registers: Vec<OwnedValue>,
|
||||
last_compare: Option<std::cmp::Ordering>,
|
||||
deferred_seek: Option<(CursorID, CursorID)>,
|
||||
ended_coroutine: bool, // flag to notify yield coroutine finished
|
||||
ended_coroutine: HashMap<usize, bool>, // flag to indicate that a coroutine has ended (key is the yield register)
|
||||
regex_cache: RegexCache,
|
||||
interrupted: bool,
|
||||
}
|
||||
|
@ -115,7 +115,7 @@ impl ProgramState {
|
|||
registers,
|
||||
last_compare: None,
|
||||
deferred_seek: None,
|
||||
ended_coroutine: false,
|
||||
ended_coroutine: HashMap::new(),
|
||||
regex_cache: RegexCache::new(),
|
||||
interrupted: false,
|
||||
}
|
||||
|
@ -179,6 +179,7 @@ impl Program {
|
|||
}
|
||||
let insn = &self.insns[state.pc as usize];
|
||||
trace_insn(self, state.pc as InsnReference, insn);
|
||||
// print_insn(self, state.pc as InsnReference, insn, "".to_string());
|
||||
let mut cursors = state.cursors.borrow_mut();
|
||||
match insn {
|
||||
Insn::Init { target_pc } => {
|
||||
|
@ -2279,12 +2280,18 @@ impl Program {
|
|||
jump_on_definition,
|
||||
start_offset,
|
||||
} => {
|
||||
assert!(*jump_on_definition >= 0);
|
||||
state.registers[*yield_reg] = OwnedValue::Integer(*start_offset);
|
||||
state.pc = *jump_on_definition;
|
||||
state.ended_coroutine.insert(*yield_reg, false);
|
||||
state.pc = if *jump_on_definition == 0 {
|
||||
state.pc + 1
|
||||
} else {
|
||||
*jump_on_definition
|
||||
};
|
||||
}
|
||||
Insn::EndCoroutine { yield_reg } => {
|
||||
if let OwnedValue::Integer(pc) = state.registers[*yield_reg] {
|
||||
state.ended_coroutine = true;
|
||||
state.ended_coroutine.insert(*yield_reg, true);
|
||||
state.pc = pc - 1; // yield jump is always next to yield. Here we substract 1 to go back to yield instruction
|
||||
} else {
|
||||
unreachable!();
|
||||
|
@ -2295,7 +2302,7 @@ impl Program {
|
|||
end_offset,
|
||||
} => {
|
||||
if let OwnedValue::Integer(pc) = state.registers[*yield_reg] {
|
||||
if state.ended_coroutine {
|
||||
if *state.ended_coroutine.get(yield_reg).unwrap_or(&false) {
|
||||
state.pc = *end_offset;
|
||||
} else {
|
||||
// swap
|
||||
|
@ -2303,7 +2310,10 @@ impl Program {
|
|||
(pc, OwnedValue::Integer(state.pc + 1));
|
||||
}
|
||||
} else {
|
||||
unreachable!();
|
||||
unreachable!(
|
||||
"yield_reg {} contains non-integer value: {:?}",
|
||||
*yield_reg, state.registers[*yield_reg]
|
||||
);
|
||||
}
|
||||
}
|
||||
Insn::InsertAsync {
|
||||
|
|
|
@ -17,4 +17,5 @@ source $testdir/pragma.test
|
|||
source $testdir/scalar-functions.test
|
||||
source $testdir/scalar-functions-datetime.test
|
||||
source $testdir/select.test
|
||||
source $testdir/subquery.test
|
||||
source $testdir/where.test
|
||||
|
|
194
testing/subquery.test
Normal file
194
testing/subquery.test
Normal file
|
@ -0,0 +1,194 @@
|
|||
#!/usr/bin/env tclsh
|
||||
|
||||
set testdir [file dirname $argv0]
|
||||
source $testdir/tester.tcl
|
||||
|
||||
do_execsql_test subquery-inner-filter {
|
||||
select sub.loud_hat from (
|
||||
select concat(name, '!!!') as loud_hat
|
||||
from products where name = 'hat'
|
||||
) sub;
|
||||
} {hat!!!}
|
||||
|
||||
do_execsql_test subquery-outer-filter {
|
||||
select sub.loud_hat from (
|
||||
select concat(name, '!!!') as loud_hat
|
||||
from products
|
||||
) sub where sub.loud_hat = 'hat!!!'
|
||||
} {hat!!!}
|
||||
|
||||
do_execsql_test subquery-without-alias {
|
||||
select loud_hat from (
|
||||
select concat(name, '!!!') as loud_hat
|
||||
from products where name = 'hat'
|
||||
);
|
||||
} {hat!!!}
|
||||
|
||||
do_execsql_test subquery-no-alias-on-col {
|
||||
select price from (
|
||||
select * from products where name = 'hat'
|
||||
)
|
||||
} {79.0}
|
||||
|
||||
do_execsql_test subquery-no-alias-on-col-named {
|
||||
select price from (
|
||||
select price from products where name = 'hat'
|
||||
)
|
||||
} {79.0}
|
||||
|
||||
do_execsql_test subquery-select-star {
|
||||
select * from (
|
||||
select price, price + 1.0, name from products where name = 'hat'
|
||||
)
|
||||
} {79.0|80.0|hat}
|
||||
|
||||
do_execsql_test subquery-select-table-star {
|
||||
select sub.* from (
|
||||
select price, price + 1.0, name from products where name = 'hat'
|
||||
) sub
|
||||
} {79.0|80.0|hat}
|
||||
|
||||
do_execsql_test nested-subquery {
|
||||
select sub.loudest_hat from (
|
||||
select upper(nested_sub.loud_hat) as loudest_hat from (
|
||||
select concat(name, '!!!') as loud_hat
|
||||
from products where name = 'hat'
|
||||
) nested_sub
|
||||
) sub;
|
||||
} {HAT!!!}
|
||||
|
||||
do_execsql_test subquery-orderby-limit {
|
||||
select upper(sub.loud_name) as loudest_name
|
||||
from (
|
||||
select concat(name, '!!!') as loud_name
|
||||
from products
|
||||
order by name
|
||||
limit 3
|
||||
) sub;
|
||||
} {ACCESSORIES!!!
|
||||
BOOTS!!!
|
||||
CAP!!!}
|
||||
|
||||
do_execsql_test table-join-subquery {
|
||||
select sub.product_name, p.name
|
||||
from products p join (
|
||||
select name as product_name
|
||||
from products
|
||||
) sub on p.name = sub.product_name where p.name = 'hat'
|
||||
} {hat|hat}
|
||||
|
||||
do_execsql_test subquery-join-table {
|
||||
select sub.product_name, p.name
|
||||
from (
|
||||
select name as product_name
|
||||
from products
|
||||
) sub join products p on sub.product_name = p.name where sub.product_name = 'hat'
|
||||
} {hat|hat}
|
||||
|
||||
do_execsql_test subquery-join-subquery {
|
||||
select sub1.sus_name, sub2.truthful_name
|
||||
from (
|
||||
select name as sus_name
|
||||
from products
|
||||
where name = 'cap'
|
||||
) sub1 join (
|
||||
select concat('no ', name) as truthful_name
|
||||
from products
|
||||
where name = 'cap'
|
||||
) sub2;
|
||||
} {"cap|no cap"}
|
||||
|
||||
do_execsql_test select-star-table-subquery {
|
||||
select *
|
||||
from products p join (
|
||||
select name, price
|
||||
from products
|
||||
where name = 'hat'
|
||||
) sub on p.name = sub.name;
|
||||
} {1|hat|79.0|hat|79.0}
|
||||
|
||||
do_execsql_test select-star-subquery-table {
|
||||
select *
|
||||
from (
|
||||
select name, price
|
||||
from products
|
||||
where name = 'hat'
|
||||
) sub join products p on sub.name = p.name;
|
||||
} {hat|79.0|1|hat|79.0}
|
||||
|
||||
do_execsql_test select-star-subquery-subquery {
|
||||
select *
|
||||
from (
|
||||
select name, price
|
||||
from products
|
||||
where name = 'hat'
|
||||
) sub1 join (
|
||||
select price
|
||||
from products
|
||||
where name = 'hat'
|
||||
) sub2 on sub1.price = sub2.price;
|
||||
} {hat|79.0|79.0}
|
||||
|
||||
|
||||
do_execsql_test subquery-inner-grouping {
|
||||
select is_jennifer, person_count
|
||||
from (
|
||||
select first_name = 'Jennifer' as is_jennifer, count(1) as person_count from users
|
||||
group by first_name = 'Jennifer'
|
||||
) order by person_count asc
|
||||
} {1|151
|
||||
0|9849}
|
||||
|
||||
do_execsql_test subquery-outer-grouping {
|
||||
select is_jennifer, count(1) as person_count
|
||||
from (
|
||||
select first_name = 'Jennifer' as is_jennifer from users
|
||||
) group by is_jennifer order by count(1) asc
|
||||
} {1|151
|
||||
0|9849}
|
||||
|
||||
do_execsql_test subquery-join-using-with-outer-limit {
|
||||
SELECT p.name, sub.funny_name
|
||||
FROM products p
|
||||
JOIN (
|
||||
select id, concat(name, '-lol') as funny_name
|
||||
from products
|
||||
) sub USING (id)
|
||||
LIMIT 3;
|
||||
} {"hat|hat-lol
|
||||
cap|cap-lol
|
||||
shirt|shirt-lol"}
|
||||
|
||||
do_execsql_test subquery-join-using-with-inner-limit {
|
||||
SELECT p.name, sub.funny_name
|
||||
FROM products p
|
||||
JOIN (
|
||||
select id, concat(name, '-lol') as funny_name
|
||||
from products
|
||||
limit 3
|
||||
) sub USING (id);
|
||||
} {"hat|hat-lol
|
||||
cap|cap-lol
|
||||
shirt|shirt-lol"}
|
||||
|
||||
do_execsql_test subquery-join-using-with-both-limits {
|
||||
SELECT p.name, sub.funny_name
|
||||
FROM products p
|
||||
JOIN (
|
||||
select id, concat(name, '-lol') as funny_name
|
||||
from products
|
||||
limit 3
|
||||
) sub USING (id)
|
||||
LIMIT 2;
|
||||
} {"hat|hat-lol
|
||||
cap|cap-lol"}
|
||||
|
||||
do_execsql_test subquery-containing-join {
|
||||
select foo, bar
|
||||
from (
|
||||
select p.name as foo, u.first_name as bar
|
||||
from products p join users u using (id)
|
||||
) limit 3;
|
||||
} {hat|Jamie
|
||||
cap|Cindy
|
||||
shirt|Tommy}
|
Loading…
Add table
Add a link
Reference in a new issue