Merge pull request #143 from jussisaurio/inner-join

Inner join, table aliases, qualified column names
This commit is contained in:
Pere Diaz Bou
2024-07-14 19:25:30 +02:00
committed by GitHub
4 changed files with 383 additions and 98 deletions

View File

@@ -33,7 +33,7 @@ impl Schema {
}
}
#[derive(Clone)]
#[derive(Clone, Debug)]
pub enum Table {
BTree(Rc<BTreeTable>),
Pseudo(Rc<PseudoTable>),
@@ -96,6 +96,7 @@ impl PartialEq for Table {
}
}
#[derive(Debug)]
pub struct BTreeTable {
pub root_page: usize,
pub name: String,
@@ -150,6 +151,7 @@ impl BTreeTable {
}
}
#[derive(Debug)]
pub struct PseudoTable {
pub columns: Vec<Column>,
}
@@ -295,6 +297,7 @@ pub fn _build_pseudo_table(columns: &[ResultColumn]) -> PseudoTable {
table
}
#[derive(Debug)]
pub struct Column {
pub name: String,
pub ty: Type,

View File

@@ -37,7 +37,8 @@ struct LoopInfo {
struct SrcTable {
table: Table,
_join_info: Option<ast::JoinedSelectTable>, // FIXME: preferably this should be a reference with lifetime == Select ast expr
alias: Option<String>,
join_info: Option<ast::JoinedSelectTable>, // FIXME: preferably this should be a reference with lifetime == Select ast expr
}
struct ColumnInfo {
@@ -91,14 +92,21 @@ fn build_select(schema: &Schema, select: ast::Select) -> Result<Select> {
where_clause,
..
} => {
let table_name = match from.select {
let (table_name, maybe_alias) = match from.select {
Some(select_table) => match *select_table {
ast::SelectTable::Table(name, ..) => name.name,
ast::SelectTable::Table(name, alias, ..) => (
name.name,
alias.map(|als| match als {
ast::As::As(alias) => alias, // users as u
ast::As::Elided(alias) => alias, // users u
}),
),
_ => todo!(),
},
None => todo!(),
};
let table_name = table_name.0;
let maybe_alias = maybe_alias.map(|als| als.0);
let table = match schema.get_table(&table_name) {
Some(table) => table,
None => anyhow::bail!("Parse error: no such table: {}", table_name),
@@ -106,22 +114,31 @@ fn build_select(schema: &Schema, select: ast::Select) -> Result<Select> {
let mut joins = Vec::new();
joins.push(SrcTable {
table: Table::BTree(table.clone()),
_join_info: None,
alias: maybe_alias,
join_info: None,
});
if let Some(selected_joins) = from.joins {
for join in selected_joins {
let table_name = match &join.table {
ast::SelectTable::Table(name, ..) => name.name.clone(),
let (table_name, maybe_alias) = match &join.table {
ast::SelectTable::Table(name, alias, ..) => (
name.name.clone(),
alias.clone().map(|als| match als {
ast::As::As(alias) => alias, // users as u
ast::As::Elided(alias) => alias, // users u
}),
),
_ => todo!(),
};
let table_name = &table_name.0;
let maybe_alias = maybe_alias.map(|als| als.0);
let table = match schema.get_table(table_name) {
Some(table) => table,
None => anyhow::bail!("Parse error: no such table: {}", table_name),
};
joins.push(SrcTable {
table: Table::BTree(table),
_join_info: Some(join.clone()),
alias: maybe_alias,
join_info: Some(join.clone()),
});
}
}
@@ -205,9 +222,7 @@ fn translate_select(mut select: Select) -> Result<Program> {
};
if !select.src_tables.is_empty() {
translate_tables_begin(&mut program, &mut select);
let where_maybe = insert_where_clause_instructions(&select, &mut program)?;
let condition_label_maybe = translate_tables_begin(&mut program, &mut select)?;
let (register_start, register_end) = translate_columns(&mut program, &select)?;
@@ -219,8 +234,8 @@ fn translate_select(mut select: Select) -> Result<Program> {
emit_limit_insn(&limit_info, &mut program);
}
if let Some(where_clause_label) = where_maybe {
program.resolve_label(where_clause_label, program.offset());
if let Some(condition_label) = condition_label_maybe {
program.resolve_label(condition_label, program.offset());
}
translate_tables_end(&mut program, &select);
@@ -245,7 +260,7 @@ fn translate_select(mut select: Select) -> Result<Program> {
}
} else {
assert!(!select.exist_aggregation);
let where_maybe = insert_where_clause_instructions(&select, &mut program)?;
let where_maybe = translate_where(&select, &mut program)?;
let (register_start, register_end) = translate_columns(&mut program, &select)?;
if let Some(where_clause_label) = where_maybe {
program.resolve_label(where_clause_label, program.offset() + 1);
@@ -288,10 +303,7 @@ fn emit_limit_insn(limit_info: &Option<LimitInfo>, program: &mut ProgramBuilder)
}
}
fn insert_where_clause_instructions(
select: &Select,
program: &mut ProgramBuilder,
) -> Result<Option<BranchOffset>> {
fn translate_where(select: &Select, program: &mut ProgramBuilder) -> Result<Option<BranchOffset>> {
if let Some(w) = &select.where_clause {
let label = program.allocate_label();
translate_condition_expr(program, select, w, label)?;
@@ -301,16 +313,76 @@ fn insert_where_clause_instructions(
}
}
fn translate_tables_begin(program: &mut ProgramBuilder, select: &mut Select) {
fn translate_conditions(
program: &mut ProgramBuilder,
select: &Select,
) -> Result<Option<BranchOffset>> {
// FIXME: clone()
// TODO: only supports INNER JOIN on a single condition atm, e.g. SELECT * FROM a JOIN b ON a.id = b.id, no AND/OR
let join_constraints = select
.src_tables
.iter()
.map(|v| v.join_info.clone())
.filter_map(|v| v.map(|v| v.constraint))
.flatten()
.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");
}
let maybe_join = join_constraints.first();
match (&select.where_clause, maybe_join) {
(Some(where_clause), Some(join)) => {
match join {
ast::JoinConstraint::On(expr) => {
// Combine where clause and join condition
let label = program.allocate_label();
translate_condition_expr(program, select, where_clause, label)?;
translate_condition_expr(program, select, expr, label)?;
Ok(Some(label))
}
ast::JoinConstraint::Using(_) => {
todo!();
}
}
}
(None, None) => {
Ok(None)
}
(Some(where_clause), None) => {
let label = program.allocate_label();
translate_condition_expr(program, select, where_clause, label)?;
Ok(Some(label))
}
(None, Some(join)) => match join {
ast::JoinConstraint::On(expr) => {
let label = program.allocate_label();
translate_condition_expr(program, select, expr, label)?;
Ok(Some(label))
}
ast::JoinConstraint::Using(_) => {
todo!();
}
},
}
}
fn translate_tables_begin(
program: &mut ProgramBuilder,
select: &mut Select,
) -> Result<Option<BranchOffset>> {
for join in &select.src_tables {
let table = &join.table;
let loop_info = translate_table_open_cursor(program, table);
let loop_info = translate_table_open_cursor(program, join);
select.loops.push(loop_info);
}
for loop_info in &mut select.loops {
translate_table_open_loop(program, loop_info);
}
translate_conditions(program, select)
}
fn translate_tables_end(program: &mut ProgramBuilder, select: &Select) {
@@ -326,9 +398,13 @@ fn translate_tables_end(program: &mut ProgramBuilder, select: &Select) {
}
}
fn translate_table_open_cursor(program: &mut ProgramBuilder, table: &Table) -> LoopInfo {
let cursor_id = program.alloc_cursor_id(table.clone());
let root_page = match table {
fn translate_table_open_cursor(program: &mut ProgramBuilder, table: &SrcTable) -> LoopInfo {
let table_identifier = match &table.alias {
Some(alias) => alias.clone(),
None => table.table.get_name().to_string(),
};
let cursor_id = program.alloc_cursor_id(table_identifier, table.table.clone());
let root_page = match &table.table {
Table::BTree(btree) => btree.root_page,
Table::Pseudo(_) => todo!(),
};
@@ -398,9 +474,8 @@ fn translate_column(
ast::ResultColumn::Star => {
let mut target_register = target_register;
for join in &select.src_tables {
let table = &join.table;
translate_table_star(table, program, target_register);
target_register += table.columns().len();
translate_table_star(join, program, target_register);
target_register += &join.table.columns().len();
}
}
ast::ResultColumn::TableStar(_) => todo!(),
@@ -408,8 +483,13 @@ fn translate_column(
Ok(())
}
fn translate_table_star(table: &Table, program: &mut ProgramBuilder, target_register: usize) {
let table_cursor = program.resolve_cursor_id(table);
fn translate_table_star(table: &SrcTable, program: &mut ProgramBuilder, target_register: usize) {
let table_identifier = match &table.alias {
Some(alias) => alias.clone(),
None => table.table.get_name().to_string(),
};
let table_cursor = program.resolve_cursor_id(&table_identifier);
let table = &table.table;
for (i, col) in table.columns().iter().enumerate() {
let col_target_register = target_register + i;
if table.column_is_rowid_alias(col) {
@@ -541,7 +621,7 @@ fn translate_condition_expr(
rhs: e2_reg,
target_pc: jump_target,
},
_ => todo!(),
other => todo!("{:?}", other),
});
Ok(())
}
@@ -572,6 +652,24 @@ fn translate_condition_expr(
}
}
fn wrap_eval_jump_expr(
program: &mut ProgramBuilder,
insn: Insn,
target_register: usize,
if_true_label: BranchOffset,
) {
program.emit_insn(Insn::Integer {
value: 1, // emit True by default
dest: target_register,
});
program.emit_insn_with_label_dependency(insn, if_true_label);
program.emit_insn(Insn::Integer {
value: 0, // emit False if we reach this point (no jump)
dest: target_register,
});
program.preassign_label_to_next_insn(if_true_label);
}
fn translate_expr(
program: &mut ProgramBuilder,
select: &Select,
@@ -585,50 +683,95 @@ fn translate_expr(
let e2_reg = program.alloc_register();
let _ = translate_expr(program, select, e1, e1_reg)?;
let _ = translate_expr(program, select, e2, e2_reg)?;
program.emit_insn(match op {
ast::Operator::NotEquals => Insn::Ne {
lhs: e1_reg,
rhs: e2_reg,
target_pc: program.offset() + 3, // jump to "emit True" instruction
},
ast::Operator::Equals => Insn::Eq {
lhs: e1_reg,
rhs: e2_reg,
target_pc: program.offset() + 3,
},
ast::Operator::Less => Insn::Lt {
lhs: e1_reg,
rhs: e2_reg,
target_pc: program.offset() + 3,
},
ast::Operator::LessEquals => Insn::Le {
lhs: e1_reg,
rhs: e2_reg,
target_pc: program.offset() + 3,
},
ast::Operator::Greater => Insn::Gt {
lhs: e1_reg,
rhs: e2_reg,
target_pc: program.offset() + 3,
},
ast::Operator::GreaterEquals => Insn::Ge {
lhs: e1_reg,
rhs: e2_reg,
target_pc: program.offset() + 3,
},
_ => todo!(),
});
program.emit_insn(Insn::Integer {
value: 0, // emit False
dest: target_register,
});
program.emit_insn(Insn::Goto {
target_pc: program.offset() + 2,
});
program.emit_insn(Insn::Integer {
value: 1, // emit True
dest: target_register,
});
match op {
ast::Operator::NotEquals => {
let if_true_label = program.allocate_label();
wrap_eval_jump_expr(
program,
Insn::Ne {
lhs: e1_reg,
rhs: e2_reg,
target_pc: if_true_label,
},
target_register,
if_true_label,
);
}
ast::Operator::Equals => {
let if_true_label = program.allocate_label();
wrap_eval_jump_expr(
program,
Insn::Eq {
lhs: e1_reg,
rhs: e2_reg,
target_pc: if_true_label,
},
target_register,
if_true_label,
);
}
ast::Operator::Less => {
let if_true_label = program.allocate_label();
wrap_eval_jump_expr(
program,
Insn::Lt {
lhs: e1_reg,
rhs: e2_reg,
target_pc: if_true_label,
},
target_register,
if_true_label,
);
}
ast::Operator::LessEquals => {
let if_true_label = program.allocate_label();
wrap_eval_jump_expr(
program,
Insn::Le {
lhs: e1_reg,
rhs: e2_reg,
target_pc: if_true_label,
},
target_register,
if_true_label,
);
}
ast::Operator::Greater => {
let if_true_label = program.allocate_label();
wrap_eval_jump_expr(
program,
Insn::Gt {
lhs: e1_reg,
rhs: e2_reg,
target_pc: if_true_label,
},
target_register,
if_true_label,
);
}
ast::Operator::GreaterEquals => {
let if_true_label = program.allocate_label();
wrap_eval_jump_expr(
program,
Insn::Ge {
lhs: e1_reg,
rhs: e2_reg,
target_pc: if_true_label,
},
target_register,
if_true_label,
);
}
ast::Operator::Add => {
program.emit_insn(Insn::Add {
lhs: e1_reg,
rhs: e2_reg,
dest: target_register,
});
}
other_unimplemented => todo!("{:?}", other_unimplemented),
}
Ok(target_register)
}
ast::Expr::Case { .. } => todo!(),
@@ -753,7 +896,23 @@ fn translate_expr(
ast::Expr::Name(_) => todo!(),
ast::Expr::NotNull(_) => todo!(),
ast::Expr::Parenthesized(_) => todo!(),
ast::Expr::Qualified(_, _) => todo!(),
ast::Expr::Qualified(tbl, ident) => {
let (idx, col, cursor_id) = resolve_ident_qualified(program, &tbl.0, &ident.0, select)?;
if col.primary_key {
program.emit_insn(Insn::RowId {
cursor_id,
dest: target_register,
});
} else {
program.emit_insn(Insn::Column {
column: idx,
dest: target_register,
cursor_id,
});
}
maybe_apply_affinity(col, target_register, program);
Ok(target_register)
}
ast::Expr::Raise(_, _) => todo!(),
ast::Expr::Subquery(_) => todo!(),
ast::Expr::Unary(_, _) => todo!(),
@@ -761,25 +920,77 @@ fn translate_expr(
}
}
fn resolve_ident_qualified<'a>(
program: &ProgramBuilder,
table_name: &String,
ident: &String,
select: &'a Select,
) -> Result<(usize, &'a Column, usize)> {
for join in &select.src_tables {
match join.table {
Table::BTree(ref table) => {
let table_identifier = match &join.alias {
Some(alias) => alias.clone(),
None => table.name.to_string(),
};
if table_identifier == *table_name {
let res = table
.columns
.iter()
.enumerate()
.find(|(_, col)| col.name == *ident);
if res.is_some() {
let (idx, col) = res.unwrap();
let cursor_id = program.resolve_cursor_id(&table_identifier);
return Ok((idx, col, cursor_id));
}
}
}
Table::Pseudo(_) => todo!(),
}
}
anyhow::bail!(
"Parse error: column with qualified name {}.{} not found",
table_name,
ident
);
}
fn resolve_ident_table<'a>(
program: &ProgramBuilder,
ident: &String,
select: &'a Select,
) -> Result<(usize, &'a Column, usize)> {
let mut found = Vec::new();
for join in &select.src_tables {
let res = join
.table
.columns()
.iter()
.enumerate()
.find(|(_, col)| col.name == *ident);
if res.is_some() {
let (idx, col) = res.unwrap();
let cursor_id = program.resolve_cursor_id(&join.table);
return Ok((idx, col, cursor_id));
match join.table {
Table::BTree(ref table) => {
let table_identifier = match &join.alias {
Some(alias) => alias.clone(),
None => table.name.to_string(),
};
let res = table
.columns
.iter()
.enumerate()
.find(|(_, col)| col.name == *ident);
if res.is_some() {
let (idx, col) = res.unwrap();
let cursor_id = program.resolve_cursor_id(&table_identifier);
found.push((idx, col, cursor_id));
}
}
Table::Pseudo(_) => todo!(),
}
}
anyhow::bail!("Parse error: column with name {} not found", ident.as_str());
if found.len() == 1 {
return Ok(found[0]);
}
if found.is_empty() {
anyhow::bail!("Parse error: column with name {} not found", ident.as_str());
}
anyhow::bail!("Parse error: ambiguous column name {}", ident.as_str());
}
fn translate_aggregation(

View File

@@ -26,6 +26,12 @@ pub enum Insn {
Null {
dest: usize,
},
// Add two registers and store the result in a third register.
Add {
lhs: usize,
rhs: usize,
dest: usize,
},
// If the given register is not NULL, jump to the given PC.
NotNull {
reg: usize,
@@ -232,7 +238,7 @@ pub struct ProgramBuilder {
unresolved_labels: Vec<Vec<InsnReference>>,
next_insn_label: Option<BranchOffset>,
// Cursors that are referenced by the program. Indexed by CursorID.
cursor_ref: Vec<Table>,
cursor_ref: Vec<(String, Table)>,
}
impl ProgramBuilder {
@@ -265,13 +271,10 @@ impl ProgramBuilder {
self.next_free_register
}
pub fn alloc_cursor_id(&mut self, table: Table) -> usize {
pub fn alloc_cursor_id(&mut self, table_identifier: String, table: Table) -> usize {
let cursor = self.next_free_cursor_id;
self.next_free_cursor_id += 1;
if self.cursor_ref.iter().any(|t| *t == table) {
todo!("duplicate table is unhandled. see how resolve_ident_table() calls resolve_cursor_id")
}
self.cursor_ref.push(table);
self.cursor_ref.push((table_identifier, table));
assert!(self.cursor_ref.len() == self.next_free_cursor_id);
cursor
}
@@ -293,7 +296,7 @@ impl ProgramBuilder {
}
pub fn emit_constant_insns(&mut self) {
self.insns.extend(self.constant_insns.drain(..));
self.insns.append(&mut self.constant_insns);
}
pub fn emit_insn_with_label_dependency(&mut self, insn: Insn, label: BranchOffset) {
@@ -449,8 +452,11 @@ impl ProgramBuilder {
}
// translate table to cursor id
pub fn resolve_cursor_id(&self, table: &Table) -> CursorID {
self.cursor_ref.iter().position(|t| t == table).unwrap()
pub fn resolve_cursor_id(&self, table_identifier: &str) -> CursorID {
self.cursor_ref
.iter()
.position(|(t_ident, _)| *t_ident == table_identifier)
.unwrap()
}
pub fn build(self) -> Program {
@@ -503,7 +509,7 @@ impl ProgramState {
pub struct Program {
pub max_registers: usize,
pub insns: Vec<Insn>,
pub cursor_ref: Vec<Table>,
pub cursor_ref: Vec<(String, Table)>,
}
impl Program {
@@ -539,6 +545,23 @@ impl Program {
assert!(*target_pc >= 0);
state.pc = *target_pc;
}
Insn::Add { lhs, rhs, dest } => {
let lhs = *lhs;
let rhs = *rhs;
let dest = *dest;
match (&state.registers[lhs], &state.registers[rhs]) {
(OwnedValue::Integer(lhs), OwnedValue::Integer(rhs)) => {
state.registers[dest] = OwnedValue::Integer(lhs + rhs);
}
(OwnedValue::Float(lhs), OwnedValue::Float(rhs)) => {
state.registers[dest] = OwnedValue::Float(lhs + rhs);
}
_ => {
todo!();
}
}
state.pc += 1;
}
Insn::Null { dest } => {
state.registers[*dest] = OwnedValue::Null;
state.pc += 1;
@@ -1212,6 +1235,15 @@ fn insn_to_str(program: &Program, addr: InsnReference, insn: &Insn, indent: Stri
0,
format!("Start at {}", target_pc),
),
Insn::Add { lhs, rhs, dest } => (
"Add",
*lhs as i32,
*rhs as i32,
*dest as i32,
OwnedValue::Text(Rc::new("".to_string())),
0,
format!("r[{}]=r[{}]+r[{}]", dest, lhs, rhs),
),
Insn::Null { dest } => (
"Null",
*dest as i32,
@@ -1377,7 +1409,7 @@ fn insn_to_str(program: &Program, addr: InsnReference, insn: &Insn, indent: Stri
column,
dest,
} => {
let table = &program.cursor_ref[*cursor_id];
let (_, table) = &program.cursor_ref[*cursor_id];
(
"Column",
*cursor_id as i32,
@@ -1513,7 +1545,7 @@ fn insn_to_str(program: &Program, addr: InsnReference, insn: &Insn, indent: Stri
format!(
"r[{}]={}.rowid",
dest,
&program.cursor_ref[*cursor_id].get_name()
&program.cursor_ref[*cursor_id].1.get_name()
),
),
Insn::DecrJumpZero { reg, target_pc } => (

View File

@@ -174,3 +174,42 @@ do_execsql_test coalesce-from-table-column {
do_execsql_test coalesce-from-table-multiple-columns {
select coalesce(NULL, age), coalesce(NULL, id) from users where age = 94 limit 1;
} {94|1}
do_execsql_test inner-join-pk {
select users.first_name as user_name, products.name as product_name from users join products on users.id = products.id;
} {Jamie|hat
Cindy|cap
Tommy|shirt
Jennifer|sweater
Edward|sweatshirt
Nicholas|shorts
Aimee|jeans
Rachel|sneakers
Matthew|boots
Daniel|coat
Travis|accessories}
do_execsql_test inner-join-non-pk-unqualified {
select first_name, name from users join products on first_name != name limit 1;
} {Jamie|hat}
do_execsql_test inner-join-non-pk-qualified {
select users.first_name as user_name, products.name as product_name from users join products on users.first_name = products.name;
} {}
do_execsql_test inner-join-self {
select u1.first_name as user_name, u2.first_name as neighbor_name from users u1 join users as u2 on u1.id = u2.id + 1 limit 1;
} {Cindy|Jamie}
do_execsql_test inner-join-self-with-where {
select u1.first_name as user_name, u2.first_name as neighbor_name from users u1 join users as u2 on u1.id = u2.id + 1 where u1.id = 5 limit 1;
} {Edward|Jennifer}
do_execsql_test inner-join-with-where-2 {
select u.first_name from users u join products as p on u.first_name != p.name where u.last_name = 'Williams' limit 1;
} {Laura}
do_execsql_test select-add {
select u.age + 1 from users u where u.age = 91 limit 1;
} {92}