mirror of
https://github.com/aljazceru/turso.git
synced 2026-02-06 16:54:23 +01:00
Merge 'Support uncorrelated FROM clause subqueries' from Jussi Saurio
I will warn that this PR is quite big out of necessity, since subqueries
are, as the name implies, queries within queries, so everything that
works with a regular query should also work with a subquery, roughly
speaking.
---
- Adds support for:
* uncorrelated subqueries in FROM clause (i.e. appear as a "table",
and do not refer to outer tables). Example of this at the end of the PR
description.
* column and subquery aliasing (`select sub.renamed from (select
name as renamed from products) sub`)
* inner and outer filtering of subqueries (`select sub.name from
(select name from products where name = 'joe') sub`, and, `select
sub.name from (select name from products) sub where sub.name = 'joe'`)
* joining between regular tables and subqueries
* joining between multiple subqueries
* in general working with subqueries should roughly equal working
with regular tables
- Main idea: subqueries are just wrappers of a `SelectPlan` that never
emit ResultRows, instead they `Yield` control back to the parent query,
and the parent query can copy the subquery result values into a
ResultRow. New variant `SourceOperator::Subquery` that wraps a subquery
`SelectPlan`.
- Plans can now not only refer to btree tables (`select p.name from
products`) but also subqueries (`select sub.foo from (select name as foo
from products) sub`. Hence this PR also adds support for column aliases
which didn't exist before.
* An `Expr::Column` that refers to a regular table will result in an
`Insn::Column` (i.e. a read from disk/memory) whereas an `Expr::Column`
that refers to a subquery will result in an `Insn::Copy` (from register
to register) instead
- Subquery handling is entirely unoptimized, there's no predicate
pushdown from outer query to subqueries, or elimination of redundant
subqueries (e.g. in the trivial example `SELECT * FROM (SELECT * FROM
users) sub` the subquery can just be entirely removed)
---
This PR does not add support (yet) for:
- subqueries in result columns: `SELECT t.foo, (SELECT .......) as
column_from_subquery FROM t`
- subqueries in WHERE clauses e.g. `SELECT * FROM t1 WHERE t1.foo IN
(SELECT ...)`
- subquery-related optimizations, of which there are plenty available.
No analysis is done regarding e.g. whether predicates on the outer query
level could be pushed into the subquery, or whether the subquery could
be entirely eliminated. Both of the above can probably be done fairly
easily for a bunch of trivial cases.
---
Example bytecode with comments added:
```
limbo> EXPLAIN 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;
addr opcode p1 p2 p3 p4 p5 comment
---- ----------------- ---- ---- ---- ------------- -- -------
0 Init 0 31 0 0 Start at 31
// Coroutine implementation starts at insn 2, jump immediately to 14
1 InitCoroutine 1 14 2 0
2 OpenReadAsync 0 3 0 0 table=products, root=3
3 OpenReadAwait 0 0 0 0
4 RewindAsync 0 0 0 0
5 RewindAwait 0 13 0 0 Rewind table products
6 RowId 0 2 0 0 r[2]=products.rowid
7 Column 0 1 4 0 r[4]=products.name
8 String8 0 5 0 -lol 0 r[5]='-lol'
9 Function 0 4 3 concat 0 r[3]=func(r[4..5])
// jump back to main loop of query (insn 20)
10 Yield 1 0 0 0
11 NextAsync 0 0 0 0
12 NextAwait 0 6 0 0
13 EndCoroutine 1 0 0 0
14 OpenReadAsync 1 3 0 0 table=p, root=3
15 OpenReadAwait 0 0 0 0
16 RewindAsync 1 0 0 0
17 RewindAwait 1 30 0 0 Rewind table p
// Since this subquery is the inner loop of the join, reinitialize it on every iteration of the outer loop
18 InitCoroutine 1 0 2 0
// Jump back to the subquery implementation to assign another row into registers
19 Yield 1 28 0 0
20 RowId 1 8 0 0 r[8]=p.rowid
// Copy sub.id
21 Copy 2 9 0 0 r[9]=r[2]
// p.id == sub.id?
22 Ne 8 9 27 0 if r[8]!=r[9] goto 27
23 Column 1 1 6 0 r[6]=p.name
// copy sub.funny_name
24 Copy 3 7 0 0 r[7]=r[3]
25 ResultRow 6 2 0 0 output=r[6..7]
26 DecrJumpZero 10 30 0 0 if (--r[10]==0) goto 30
27 Goto 0 19 0 0
28 NextAsync 1 0 0 0
29 NextAwait 1 18 0 0
30 Halt 0 0 0 0
31 Transaction 0 0 0 0
32 Integer 3 10 0 0 r[10]=3
33 Goto 0 1 0 0
```
Closes #566
This commit is contained in:
14
core/lib.rs
14
core/lib.rs
@@ -250,7 +250,7 @@ impl Connection {
|
||||
self.header.clone(),
|
||||
self.pager.clone(),
|
||||
Rc::downgrade(self),
|
||||
&syms,
|
||||
syms,
|
||||
)?);
|
||||
Ok(Statement::new(program, self.pager.clone()))
|
||||
}
|
||||
@@ -278,7 +278,7 @@ impl Connection {
|
||||
self.header.clone(),
|
||||
self.pager.clone(),
|
||||
Rc::downgrade(self),
|
||||
&syms,
|
||||
syms,
|
||||
)?);
|
||||
let stmt = Statement::new(program, self.pager.clone());
|
||||
Ok(Some(Rows { stmt }))
|
||||
@@ -290,7 +290,7 @@ impl Connection {
|
||||
self.header.clone(),
|
||||
self.pager.clone(),
|
||||
Rc::downgrade(self),
|
||||
&syms,
|
||||
syms,
|
||||
)?;
|
||||
program.explain();
|
||||
Ok(None)
|
||||
@@ -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!(),
|
||||
@@ -327,7 +327,7 @@ impl Connection {
|
||||
self.header.clone(),
|
||||
self.pager.clone(),
|
||||
Rc::downgrade(self),
|
||||
&syms,
|
||||
syms,
|
||||
)?;
|
||||
program.explain();
|
||||
}
|
||||
@@ -339,7 +339,7 @@ impl Connection {
|
||||
self.header.clone(),
|
||||
self.pager.clone(),
|
||||
Rc::downgrade(self),
|
||||
&syms,
|
||||
syms,
|
||||
)?;
|
||||
let mut state = vdbe::ProgramState::new(program.max_registers);
|
||||
program.step(&mut state, self.pager.clone())?;
|
||||
|
||||
@@ -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,
|
||||
@@ -196,7 +211,7 @@ fn use_indexes(
|
||||
use_indexes(right, referenced_tables, available_indexes)?;
|
||||
Ok(())
|
||||
}
|
||||
SourceOperator::Nothing => Ok(()),
|
||||
SourceOperator::Nothing { .. } => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,6 +244,7 @@ fn eliminate_constants(
|
||||
}
|
||||
}
|
||||
match operator {
|
||||
SourceOperator::Subquery { .. } => Ok(ConstantConditionEliminationResult::Continue),
|
||||
SourceOperator::Join {
|
||||
left,
|
||||
right,
|
||||
@@ -319,7 +335,7 @@ fn eliminate_constants(
|
||||
|
||||
Ok(ConstantConditionEliminationResult::Continue)
|
||||
}
|
||||
SourceOperator::Nothing => Ok(ConstantConditionEliminationResult::Continue),
|
||||
SourceOperator::Nothing { .. } => Ok(ConstantConditionEliminationResult::Continue),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
@@ -413,7 +430,7 @@ fn push_predicates(
|
||||
// Base cases - nowhere else to push to
|
||||
SourceOperator::Scan { .. } => Ok(()),
|
||||
SourceOperator::Search { .. } => Ok(()),
|
||||
SourceOperator::Nothing => Ok(()),
|
||||
SourceOperator::Nothing { .. } => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -510,7 +585,7 @@ fn push_predicate(
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
SourceOperator::Nothing => Ok(Some(predicate)),
|
||||
SourceOperator::Nothing { .. } => Ok(Some(predicate)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -946,9 +1018,3 @@ impl TakeOwnership for ast::Expr {
|
||||
std::mem::replace(self, ast::Expr::Literal(ast::Literal::Null))
|
||||
}
|
||||
}
|
||||
|
||||
impl TakeOwnership for SourceOperator {
|
||||
fn take_ownership(&mut self) -> Self {
|
||||
std::mem::replace(self, Self::Nothing)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,14 +158,16 @@ 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(),
|
||||
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,76 @@ 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,
|
||||
Nothing {
|
||||
id: usize,
|
||||
},
|
||||
}
|
||||
|
||||
/// 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,7 +307,8 @@ impl SourceOperator {
|
||||
SourceOperator::Join { id, .. } => *id,
|
||||
SourceOperator::Scan { id, .. } => *id,
|
||||
SourceOperator::Search { id, .. } => *id,
|
||||
SourceOperator::Nothing => unreachable!(),
|
||||
SourceOperator::Subquery { id, .. } => *id,
|
||||
SourceOperator::Nothing { id } => *id,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -320,12 +398,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,7 +444,10 @@ impl Display for SourceOperator {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
SourceOperator::Nothing => Ok(()),
|
||||
SourceOperator::Subquery { plan, .. } => {
|
||||
fmt_operator(&plan.source, f, level + 1, last)
|
||||
}
|
||||
SourceOperator::Nothing { .. } => Ok(()),
|
||||
}
|
||||
}
|
||||
writeln!(f, "QUERY PLAN")?;
|
||||
@@ -382,7 +464,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,7 +491,8 @@ pub fn get_table_ref_bitmask_for_operator<'a>(
|
||||
.position(|t| t.table_identifier == table_reference.table_identifier)
|
||||
.unwrap();
|
||||
}
|
||||
SourceOperator::Nothing => {}
|
||||
SourceOperator::Subquery { .. } => {}
|
||||
SourceOperator::Nothing { .. } => {}
|
||||
}
|
||||
Ok(table_refs_mask)
|
||||
}
|
||||
@@ -424,7 +507,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,85 @@ 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 {
|
||||
id: operator_id_counter.get_next_id(),
|
||||
},
|
||||
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 +367,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 +381,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 +427,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 +483,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 +499,7 @@ fn parse_join(
|
||||
);
|
||||
}
|
||||
let right_col = right_table
|
||||
.table
|
||||
.columns
|
||||
.columns()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, col)| col.name == name_normalized);
|
||||
@@ -499,17 +533,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,14 +123,26 @@ 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,
|
||||
});
|
||||
}
|
||||
Ok(_) => {
|
||||
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,
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -103,7 +103,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,
|
||||
}
|
||||
@@ -119,7 +119,7 @@ impl ProgramState {
|
||||
registers,
|
||||
last_compare: None,
|
||||
deferred_seek: None,
|
||||
ended_coroutine: false,
|
||||
ended_coroutine: HashMap::new(),
|
||||
regex_cache: RegexCache::new(),
|
||||
interrupted: false,
|
||||
}
|
||||
@@ -1667,12 +1667,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!();
|
||||
@@ -1683,15 +1689,23 @@ impl Program {
|
||||
end_offset,
|
||||
} => {
|
||||
if let OwnedValue::Integer(pc) = state.registers[*yield_reg] {
|
||||
if state.ended_coroutine {
|
||||
if *state
|
||||
.ended_coroutine
|
||||
.get(yield_reg)
|
||||
.expect("coroutine not initialized")
|
||||
{
|
||||
state.pc = *end_offset;
|
||||
} else {
|
||||
// swap
|
||||
// swap the program counter with the value in the yield register
|
||||
// this is the mechanism that allows jumping back and forth between the coroutine and the caller
|
||||
(state.pc, state.registers[*yield_reg]) =
|
||||
(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}
|
||||
Reference in New Issue
Block a user