Refactor join processing

- Make all constraints a list of WhereTerms in a ProcessedWhereClause
- Support multiple joins instead of just one
This commit is contained in:
jussisaurio 2024-07-22 21:21:08 +03:00
parent b2ba69cfd5
commit 84cf4033d5
8 changed files with 368 additions and 400 deletions

View file

@ -36,10 +36,11 @@ pub fn build_select<'a>(schema: &Schema, select: &'a ast::Select) -> Result<Sele
Some(table) => table,
None => anyhow::bail!("Parse error: no such table: {}", table_name),
};
let identifier = normalize_ident(maybe_alias.unwrap_or(table_name));
let mut joins = Vec::new();
joins.push(SrcTable {
table: Table::BTree(table.clone()),
alias: maybe_alias,
identifier,
join_info: None,
});
if let Some(selected_joins) = &from.joins {
@ -60,9 +61,11 @@ pub fn build_select<'a>(schema: &Schema, select: &'a ast::Select) -> Result<Sele
Some(table) => table,
None => anyhow::bail!("Parse error: no such table: {}", table_name),
};
let identifier = normalize_ident(maybe_alias.unwrap_or(table_name));
joins.push(SrcTable {
table: Table::BTree(table),
alias: maybe_alias,
identifier,
join_info: Some(join),
});
}
@ -292,7 +295,7 @@ pub fn translate_expr(
};
for arg in args {
let reg = program.alloc_register();
let _ = translate_expr(program, select, &arg, reg, cursor_hint)?;
let _ = translate_expr(program, select, arg, reg, cursor_hint)?;
match arg {
ast::Expr::Literal(_) => program.mark_last_insn_constant(),
_ => {}
@ -625,11 +628,11 @@ fn wrap_eval_jump_expr(
program.preassign_label_to_next_insn(if_true_label);
}
pub fn resolve_ident_qualified<'a>(
pub fn resolve_ident_qualified(
program: &ProgramBuilder,
table_name: &String,
ident: &String,
select: &'a Select,
select: &Select,
cursor_hint: Option<usize>,
) -> Result<(usize, Type, usize, bool)> {
let ident = normalize_ident(ident);
@ -637,11 +640,7 @@ pub fn resolve_ident_qualified<'a>(
for join in &select.src_tables {
match join.table {
Table::BTree(ref table) => {
let table_identifier = normalize_ident(match join.alias {
Some(alias) => alias,
None => &table.name,
});
if table_identifier == *table_name {
if *join.identifier == table_name {
let res = table
.columns
.iter()
@ -649,7 +648,7 @@ pub fn resolve_ident_qualified<'a>(
.find(|(_, col)| col.name == *ident);
if res.is_some() {
let (idx, col) = res.unwrap();
let cursor_id = program.resolve_cursor_id(&table_identifier, cursor_hint);
let cursor_id = program.resolve_cursor_id(&join.identifier, cursor_hint);
return Ok((idx, col.ty, cursor_id, col.primary_key));
}
}
@ -664,10 +663,10 @@ pub fn resolve_ident_qualified<'a>(
);
}
pub fn resolve_ident_table<'a>(
pub fn resolve_ident_table(
program: &ProgramBuilder,
ident: &String,
select: &'a Select,
select: &Select,
cursor_hint: Option<usize>,
) -> Result<(usize, Type, usize, bool)> {
let ident = normalize_ident(ident);
@ -675,10 +674,6 @@ pub fn resolve_ident_table<'a>(
for join in &select.src_tables {
match join.table {
Table::BTree(ref table) => {
let table_identifier = normalize_ident(match join.alias {
Some(alias) => alias,
None => &table.name,
});
let res = table
.columns
.iter()
@ -704,7 +699,7 @@ pub fn resolve_ident_table<'a>(
is_primary_key = res.1.primary_key;
}
}
let cursor_id = program.resolve_cursor_id(&table_identifier, cursor_hint);
let cursor_id = program.resolve_cursor_id(&join.identifier, cursor_hint);
found.push((idx, col_type, cursor_id, is_primary_key));
}
}

View file

@ -10,15 +10,15 @@ use crate::pager::Pager;
use crate::schema::{Column, PseudoTable, Schema, Table};
use crate::sqlite3_ondisk::{DatabaseHeader, MIN_PAGE_CACHE_SIZE};
use crate::translate::select::{ColumnInfo, LoopInfo, Select, SrcTable};
use crate::translate::where_clause::{
evaluate_conditions, translate_conditions, translate_where, Inner, Left, QueryConstraint,
};
use crate::translate::where_clause::{translate_processed_where, translate_where};
use crate::types::{OwnedRecord, OwnedValue};
use crate::util::normalize_ident;
use crate::vdbe::{BranchOffset, Insn, Program, builder::ProgramBuilder};
use anyhow::Result;
use expr::{build_select, maybe_apply_affinity, translate_expr};
use select::LeftJoinBookkeeping;
use sqlite3_parser::ast::{self, Literal};
use where_clause::{process_where, ProcessedWhereClause};
struct LimitInfo {
limit_reg: usize,
@ -114,7 +114,7 @@ fn translate_select(mut select: Select) -> Result<Program> {
};
if !select.src_tables.is_empty() {
let constraint = translate_tables_begin(&mut program, &mut select)?;
translate_tables_begin(&mut program, &mut select)?;
let (register_start, column_count) = if let Some(sort_columns) = select.order_by {
let start = program.next_free_register();
@ -153,7 +153,7 @@ fn translate_select(mut select: Select) -> Result<Program> {
}
}
translate_tables_end(&mut program, &select, constraint);
translate_tables_end(&mut program, &select);
if select.exist_aggregation {
let mut target = register_start;
@ -283,7 +283,7 @@ fn translate_sorter(
start_reg: register_start,
count,
});
emit_limit_insn(&limit_info, program);
emit_limit_insn(limit_info, program);
program.emit_insn(Insn::SorterNext {
cursor_id: sort_info.sorter_cursor,
pc_if_next: sorter_data_offset,
@ -292,145 +292,44 @@ fn translate_sorter(
Ok(())
}
fn translate_tables_begin(
program: &mut ProgramBuilder,
select: &mut Select,
) -> Result<Option<QueryConstraint>> {
fn translate_tables_begin(program: &mut ProgramBuilder, select: &mut Select) -> Result<()> {
for join in &select.src_tables {
let loop_info = translate_table_open_cursor(program, join);
select.loops.push(loop_info);
}
let conditions = evaluate_conditions(program, select, None)?;
let processed_where = process_where(program, select)?;
for loop_info in &mut select.loops {
let mut left_join_match_flag_maybe = None;
if let Some(QueryConstraint::Left(Left {
match_flag,
right_cursor,
..
})) = conditions.as_ref()
{
if loop_info.open_cursor == *right_cursor {
left_join_match_flag_maybe = Some(*match_flag);
}
}
translate_table_open_loop(program, loop_info, left_join_match_flag_maybe);
for loop_info in &select.loops {
translate_table_open_loop(program, select, loop_info, &processed_where)?;
}
translate_conditions(program, select, conditions, None)
Ok(())
}
fn handle_skip_row(
program: &mut ProgramBuilder,
cursor_id: usize,
next_row_instruction_offset: BranchOffset,
constraint: &Option<QueryConstraint>,
) {
match constraint {
Some(QueryConstraint::Left(Left {
where_clause,
join_clause,
match_flag,
match_flag_hit_marker,
found_match_next_row_label,
left_cursor,
right_cursor,
..
})) => {
if let Some(where_clause) = where_clause {
if where_clause.no_match_target_cursor == cursor_id {
program.resolve_label(
where_clause.no_match_jump_label,
next_row_instruction_offset,
);
}
}
if let Some(join_clause) = join_clause {
if join_clause.no_match_target_cursor == cursor_id {
program.resolve_label(
join_clause.no_match_jump_label,
next_row_instruction_offset,
);
}
}
if cursor_id == *right_cursor {
// If the left join match flag has been set to 1, we jump to the next row (result row has been emitted already)
program.emit_insn_with_label_dependency(
Insn::IfPos {
reg: *match_flag,
target_pc: *found_match_next_row_label,
decrement_by: 0,
},
*found_match_next_row_label,
);
// If not, we set the right table cursor's "pseudo null bit" on, which means any Insn::Column will return NULL
program.emit_insn(Insn::NullRow {
cursor_id: *right_cursor,
});
// Jump to setting the left join match flag to 1 again, but this time the right table cursor will set everything to null
program.emit_insn_with_label_dependency(
Insn::Goto {
target_pc: *match_flag_hit_marker,
},
*match_flag_hit_marker,
);
}
if cursor_id == *left_cursor {
program.resolve_label(*found_match_next_row_label, next_row_instruction_offset);
}
}
Some(QueryConstraint::Inner(Inner {
where_clause,
join_clause,
..
})) => {
if let Some(join_clause) = join_clause {
if cursor_id == join_clause.no_match_target_cursor {
program.resolve_label(
join_clause.no_match_jump_label,
next_row_instruction_offset,
);
}
}
if let Some(where_clause) = where_clause {
if cursor_id == where_clause.no_match_target_cursor {
program.resolve_label(
where_clause.no_match_jump_label,
next_row_instruction_offset,
);
}
}
}
None => {}
}
}
fn translate_tables_end(
program: &mut ProgramBuilder,
select: &Select,
constraint: Option<QueryConstraint>,
) {
fn translate_tables_end(program: &mut ProgramBuilder, select: &Select) {
// iterate in reverse order as we open cursors in order
for table_loop in select.loops.iter().rev() {
let cursor_id = table_loop.open_cursor;
let next_row_instruction_offset = program.offset();
program.resolve_label(table_loop.next_row_label, program.offset());
program.emit_insn(Insn::NextAsync { cursor_id });
program.emit_insn(Insn::NextAwait {
cursor_id,
pc_if_next: table_loop.rewind_offset as BranchOffset,
});
program.resolve_label(table_loop.rewind_label, program.offset());
handle_skip_row(program, cursor_id, next_row_instruction_offset, &constraint);
program.emit_insn_with_label_dependency(
Insn::NextAwait {
cursor_id,
pc_if_next: table_loop.rewind_label,
},
table_loop.rewind_label,
);
if let Some(ljbk) = &table_loop.left_join_bookkeeping {
left_join_match_flag_check(program, ljbk, cursor_id);
}
}
}
fn translate_table_open_cursor(program: &mut ProgramBuilder, table: &SrcTable) -> LoopInfo {
let table_identifier = normalize_ident(match table.alias {
Some(alias) => alias,
None => &table.table.get_name(),
});
let cursor_id = program.alloc_cursor_id(Some(table_identifier), Some(table.table.clone()));
let cursor_id =
program.alloc_cursor_id(Some(table.identifier.clone()), Some(table.table.clone()));
let root_page = match &table.table {
Table::BTree(btree) => btree.root_page,
Table::Pseudo(_) => todo!(),
@ -441,37 +340,109 @@ fn translate_table_open_cursor(program: &mut ProgramBuilder, table: &SrcTable) -
});
program.emit_insn(Insn::OpenReadAwait);
LoopInfo {
identifier: table.identifier.clone(),
left_join_bookkeeping: if table.is_outer_join() {
Some(LeftJoinBookkeeping {
match_flag_register: program.alloc_register(),
on_match_jump_to_label: program.allocate_label(),
set_match_flag_true_label: program.allocate_label(),
})
} else {
None
},
open_cursor: cursor_id,
rewind_offset: 0,
rewind_label: 0,
next_row_label: program.allocate_label(),
rewind_label: program.allocate_label(),
rewind_on_empty_label: program.allocate_label(),
}
}
/**
* initialize left join match flag to false
* if condition checks pass, it will eventually be set to true
*/
fn left_join_match_flag_initialize(program: &mut ProgramBuilder, ljbk: &LeftJoinBookkeeping) {
program.emit_insn(Insn::Integer {
value: 0,
dest: ljbk.match_flag_register,
});
}
/**
* after the relevant conditional jumps have been emitted, set the left join match flag to true
*/
fn left_join_match_flag_set_true(program: &mut ProgramBuilder, ljbk: &LeftJoinBookkeeping) {
program.defer_label_resolution(ljbk.set_match_flag_true_label, program.offset() as usize);
program.emit_insn(Insn::Integer {
value: 1,
dest: ljbk.match_flag_register,
});
}
/**
* check if the left join match flag is set to true
* if it is, jump to the next row on the outer table
* if not, set the right table cursor's "pseudo null bit" on
* then jump to setting the left join match flag to true again,
* which will effectively emit all nulls for the right table.
*/
fn left_join_match_flag_check(
program: &mut ProgramBuilder,
ljbk: &LeftJoinBookkeeping,
cursor_id: usize,
) {
// If the left join match flag has been set to 1, we jump to the next row on the outer table (result row has been emitted already)
program.emit_insn_with_label_dependency(
Insn::IfPos {
reg: ljbk.match_flag_register,
target_pc: ljbk.on_match_jump_to_label,
decrement_by: 0,
},
ljbk.on_match_jump_to_label,
);
// If not, we set the right table cursor's "pseudo null bit" on, which means any Insn::Column will return NULL
program.emit_insn(Insn::NullRow { cursor_id });
// Jump to setting the left join match flag to 1 again, but this time the right table cursor will set everything to null
program.emit_insn_with_label_dependency(
Insn::Goto {
target_pc: ljbk.set_match_flag_true_label,
},
ljbk.set_match_flag_true_label,
);
// This points to the NextAsync instruction of the next table in the loop
// (i.e. the outer table, since we're iterating in reverse order)
program.resolve_label(ljbk.on_match_jump_to_label, program.offset());
}
fn translate_table_open_loop(
program: &mut ProgramBuilder,
loop_info: &mut LoopInfo,
left_join_match_flag_maybe: Option<usize>,
) {
if let Some(match_flag) = left_join_match_flag_maybe {
// Initialize left join as not matched
program.emit_insn(Insn::Integer {
value: 0,
dest: match_flag,
});
select: &Select,
loop_info: &LoopInfo,
w: &ProcessedWhereClause,
) -> Result<()> {
if let Some(ljbk) = loop_info.left_join_bookkeeping.as_ref() {
left_join_match_flag_initialize(program, ljbk);
}
program.emit_insn(Insn::RewindAsync {
cursor_id: loop_info.open_cursor,
});
let rewind_await_label = program.allocate_label();
program.defer_label_resolution(loop_info.rewind_label, program.offset() as usize);
program.emit_insn_with_label_dependency(
Insn::RewindAwait {
cursor_id: loop_info.open_cursor,
pc_if_empty: rewind_await_label,
pc_if_empty: loop_info.rewind_on_empty_label,
},
rewind_await_label,
loop_info.rewind_on_empty_label,
);
loop_info.rewind_label = rewind_await_label;
loop_info.rewind_offset = program.offset() - 1;
translate_processed_where(program, select, loop_info, w, None)?;
if let Some(ljbk) = loop_info.left_join_bookkeeping.as_ref() {
left_join_match_flag_set_true(program, ljbk);
}
Ok(())
}
fn translate_columns(
@ -539,11 +510,7 @@ fn translate_table_star(
target_register: usize,
cursor_hint: Option<usize>,
) {
let table_identifier = normalize_ident(match table.alias {
Some(alias) => alias,
None => &table.table.get_name(),
});
let table_cursor = program.resolve_cursor_id(&table_identifier, cursor_hint);
let table_cursor = program.resolve_cursor_id(&table.identifier, cursor_hint);
let table = &table.table;
for (i, col) in table.columns().iter().enumerate() {
let col_target_register = target_register + i;

View file

@ -1,11 +1,32 @@
use sqlite3_parser::ast;
use sqlite3_parser::ast::{self, JoinOperator, JoinType};
use crate::{function::Func, schema::Table, vdbe::BranchOffset};
#[derive(Debug)]
pub struct SrcTable<'a> {
pub table: Table,
pub alias: Option<&'a String>,
pub join_info: Option<&'a ast::JoinedSelectTable>, // FIXME: preferably this should be a reference with lifetime == Select ast expr
pub identifier: String,
pub join_info: Option<&'a ast::JoinedSelectTable>,
}
impl SrcTable<'_> {
pub fn is_outer_join(&self) -> bool {
matches!(
self.join_info,
Some(ast::JoinedSelectTable {
operator: JoinOperator::TypedJoin {
natural: false,
join_type: Some(
JoinType::Left
| JoinType::LeftOuter
| JoinType::Right
| JoinType::RightOuter
)
},
..
})
)
}
}
#[derive(Debug)]
@ -29,9 +50,27 @@ impl<'a> ColumnInfo<'a> {
}
}
pub struct LeftJoinBookkeeping {
// integer register that holds a flag that is set to true if the current row has a match for the left join
pub match_flag_register: usize,
// label for the instruction that sets the match flag to true
pub set_match_flag_true_label: BranchOffset,
// label for the instruction where the program jumps to if the current row has a match for the left join
pub on_match_jump_to_label: BranchOffset,
}
pub struct LoopInfo {
pub rewind_offset: BranchOffset,
// The table or table alias that we are looping over
pub identifier: String,
// Metadata about a left join, if any
pub left_join_bookkeeping: Option<LeftJoinBookkeeping>,
// The label for the instruction that reads the next row for this table
pub next_row_label: BranchOffset,
// The label for the instruction that rewinds the cursor for this table
pub rewind_label: BranchOffset,
// The label for the instruction that is jumped to in the Rewind instruction if the table is empty
pub rewind_on_empty_label: BranchOffset,
// The ID of the cursor that is opened for this table
pub open_cursor: usize,
}

View file

@ -1,51 +1,142 @@
use anyhow::Result;
use sqlite3_parser::ast::{self, JoinOperator};
use sqlite3_parser::ast::{self};
use crate::{
translate::expr::{resolve_ident_qualified, resolve_ident_table, translate_expr},
function::SingleRowFunc,
translate::expr::{resolve_ident_qualified, resolve_ident_table, translate_expr},
translate::select::Select,
vdbe::{BranchOffset, Insn, builder::ProgramBuilder},
};
const HARDCODED_CURSOR_LEFT_TABLE: usize = 0;
const HARDCODED_CURSOR_RIGHT_TABLE: usize = 1;
use super::select::LoopInfo;
#[derive(Debug)]
pub struct Where {
pub constraint_expr: ast::Expr,
pub no_match_jump_label: BranchOffset,
pub no_match_target_cursor: usize,
pub struct WhereTerm {
pub expr: ast::Expr,
pub evaluate_at_cursor: usize,
}
#[derive(Debug)]
pub struct Join {
pub constraint_expr: ast::Expr,
pub no_match_jump_label: BranchOffset,
pub no_match_target_cursor: usize,
pub struct ProcessedWhereClause {
pub terms: Vec<WhereTerm>,
}
#[derive(Debug)]
pub struct Left {
pub where_clause: Option<Where>,
pub join_clause: Option<Join>,
pub match_flag: usize,
pub match_flag_hit_marker: BranchOffset,
pub found_match_next_row_label: BranchOffset,
pub left_cursor: usize,
pub right_cursor: usize,
/**
* Split a constraint into a flat list of WhereTerms.
* The splitting is done at logical 'AND' operator boundaries.
* WhereTerms are currently just a wrapper around an ast::Expr,
* combined with the ID of the cursor where the term should be evaluated.
*/
pub fn split_constraint_to_terms<'a>(
program: &'a mut ProgramBuilder,
select: &'a Select,
where_clause_or_join_constraint: &ast::Expr,
outer_join_table_name: Option<&'a String>,
) -> Result<Vec<WhereTerm>> {
let mut terms = Vec::new();
let mut queue = vec![where_clause_or_join_constraint];
while let Some(expr) = queue.pop() {
match expr {
ast::Expr::Binary(left, ast::Operator::And, right) => {
queue.push(left);
queue.push(right);
}
expr => {
let term = WhereTerm {
expr: expr.clone(),
evaluate_at_cursor: match outer_join_table_name {
Some(table) => {
// If we had e.g. SELECT * FROM t1 LEFT JOIN t2 WHERE t1.a > 10,
// we could evaluate the t1.a > 10 condition at the cursor for t1, i.e. the outer table,
// skipping t1 rows that don't match the condition.
//
// However, if we have SELECT * FROM t1 LEFT JOIN t2 ON t1.a > 10,
// we need to evaluate the t1.a > 10 condition at the cursor for t2, i.e. the inner table,
// because we need to skip rows from t2 that don't match the condition.
//
// In inner joins, both of the above are equivalent, but in left joins they are not.
select
.loops
.iter()
.find(|t| t.identifier == *table)
.ok_or(anyhow::anyhow!(
"Could not find cursor for table {}",
table
))?
.open_cursor
}
None => {
// For any non-outer-join condition expression, find the cursor that it should be evaluated at.
// This is the cursor that is the rightmost/innermost cursor that the expression depends on.
// In SELECT * FROM t1, t2 WHERE t1.a > 10, the condition should be evaluated at the cursor for t1.
// In SELECT * FROM t1, t2 WHERE t1.a > 10 OR t2.b > 20, the condition should be evaluated at the cursor for t2.
//
// We are splitting any AND expressions in this function, so for example in this query:
// 'SELECT * FROM t1, t2 WHERE t1.a > 10 AND t2.b > 20'
// we can evaluate the t1.a > 10 condition at the cursor for t1, and the t2.b > 20 condition at the cursor for t2.
//
// For expressions that don't depend on any cursor, we can evaluate them at the leftmost/outermost cursor.
// E.g. 'SELECT * FROM t1 JOIN t2 ON false' can be evaluated at the cursor for t1.
let cursors =
introspect_expression_for_cursors(program, select, expr, None)?;
let outermost_cursor = select
.loops
.iter()
.map(|t| t.open_cursor)
.min()
.ok_or_else(|| {
anyhow::anyhow!("No open cursors found in any of the loops")
})?;
*cursors.iter().max().unwrap_or(&outermost_cursor)
}
},
};
terms.push(term);
}
}
}
Ok(terms)
}
#[derive(Debug)]
pub struct Inner {
pub where_clause: Option<Where>,
pub join_clause: Option<Join>,
}
/**
* Split the WHERE clause and any JOIN ON clauses into a flat list of WhereTerms
* that can be evaluated at the appropriate cursor.
*/
pub fn process_where<'a>(
program: &'a mut ProgramBuilder,
select: &'a Select,
) -> Result<ProcessedWhereClause> {
let mut wc = ProcessedWhereClause { terms: Vec::new() };
if let Some(w) = &select.where_clause {
wc.terms
.extend(split_constraint_to_terms(program, select, w, None)?);
}
#[derive(Debug)]
pub enum QueryConstraint {
Left(Left),
Inner(Inner),
for table in select.src_tables.iter() {
if table.join_info.is_none() {
continue;
}
let join_info = table.join_info.unwrap();
if let Some(ast::JoinConstraint::On(expr)) = &join_info.constraint {
let terms = split_constraint_to_terms(
program,
select,
expr,
if table.is_outer_join() {
Some(&table.identifier)
} else {
None
},
)?;
wc.terms.extend(terms);
}
}
Ok(wc)
}
pub fn translate_where(
@ -61,175 +152,27 @@ pub fn translate_where(
}
}
pub fn evaluate_conditions(
/**
* Translate the WHERE clause and JOIN ON clauses into a series of conditional jump instructions.
* At this point the WHERE clause and JOIN ON clauses have been split into a series of terms that can be evaluated at the appropriate cursor.
* We evaluate each term at the appropriate cursor.
*/
pub fn translate_processed_where<'a>(
program: &mut ProgramBuilder,
select: &Select,
select: &'a Select,
current_loop: &'a LoopInfo,
where_c: &'a ProcessedWhereClause,
cursor_hint: Option<usize>,
) -> Result<Option<QueryConstraint>> {
let join_constraints = select
.src_tables
.iter()
.map(|v| v.join_info)
.filter_map(|v| v.map(|v| (v.constraint.clone(), v.operator)))
.collect::<Vec<_>>();
// TODO: only supports one JOIN; -> add support for multiple JOINs, e.g. SELECT * FROM a JOIN b ON a.id = b.id JOIN c ON b.id = c.id
if join_constraints.len() > 1 {
anyhow::bail!("Parse error: multiple JOINs not supported");
) -> Result<()> {
for term in where_c.terms.iter() {
if term.evaluate_at_cursor != current_loop.open_cursor {
continue;
}
let target_jump = current_loop.next_row_label;
translate_condition_expr(program, select, &term.expr, target_jump, false, cursor_hint)?;
}
let join_maybe = join_constraints.first();
let parsed_where_maybe = select.where_clause.as_ref().map(|where_clause| Where {
constraint_expr: where_clause.clone(),
no_match_jump_label: program.allocate_label(),
no_match_target_cursor: get_no_match_target_cursor(
program,
select,
where_clause,
cursor_hint,
),
});
let parsed_join_maybe = join_maybe.and_then(|(constraint, _)| {
if let Some(ast::JoinConstraint::On(expr)) = constraint {
Some(Join {
constraint_expr: expr.clone(),
no_match_jump_label: program.allocate_label(),
no_match_target_cursor: get_no_match_target_cursor(
program,
select,
expr,
cursor_hint,
),
})
} else {
None
}
});
let constraint_maybe = match (parsed_where_maybe, parsed_join_maybe) {
(None, None) => None,
(Some(where_clause), None) => Some(QueryConstraint::Inner(Inner {
where_clause: Some(where_clause),
join_clause: None,
})),
(where_clause, Some(join_clause)) => {
let (_, op) = join_maybe.unwrap();
match op {
JoinOperator::TypedJoin { natural, join_type } => {
if *natural {
todo!("Natural join not supported");
}
// default to inner join when no join type is specified
let join_type = join_type.unwrap_or(ast::JoinType::Inner);
match join_type {
ast::JoinType::Inner | ast::JoinType::Cross => {
// cross join with a condition is an inner join
Some(QueryConstraint::Inner(Inner {
where_clause,
join_clause: Some(join_clause),
}))
}
ast::JoinType::LeftOuter | ast::JoinType::Left => {
let left_join_match_flag = program.alloc_register();
let left_join_match_flag_hit_marker = program.allocate_label();
let left_join_found_match_next_row_label = program.allocate_label();
Some(QueryConstraint::Left(Left {
where_clause,
join_clause: Some(join_clause),
found_match_next_row_label: left_join_found_match_next_row_label,
match_flag: left_join_match_flag,
match_flag_hit_marker: left_join_match_flag_hit_marker,
left_cursor: HARDCODED_CURSOR_LEFT_TABLE, // FIXME: hardcoded
right_cursor: HARDCODED_CURSOR_RIGHT_TABLE, // FIXME: hardcoded
}))
}
ast::JoinType::RightOuter | ast::JoinType::Right => {
todo!();
}
ast::JoinType::FullOuter | ast::JoinType::Full => {
todo!();
}
}
}
JoinOperator::Comma => {
todo!();
}
}
}
};
Ok(constraint_maybe)
}
pub fn translate_conditions(
program: &mut ProgramBuilder,
select: &Select,
conditions: Option<QueryConstraint>,
cursor_hint: Option<usize>,
) -> Result<Option<QueryConstraint>> {
match conditions.as_ref() {
Some(QueryConstraint::Left(Left {
where_clause,
join_clause,
match_flag,
match_flag_hit_marker,
..
})) => {
if let Some(where_clause) = where_clause {
translate_condition_expr(
program,
select,
&where_clause.constraint_expr,
where_clause.no_match_jump_label,
false,
cursor_hint,
)?;
}
if let Some(join_clause) = join_clause {
translate_condition_expr(
program,
select,
&join_clause.constraint_expr,
join_clause.no_match_jump_label,
false,
cursor_hint,
)?;
}
// Set match flag to 1 if we hit the marker (i.e. jump didn't happen to no_match_label as a result of the condition)
program.emit_insn(Insn::Integer {
value: 1,
dest: *match_flag,
});
program.defer_label_resolution(*match_flag_hit_marker, (program.offset() - 1) as usize);
}
Some(QueryConstraint::Inner(inner_join)) => {
if let Some(where_clause) = &inner_join.where_clause {
translate_condition_expr(
program,
select,
&where_clause.constraint_expr,
where_clause.no_match_jump_label,
false,
cursor_hint,
)?;
}
if let Some(join_clause) = &inner_join.join_clause {
translate_condition_expr(
program,
select,
&join_clause.constraint_expr,
join_clause.no_match_jump_label,
false,
cursor_hint,
)?;
}
}
None => {}
}
Ok(conditions)
Ok(())
}
fn translate_condition_expr(
@ -532,13 +475,7 @@ fn introspect_expression_for_cursors(
cursors.push(cursor_id);
}
ast::Expr::Literal(_) => {}
ast::Expr::Like {
lhs,
not: _,
op: _,
rhs,
escape: _,
} => {
ast::Expr::Like { lhs, rhs, .. } => {
cursors.extend(introspect_expression_for_cursors(
program,
select,
@ -552,6 +489,18 @@ fn introspect_expression_for_cursors(
cursor_hint,
)?);
}
ast::Expr::FunctionCall { args, .. } => {
if let Some(args) = args {
for arg in args {
cursors.extend(introspect_expression_for_cursors(
program,
select,
arg,
cursor_hint,
)?);
}
}
}
other => {
anyhow::bail!("Parse error: unsupported expression: {:?}", other);
}
@ -559,26 +508,3 @@ fn introspect_expression_for_cursors(
Ok(cursors)
}
fn get_no_match_target_cursor(
program: &ProgramBuilder,
select: &Select,
expr: &ast::Expr,
cursor_hint: Option<usize>,
) -> usize {
// This is the hackiest part of the code. We are finding the cursor that should be advanced to the next row
// when the condition is not met. This is done by introspecting the expression and finding the innermost cursor that is
// used in the expression. This is a very naive approach and will not work in all cases.
// Thankfully though it might be possible to just refine the logic contained here to make it work in all cases. Maybe.
let cursors =
introspect_expression_for_cursors(program, select, expr, cursor_hint).unwrap_or_default();
if cursors.is_empty() {
assert!(
select.loops.len() > 0,
"select.loops is populated based on select.src_tables. Expect at least 1 table if this function is called"
);
select.loops.first().unwrap().open_cursor
} else {
*cursors.iter().max().unwrap()
}
}

View file

@ -242,6 +242,10 @@ impl ProgramBuilder {
assert!(*target_pc < 0);
*target_pc = to_offset;
}
Insn::NextAwait { pc_if_next, .. } => {
assert!(*pc_if_next < 0);
*pc_if_next = to_offset;
}
_ => {
todo!("missing resolve_label for {:?}", insn);
}

View file

@ -686,7 +686,7 @@ impl Program {
if let Some(ref rowid) = cursor.rowid()? {
state.registers[*dest] = OwnedValue::Integer(*rowid as i64);
} else {
todo!();
state.registers[*dest] = OwnedValue::Null;
}
state.pc += 1;
}
@ -1397,7 +1397,7 @@ mod tests {
OwnedValue::Integer(value) => {
// Check that the value is within the range of i64
assert!(
value >= i64::MIN && value <= i64::MAX,
(i64::MIN..=i64::MAX).contains(&value),
"Random number out of range"
);
}

View file

@ -808,8 +808,7 @@ pub unsafe extern "C" fn sqlite3_errmsg(_db: *mut sqlite3) -> *const std::ffi::c
let err_msg = if (*_db).err_code != SQLITE_OK {
if !(*_db).p_err.is_null() {
let cstr = (*_db).p_err as *const std::ffi::c_char;
cstr
(*_db).p_err as *const std::ffi::c_char
} else {
std::ptr::null()
}

View file

@ -79,6 +79,21 @@ Alan|
Michael|
Brianna|}
do_execsql_test left-join-with-where-2 {
select users.first_name, products.name from users left join products on users.id < 2 where users.id < 3;
} {Jamie|hat
Jamie|cap
Jamie|shirt
Jamie|sweater
Jamie|sweatshirt
Jamie|shorts
Jamie|jeans
Jamie|sneakers
Jamie|boots
Jamie|coat
Jamie|accessories
Cindy|}
do_execsql_test left-join-non-pk {
select users.first_name as user_name, products.name as product_name from users left join products on users.first_name = products.name limit 3;
} {Jamie|
@ -107,4 +122,27 @@ Cindy|cap}
do_execsql_test left-join-no-join-conditions-but-multiple-where {
select u.first_name, p.name from users u left join products as p where u.id = p.id or u.first_name = p.name limit 2;
} {Jamie|hat
Cindy|cap}
Cindy|cap}
do_execsql_test four-way-inner-join {
select u1.first_name, u2.first_name, u3.first_name, u4.first_name from users u1 join users u2 on u1.id = u2.id join users u3 on u2.id = u3.id + 1 join users u4 on u3.id = u4.id + 1 limit 1;
} {Tommy|Tommy|Cindy|Jamie}
do_execsql_test leftjoin-innerjoin-where {
select u.first_name, p.name, p2.name from users u left join products p on p.name = u.first_name join products p2 on length(p2.name) > 8 where u.first_name = 'Franklin';
} {Franklin||sweatshirt
Franklin||accessories}
do_execsql_test leftjoin-leftjoin-where {
select u.first_name, p.name, p2.name from users u left join products p on p.name = u.first_name join products p2 on length(p2.name) > 8 where u.first_name = 'Franklin';
} {Franklin||sweatshirt
Franklin||accessories}
do_execsql_test innerjoin-leftjoin-where {
select u.first_name, u2.first_name, p.name from users u join users u2 on u.id = u2.id + 1 left join products p on p.name = u.first_name where u.first_name = 'Franklin';
} {Franklin|Cynthia|}
do_execsql_test innerjoin-leftjoin-with-or-terms {
select u.first_name, u2.first_name, p.name from users u join users u2 on u.id = u2.id + 1 left join products p on p.name = u.first_name or p.name like 'sweat%' where u.first_name = 'Franklin';
} {Franklin|Cynthia|sweater
Franklin|Cynthia|sweatshirt}