diff --git a/crates/execution/src/iter.rs b/crates/execution/src/iter.rs index 0dee161ca5e..e630d6624ee 100644 --- a/crates/execution/src/iter.rs +++ b/crates/execution/src/iter.rs @@ -792,6 +792,8 @@ pub struct HashJoinIter<'a> { rhs_ptr: usize, /// The lhs probe field lhs_field: &'a TupleField, + /// Is the join outer + outer: bool, } impl<'a> HashJoinIter<'a> { @@ -820,6 +822,7 @@ impl<'a> HashJoinIter<'a> { lhs_tuple: None, rhs_ptr: 0, lhs_field: &join.lhs_field, + outer: join.outer, }) } } @@ -839,11 +842,15 @@ impl<'a> Iterator for HashJoinIter<'a> { }) .or_else(|| { self.lhs.find_map(|tuple| { - self.rhs.get(&tuple.project(self.lhs_field)).and_then(|ptrs| { + if let Some(ptrs) = self.rhs.get(&tuple.project(self.lhs_field)) { self.rhs_ptr = 1; self.lhs_tuple = Some(tuple.clone()); ptrs.first().map(|ptr| (tuple, ptr.clone())) - }) + } else { + if self.outer { + Some((tuple, Row::Null)) + } else { None } + } }) }) } diff --git a/crates/execution/src/lib.rs b/crates/execution/src/lib.rs index 8fb8e50e59b..282333c0e4e 100644 --- a/crates/execution/src/lib.rs +++ b/crates/execution/src/lib.rs @@ -117,6 +117,7 @@ pub trait DeltaStore { #[derive(Clone)] pub enum Row<'a> { + Null, Ptr(RowRef<'a>), Ref(&'a ProductValue), } @@ -128,6 +129,7 @@ impl PartialEq for Row<'_> { (Self::Ref(x), Self::Ref(y)) => x == y, (Self::Ptr(x), Self::Ref(y)) => x == *y, (Self::Ref(x), Self::Ptr(y)) => y == *x, + (Self::Null, _) | (_, Self::Null) => false, } } } @@ -137,6 +139,7 @@ impl Eq for Row<'_> {} impl Hash for Row<'_> { fn hash(&self, state: &mut H) { match self { + Self::Null => AlgebraicValue::unit().hash(state), Self::Ptr(x) => x.hash(state), Self::Ref(x) => x.hash(state), } @@ -146,6 +149,7 @@ impl Hash for Row<'_> { impl Row<'_> { pub fn to_product_value(&self) -> ProductValue { match self { + Self::Null => ProductValue { elements: Box::new([]) }, Self::Ptr(ptr) => ptr.to_product_value(), Self::Ref(val) => (*val).clone(), } @@ -153,6 +157,7 @@ impl Row<'_> { } impl_serialize!(['a] Row<'a>, (self, ser) => match self { + Self::Null => AlgebraicValue::unit().serialize(ser), Self::Ptr(row) => row.serialize(ser), Self::Ref(row) => row.serialize(ser), }); @@ -160,6 +165,7 @@ impl_serialize!(['a] Row<'a>, (self, ser) => match self { impl ToBsatn for Row<'_> { fn static_bsatn_size(&self) -> Option { match self { + Self::Null => self.to_product_value().static_bsatn_size(), Self::Ptr(ptr) => ptr.static_bsatn_size(), Self::Ref(val) => val.static_bsatn_size(), } @@ -167,6 +173,7 @@ impl ToBsatn for Row<'_> { fn to_bsatn_extend(&self, buf: &mut Vec) -> std::result::Result<(), EncodeError> { match self { + Self::Null => self.to_product_value().to_bsatn_extend(buf), Self::Ptr(ptr) => ptr.to_bsatn_extend(buf), Self::Ref(val) => val.to_bsatn_extend(buf), } @@ -174,6 +181,7 @@ impl ToBsatn for Row<'_> { fn to_bsatn_vec(&self) -> std::result::Result, EncodeError> { match self { + Self::Null => self.to_product_value().to_bsatn_vec(), Self::Ptr(ptr) => ptr.to_bsatn_vec(), Self::Ref(val) => val.to_bsatn_vec(), } @@ -183,6 +191,7 @@ impl ToBsatn for Row<'_> { impl ProjectField for Row<'_> { fn project(&self, field: &TupleField) -> AlgebraicValue { match self { + Self::Null => AlgebraicValue::unit(), Self::Ptr(ptr) => ptr.project(field), Self::Ref(val) => val.project(field), } @@ -208,7 +217,7 @@ impl ProjectField for Tuple<'_> { .label_pos .and_then(|i| ptrs.get(i)) .map(|ptr| ptr.project(field)) - .unwrap(), + .unwrap_or(AlgebraicValue::unit()), } } } diff --git a/crates/execution/src/pipelined.rs b/crates/execution/src/pipelined.rs index 1e7ea064039..4ff82a95ed3 100644 --- a/crates/execution/src/pipelined.rs +++ b/crates/execution/src/pipelined.rs @@ -257,6 +257,7 @@ impl From for PipelinedExecutor { lhs_field, rhs_field, unique, + outer, }, semijoin, ) => Self::HashJoin(BlockingHashJoin { @@ -265,6 +266,7 @@ impl From for PipelinedExecutor { lhs_field, rhs_field, unique, + outer, semijoin, }), PhysicalPlan::NLJoin(lhs, rhs) => Self::NLJoin(BlockingNLJoin { @@ -1088,6 +1090,7 @@ pub struct BlockingHashJoin { pub lhs_field: TupleField, pub rhs_field: TupleField, pub unique: bool, + pub outer: bool, pub semijoin: Semi, } @@ -1106,12 +1109,18 @@ impl BlockingHashJoin { let mut n = 0; let mut bytes_scanned = 0; match self { + Self { + outer: true, + semijoin: Semi::Lhs | Semi::Rhs, + .. + } => unreachable!("Outer semijoin is not possible"), Self { lhs, rhs, lhs_field, rhs_field, unique: true, + outer: false, semijoin: Semi::Lhs, } => { let mut rhs_table = HashSet::new(); @@ -1137,6 +1146,7 @@ impl BlockingHashJoin { lhs_field, rhs_field, unique: true, + outer: false, semijoin: Semi::Rhs, } => { let mut rhs_table = HashMap::new(); @@ -1162,6 +1172,7 @@ impl BlockingHashJoin { lhs_field, rhs_field, unique: true, + outer, semijoin: Semi::All, } => { let mut rhs_table = HashMap::new(); @@ -1177,6 +1188,8 @@ impl BlockingHashJoin { n += 1; if let Some(v) = rhs_table.get(&project(&u, lhs_field, &mut bytes_scanned)) { f(u.clone().join(v.clone()))?; + } else if *outer { + f(u.clone().append(Row::Null))?; } Ok(()) })?; @@ -1187,6 +1200,7 @@ impl BlockingHashJoin { lhs_field, rhs_field, unique: false, + outer: false, semijoin: Semi::Lhs, } => { let mut rhs_table = HashMap::new(); @@ -1214,6 +1228,7 @@ impl BlockingHashJoin { lhs_field, rhs_field, unique: false, + outer: false, semijoin: Semi::Rhs, } => { let mut rhs_table: HashMap> = HashMap::new(); @@ -1243,6 +1258,7 @@ impl BlockingHashJoin { lhs_field, rhs_field, unique: false, + outer, semijoin: Semi::All, } => { let mut rhs_table: HashMap> = HashMap::new(); @@ -1262,6 +1278,8 @@ impl BlockingHashJoin { for v in rhs_tuples { f(u.clone().join(v.clone()))?; } + } else if *outer { + f(u.clone().append(Row::Null))?; } Ok(()) })?; diff --git a/crates/expr/src/check.rs b/crates/expr/src/check.rs index 14aaa34a9ed..8a189b9bc3a 100644 --- a/crates/expr/src/check.rs +++ b/crates/expr/src/check.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use std::ops::{Deref, DerefMut}; use std::sync::Arc; +use crate::ast::{CrossJoin, InnerJoin, OuterJoin}; use crate::expr::LeftDeepJoin; use crate::expr::{Expr, ProjectList, ProjectName, Relvar}; use spacetimedb_lib::identity::AuthCtx; @@ -78,34 +79,56 @@ pub trait TypeChecker { delta: None, }); - for SqlJoin { - var: SqlIdent(name), - alias: SqlIdent(alias), - on, - } in joins - { + for jn in joins { // Check for duplicate aliases - if vars.contains_key(&alias) { - return Err(DuplicateName(alias.into_string()).into()); + match jn { + SqlJoin::Cross(CrossJoin { alias: SqlIdent(alias), .. }) + | SqlJoin::Inner(InnerJoin { alias: SqlIdent(alias), .. }) + | SqlJoin::Left(OuterJoin { alias: SqlIdent(alias), .. }) + if vars.contains_key(&alias) => { + return Err(DuplicateName(alias.into_string()).into()); + } + SqlJoin::Cross(_) => (), + SqlJoin::Inner(_) => (), + SqlJoin::Left(_) => (), } let lhs = Box::new(join); - let rhs = Relvar { - schema: Self::type_relvar(tx, &name)?, - alias, - delta: None, + let rhs = match &jn { + SqlJoin::Cross(CrossJoin { var: SqlIdent(name), alias: SqlIdent(alias), .. }) + | SqlJoin::Inner(InnerJoin { var: SqlIdent(name), alias: SqlIdent(alias), .. }) + | SqlJoin::Left(OuterJoin { var: SqlIdent(name), alias: SqlIdent(alias), .. }) => { + Relvar { + schema: Self::type_relvar(tx, &name)?, + alias: alias.clone(), + delta: None, + } + } }; vars.insert(rhs.alias.clone(), rhs.schema.clone()); - if let Some(on) = on { - if let Expr::BinOp(BinOp::Eq, a, b) = type_expr(vars, on, Some(&AlgebraicType::Bool))? { - if let (Expr::Field(a), Expr::Field(b)) = (*a, *b) { - join = RelExpr::EqJoin(LeftDeepJoin { lhs, rhs }, a, b); - continue; + match jn { + SqlJoin::Cross(_) => (), + SqlJoin::Inner(InnerJoin { on: Some(on), .. }) => { + if let Expr::BinOp(BinOp::Eq, a, b) = type_expr(vars, on, Some(&AlgebraicType::Bool))? { + if let (Expr::Field(a), Expr::Field(b)) = (*a, *b) { + join = RelExpr::InnerEqJoin(LeftDeepJoin { lhs, rhs }, a, b); + continue; + } + } + unreachable!("Unreachability guaranteed by parser") + } + SqlJoin::Inner(_) => (), + SqlJoin::Left(OuterJoin { on, .. }) => { + if let Expr::BinOp(BinOp::Eq, a, b) = type_expr(vars, on, Some(&AlgebraicType::Bool))? { + if let (Expr::Field(a), Expr::Field(b)) = (*a, *b) { + join = RelExpr::LeftOuterEqJoin(LeftDeepJoin { lhs, rhs }, a, b); + continue; + } } + unreachable!("Unreachability guaranteed by parser") } - unreachable!("Unreachability guaranteed by parser") } join = RelExpr::LeftDeepJoin(LeftDeepJoin { lhs, rhs }); diff --git a/crates/expr/src/expr.rs b/crates/expr/src/expr.rs index 4f4a68592a0..e55910fed2b 100644 --- a/crates/expr/src/expr.rs +++ b/crates/expr/src/expr.rs @@ -197,8 +197,10 @@ pub enum RelExpr { Select(Box, Expr), /// A left deep binary cross product LeftDeepJoin(LeftDeepJoin), - /// A left deep binary equi-join - EqJoin(LeftDeepJoin, FieldProject, FieldProject), + /// A left deep binary inner equi-join + InnerEqJoin(LeftDeepJoin, FieldProject, FieldProject), + /// A left deep binary left outer equi-join + LeftOuterEqJoin(LeftDeepJoin, FieldProject, FieldProject), } /// A table reference @@ -219,7 +221,8 @@ impl RelExpr { match self { Self::Select(lhs, _) | Self::LeftDeepJoin(LeftDeepJoin { lhs, .. }) - | Self::EqJoin(LeftDeepJoin { lhs, .. }, ..) => { + | Self::InnerEqJoin(LeftDeepJoin { lhs, .. }, ..) + | Self::LeftOuterEqJoin(LeftDeepJoin { lhs, .. }, ..) => { lhs.visit(f); } Self::RelVar(..) => {} @@ -232,7 +235,8 @@ impl RelExpr { match self { Self::Select(lhs, _) | Self::LeftDeepJoin(LeftDeepJoin { lhs, .. }) - | Self::EqJoin(LeftDeepJoin { lhs, .. }, ..) => { + | Self::InnerEqJoin(LeftDeepJoin { lhs, .. }, ..) + | Self::LeftOuterEqJoin(LeftDeepJoin { lhs, .. }, ..) => { lhs.visit_mut(f); } Self::RelVar(..) => {} @@ -243,7 +247,11 @@ impl RelExpr { pub fn nfields(&self) -> usize { match self { Self::RelVar(..) => 1, - Self::LeftDeepJoin(join) | Self::EqJoin(join, ..) => join.lhs.nfields() + 1, + Self::LeftDeepJoin(join) + | Self::InnerEqJoin(join, ..) + | Self::LeftOuterEqJoin(join, ..) => { + join.lhs.nfields() + 1 + } Self::Select(input, _) => input.nfields(), } } @@ -252,7 +260,9 @@ impl RelExpr { pub fn has_field(&self, field: &str) -> bool { match self { Self::RelVar(Relvar { alias, .. }) => alias.as_ref() == field, - Self::LeftDeepJoin(join) | Self::EqJoin(join, ..) => { + Self::LeftDeepJoin(join) + | Self::InnerEqJoin(join, ..) + | Self::LeftOuterEqJoin(join, ..) => { join.rhs.alias.as_ref() == field || join.lhs.has_field(field) } Self::Select(input, _) => input.has_field(field), @@ -264,10 +274,12 @@ impl RelExpr { match self { Self::RelVar(relvar) if relvar.alias.as_ref() == alias => Some(&relvar.schema), Self::Select(input, _) => input.find_table_schema(alias), - Self::EqJoin(LeftDeepJoin { rhs, .. }, ..) if rhs.alias.as_ref() == alias => Some(&rhs.schema), - Self::EqJoin(LeftDeepJoin { lhs, .. }, ..) => lhs.find_table_schema(alias), Self::LeftDeepJoin(LeftDeepJoin { rhs, .. }) if rhs.alias.as_ref() == alias => Some(&rhs.schema), Self::LeftDeepJoin(LeftDeepJoin { lhs, .. }) => lhs.find_table_schema(alias), + Self::InnerEqJoin(LeftDeepJoin { rhs, .. }, ..) if rhs.alias.as_ref() == alias => Some(&rhs.schema), + Self::InnerEqJoin(LeftDeepJoin { lhs, .. }, ..) => lhs.find_table_schema(alias), + Self::LeftOuterEqJoin(LeftDeepJoin { rhs, .. }, ..) if rhs.alias.as_ref() == alias => Some(&rhs.schema), + Self::LeftOuterEqJoin(LeftDeepJoin { lhs, .. }, ..) => lhs.find_table_schema(alias), _ => None, } } diff --git a/crates/expr/src/rls.rs b/crates/expr/src/rls.rs index 89bdf7149f8..8edd5b3e978 100644 --- a/crates/expr/src/rls.rs +++ b/crates/expr/src/rls.rs @@ -219,7 +219,8 @@ fn resolve_views_for_expr( view.visit(&mut |expr| match expr { RelExpr::RelVar(rhs) | RelExpr::LeftDeepJoin(LeftDeepJoin { rhs, .. }) - | RelExpr::EqJoin(LeftDeepJoin { rhs, .. }, ..) + | RelExpr::InnerEqJoin(LeftDeepJoin { rhs, .. }, ..) + | RelExpr::LeftOuterEqJoin(LeftDeepJoin { rhs, .. }, ..) if !is_return_table(rhs) => { names.push((rhs.schema.table_id, rhs.alias.clone())); @@ -365,7 +366,8 @@ fn alpha_rename(expr: &mut RelExpr, f: &mut impl FnMut(&str) -> Box) { RelExpr::RelVar(rhs) | RelExpr::LeftDeepJoin(LeftDeepJoin { rhs, .. }) => { rename(rhs, f); } - RelExpr::EqJoin(LeftDeepJoin { rhs, .. }, a, b) => { + RelExpr::InnerEqJoin(LeftDeepJoin { rhs, .. }, a, b) + | RelExpr::LeftOuterEqJoin(LeftDeepJoin { rhs, .. }, a, b) => { rename(rhs, f); rename_field(a, f); rename_field(b, f); @@ -427,7 +429,15 @@ fn extend_lhs(expr: RelExpr, with: RelExpr) -> RelExpr { lhs: Box::new(extend_lhs(*join.lhs, with)), ..join }), - RelExpr::EqJoin(join, a, b) => RelExpr::EqJoin( + RelExpr::InnerEqJoin(join, a, b) => RelExpr::InnerEqJoin( + LeftDeepJoin { + lhs: Box::new(extend_lhs(*join.lhs, with)), + ..join + }, + a, + b, + ), + RelExpr::LeftOuterEqJoin(join, a, b) => RelExpr::LeftOuterEqJoin( LeftDeepJoin { lhs: Box::new(extend_lhs(*join.lhs, with)), ..join @@ -451,11 +461,23 @@ fn expand_leaf(expr: RelExpr, table_id: TableId, alias: &str, with: &RelExpr) -> lhs: Box::new(expand_leaf(*lhs, table_id, alias, with)), rhs, }), - RelExpr::EqJoin(join, a, b) if ok(&join.rhs) => RelExpr::Select( - Box::new(extend_lhs(with.clone(), *join.lhs)), - Expr::BinOp(BinOp::Eq, Box::new(Expr::Field(a)), Box::new(Expr::Field(b))), + RelExpr::InnerEqJoin(join, a, b) + | RelExpr::LeftOuterEqJoin(join, a, b) + if ok(&join.rhs) => { + RelExpr::Select( + Box::new(extend_lhs(with.clone(), *join.lhs)), + Expr::BinOp(BinOp::Eq, Box::new(Expr::Field(a)), Box::new(Expr::Field(b))), + ) + } + RelExpr::InnerEqJoin(LeftDeepJoin { lhs, rhs }, a, b) => RelExpr::InnerEqJoin( + LeftDeepJoin { + lhs: Box::new(expand_leaf(*lhs, table_id, alias, with)), + rhs, + }, + a, + b, ), - RelExpr::EqJoin(LeftDeepJoin { lhs, rhs }, a, b) => RelExpr::EqJoin( + RelExpr::LeftOuterEqJoin(LeftDeepJoin { lhs, rhs }, a, b) => RelExpr::LeftOuterEqJoin( LeftDeepJoin { lhs: Box::new(expand_leaf(*lhs, table_id, alias, with)), rhs, diff --git a/crates/physical-plan/src/compile.rs b/crates/physical-plan/src/compile.rs index 56215f5e354..af17f3fce1f 100644 --- a/crates/physical-plan/src/compile.rs +++ b/crates/physical-plan/src/compile.rs @@ -79,7 +79,7 @@ fn compile_rel_expr(var: &mut impl VarLabel, ast: RelExpr) -> PhysicalPlan { let input = Box::new(input); PhysicalPlan::Filter(input, compile_expr(expr, var)) } - RelExpr::EqJoin( + RelExpr::InnerEqJoin( LeftDeepJoin { lhs, rhs: @@ -114,6 +114,46 @@ fn compile_rel_expr(var: &mut impl VarLabel, ast: RelExpr) -> PhysicalPlan { field_pos: b, }, unique: false, + outer: false, + }, + Semi::All, + ), + RelExpr::LeftOuterEqJoin( + LeftDeepJoin { + lhs, + rhs: + Relvar { + schema: rhs_schema, + alias: rhs_alias, + delta, + .. + }, + }, + FieldProject { table: u, field: a, .. }, + FieldProject { table: v, field: b, .. }, + ) => PhysicalPlan::HashJoin( + HashJoin { + lhs: Box::new(compile_rel_expr(var, *lhs)), + rhs: Box::new(PhysicalPlan::TableScan( + TableScan { + schema: rhs_schema, + limit: None, + delta, + }, + var.label(&rhs_alias), + )), + lhs_field: TupleField { + label: var.label(u.as_ref()), + label_pos: None, + field_pos: a, + }, + rhs_field: TupleField { + label: var.label(v.as_ref()), + label_pos: None, + field_pos: b, + }, + unique: false, + outer: true, }, Semi::All, ), diff --git a/crates/physical-plan/src/plan.rs b/crates/physical-plan/src/plan.rs index cb90637a6ff..792622d47f3 100644 --- a/crates/physical-plan/src/plan.rs +++ b/crates/physical-plan/src/plan.rs @@ -500,6 +500,7 @@ impl PhysicalPlan { lhs_field, rhs_field, unique, + outer, }, semi, ) if rhs.has_label(&lhs_field.label) || lhs.has_label(&rhs_field.label) => Self::HashJoin( @@ -509,6 +510,7 @@ impl PhysicalPlan { lhs_field: rhs_field, rhs_field: lhs_field, unique, + outer, }, semi, ), @@ -629,15 +631,18 @@ impl PhysicalPlan { lhs_field: lhs_field @ TupleField { label: u, .. }, rhs_field: rhs_field @ TupleField { label: v, .. }, unique, + outer, }, Semi::All, ) => { - let semi = reqs - .iter() - .all(|label| lhs.has_label(label)) - .then_some(Semi::Lhs) - .or_else(|| reqs.iter().all(|label| rhs.has_label(label)).then_some(Semi::Rhs)) - .unwrap_or(Semi::All); + let semi = if !outer { + reqs + .iter() + .all(|label| lhs.has_label(label)) + .then_some(Semi::Lhs) + .or_else(|| reqs.iter().all(|label| rhs.has_label(label)).then_some(Semi::Rhs)) + .unwrap_or(Semi::All) + } else { Semi::All }; let mut lhs_reqs = vec![u]; let mut rhs_reqs = vec![v]; for var in reqs { @@ -655,6 +660,7 @@ impl PhysicalPlan { lhs_field, rhs_field, unique, + outer, }, semi, ) @@ -991,6 +997,7 @@ pub struct HashJoin { pub lhs_field: TupleField, pub rhs_field: TupleField, pub unique: bool, + pub outer: bool, } /// An index join is a left deep join tree, @@ -1703,6 +1710,7 @@ mod tests { lhs_field: TupleField { field_pos: 1, .. }, rhs_field: TupleField { field_pos: 1, .. }, unique: true, + outer: false, }, Semi::Rhs, ) => (*rhs, *lhs), diff --git a/crates/physical-plan/src/rules.rs b/crates/physical-plan/src/rules.rs index dfcca1142e8..8af34d51c8e 100644 --- a/crates/physical-plan/src/rules.rs +++ b/crates/physical-plan/src/rules.rs @@ -949,6 +949,7 @@ impl RewriteRule for ReorderHashJoin { lhs_field: join.rhs_field, rhs_field: join.lhs_field, unique: join.unique, + outer: join.outer, }, Semi::All, )), @@ -1001,6 +1002,7 @@ impl RewriteRule for ReorderDeltaJoinRhs { lhs_field: join.rhs_field, rhs_field: join.lhs_field, unique: join.unique, + outer: join.outer, }, Semi::All, )), diff --git a/crates/sql-parser/src/ast/mod.rs b/crates/sql-parser/src/ast/mod.rs index 776d4fc5006..eaf113b8534 100644 --- a/crates/sql-parser/src/ast/mod.rs +++ b/crates/sql-parser/src/ast/mod.rs @@ -22,20 +22,48 @@ impl SqlFrom { } } -/// An inner join in a FROM clause +/// A join in a FROM clause #[derive(Debug)] -pub struct SqlJoin { - pub var: SqlIdent, - pub alias: SqlIdent, - pub on: Option, +pub enum SqlJoin { + Cross(CrossJoin), + Inner(InnerJoin), + Left(OuterJoin), } impl SqlJoin { pub fn has_unqualified_vars(&self) -> bool { - self.on.as_ref().is_some_and(|expr| expr.has_unqualified_vars()) + match self { + SqlJoin::Cross(_) => false, + SqlJoin::Inner(InnerJoin { on: None, .. }) => false, + SqlJoin::Inner(InnerJoin { on: Some(on), .. }) => on.has_unqualified_vars(), + SqlJoin::Left(OuterJoin { on, .. }) => on.has_unqualified_vars(), + } } } +/// Cross join +#[derive(Debug)] +pub struct CrossJoin { + pub var: SqlIdent, + pub alias: SqlIdent, +} + +/// Inner join +#[derive(Debug)] +pub struct InnerJoin { + pub var: SqlIdent, + pub alias: SqlIdent, + pub on: Option, +} + +/// Outer join +#[derive(Debug)] +pub struct OuterJoin { + pub var: SqlIdent, + pub alias: SqlIdent, + pub on: SqlExpr, +} + /// A projection expression in a SELECT clause #[derive(Debug)] pub struct ProjectElem(pub ProjectExpr, pub SqlIdent); diff --git a/crates/sql-parser/src/parser/errors.rs b/crates/sql-parser/src/parser/errors.rs index 0dc7b773a08..534f45f23b3 100644 --- a/crates/sql-parser/src/parser/errors.rs +++ b/crates/sql-parser/src/parser/errors.rs @@ -59,8 +59,8 @@ pub enum SqlUnsupported { MultiPartName(ObjectName), #[error("Unsupported: {0}")] Feature(String), - #[error("Non-inner joins are not supported")] - JoinType, + #[error("Non-column join constraints are not supported")] + JoinConstraintType, #[error("Implicit joins are not supported")] ImplicitJoins, #[error("Mixed wildcard projections are not supported")] diff --git a/crates/sql-parser/src/parser/mod.rs b/crates/sql-parser/src/parser/mod.rs index 61260823a43..72f03ac5639 100644 --- a/crates/sql-parser/src/parser/mod.rs +++ b/crates/sql-parser/src/parser/mod.rs @@ -6,7 +6,8 @@ use sqlparser::ast::{ }; use crate::ast::{ - BinOp, LogOp, Parameter, Project, ProjectElem, ProjectExpr, SqlExpr, SqlFrom, SqlIdent, SqlJoin, SqlLiteral, + BinOp, CrossJoin, InnerJoin, LogOp, OuterJoin, Parameter, Project, ProjectElem, ProjectExpr, + SqlExpr, SqlFrom, SqlIdent, SqlJoin, SqlLiteral, }; pub mod errors; @@ -49,8 +50,8 @@ trait RelParser { fn parse_join(join: Join) -> SqlParseResult { let (var, alias) = Self::parse_relvar(join.relation)?; match join.join_operator { - JoinOperator::CrossJoin => Ok(SqlJoin { var, alias, on: None }), - JoinOperator::Inner(JoinConstraint::None) => Ok(SqlJoin { var, alias, on: None }), + JoinOperator::CrossJoin => Ok(SqlJoin::Cross(CrossJoin { var, alias })), + JoinOperator::Inner(JoinConstraint::None) => Ok(SqlJoin::Inner(InnerJoin { var, alias, on: None })), JoinOperator::Inner(JoinConstraint::On(Expr::BinaryOp { left, op: BinaryOperator::Eq, @@ -58,7 +59,7 @@ trait RelParser { })) if matches!(*left, Expr::Identifier(..) | Expr::CompoundIdentifier(..)) && matches!(*right, Expr::Identifier(..) | Expr::CompoundIdentifier(..)) => { - Ok(SqlJoin { + Ok(SqlJoin::Inner(InnerJoin { var, alias, on: Some(parse_expr(Expr::BinaryOp { @@ -66,9 +67,26 @@ trait RelParser { op: BinaryOperator::Eq, right, })?), - }) + })) } - _ => Err(SqlUnsupported::JoinType.into()), + JoinOperator::LeftOuter(JoinConstraint::On(Expr::BinaryOp { + left, + op: BinaryOperator::Eq, + right, + })) if matches!(*left, Expr::Identifier(..) | Expr::CompoundIdentifier(..)) + && matches!(*right, Expr::Identifier(..) | Expr::CompoundIdentifier(..)) => + { + Ok(SqlJoin::Left(OuterJoin { + var, + alias, + on: parse_expr(Expr::BinaryOp { + left, + op: BinaryOperator::Eq, + right, + })?, + })) + } + _ => Err(SqlUnsupported::JoinConstraintType.into()), } } diff --git a/crates/vm/src/relation.rs b/crates/vm/src/relation.rs index 1c96413e986..c55c6ba2bce 100644 --- a/crates/vm/src/relation.rs +++ b/crates/vm/src/relation.rs @@ -32,6 +32,7 @@ pub enum RelValue<'a> { impl<'a> From> for RelValue<'a> { fn from(value: Row<'a>) -> Self { match value { + Row::Null => Self::Projection(ProductValue { elements: Box::new([]) }), Row::Ptr(ptr) => Self::Row(ptr), Row::Ref(ptr) => Self::ProjRef(ptr), }