feat: subqueries in FROM clause

This commit is contained in:
Jussi Saurio 2024-12-31 14:13:11 +02:00
parent b1ca2b050b
commit 2066475e03
12 changed files with 1092 additions and 310 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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,
});

View file

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

View file

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