Skip to content

Commit

Permalink
Support for (+) outer join syntax (#1145)
Browse files Browse the repository at this point in the history
  • Loading branch information
jmhain authored Feb 28, 2024
1 parent b284fdf commit 6a9b6f5
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 2 deletions.
18 changes: 18 additions & 0 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -713,6 +713,21 @@ pub enum Expr {
/// Qualified wildcard, e.g. `alias.*` or `schema.table.*`.
/// (Same caveats apply to `QualifiedWildcard` as to `Wildcard`.)
QualifiedWildcard(ObjectName),
/// Some dialects support an older syntax for outer joins where columns are
/// marked with the `(+)` operator in the WHERE clause, for example:
///
/// ```sql
/// SELECT t1.c1, t2.c2 FROM t1, t2 WHERE t1.c1 = t2.c2 (+)
/// ```
///
/// which is equivalent to
///
/// ```sql
/// SELECT t1.c1, t2.c2 FROM t1 LEFT OUTER JOIN t2 ON t1.c1 = t2.c2
/// ```
///
/// See <https://docs.snowflake.com/en/sql-reference/constructs/where#joins-in-the-where-clause>.
OuterJoin(Box<Expr>),
}

impl fmt::Display for CastFormat {
Expand Down Expand Up @@ -1174,6 +1189,9 @@ impl fmt::Display for Expr {

Ok(())
}
Expr::OuterJoin(expr) => {
write!(f, "{expr} (+)")
}
}
}
}
Expand Down
30 changes: 28 additions & 2 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -998,8 +998,19 @@ impl<'a> Parser<'a> {
if ends_with_wildcard {
Ok(Expr::QualifiedWildcard(ObjectName(id_parts)))
} else if self.consume_token(&Token::LParen) {
self.prev_token();
self.parse_function(ObjectName(id_parts))
if dialect_of!(self is SnowflakeDialect | MsSqlDialect)
&& self.consume_tokens(&[Token::Plus, Token::RParen])
{
Ok(Expr::OuterJoin(Box::new(
match <[Ident; 1]>::try_from(id_parts) {
Ok([ident]) => Expr::Identifier(ident),
Err(parts) => Expr::CompoundIdentifier(parts),
},
)))
} else {
self.prev_token();
self.parse_function(ObjectName(id_parts))
}
} else {
Ok(Expr::CompoundIdentifier(id_parts))
}
Expand Down Expand Up @@ -2860,6 +2871,21 @@ impl<'a> Parser<'a> {
}
}

/// If the current and subsequent tokens exactly match the `tokens`
/// sequence, consume them and returns true. Otherwise, no tokens are
/// consumed and returns false
#[must_use]
pub fn consume_tokens(&mut self, tokens: &[Token]) -> bool {
let index = self.index;
for token in tokens {
if !self.consume_token(token) {
self.index = index;
return false;
}
}
true
}

/// Bail out if the current token is not an expected keyword, or consume it if it is
pub fn expect_token(&mut self, expected: &Token) -> Result<(), ParserError> {
if self.consume_token(expected) {
Expand Down
65 changes: 65 additions & 0 deletions tests/sqlparser_snowflake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1173,3 +1173,68 @@ fn parse_top() {
"SELECT TOP 4 c1 FROM testtable",
);
}

#[test]
fn parse_comma_outer_join() {
// compound identifiers
let case1 =
snowflake().verified_only_select("SELECT t1.c1, t2.c2 FROM t1, t2 WHERE t1.c1 = t2.c2 (+)");
assert_eq!(
case1.selection,
Some(Expr::BinaryOp {
left: Box::new(Expr::CompoundIdentifier(vec![
Ident::new("t1"),
Ident::new("c1")
])),
op: BinaryOperator::Eq,
right: Box::new(Expr::OuterJoin(Box::new(Expr::CompoundIdentifier(vec![
Ident::new("t2"),
Ident::new("c2")
]))))
})
);

// regular identifiers
let case2 =
snowflake().verified_only_select("SELECT t1.c1, t2.c2 FROM t1, t2 WHERE c1 = c2 (+)");
assert_eq!(
case2.selection,
Some(Expr::BinaryOp {
left: Box::new(Expr::Identifier(Ident::new("c1"))),
op: BinaryOperator::Eq,
right: Box::new(Expr::OuterJoin(Box::new(Expr::Identifier(Ident::new(
"c2"
)))))
})
);

// ensure we can still parse function calls with a unary plus arg
let case3 =
snowflake().verified_only_select("SELECT t1.c1, t2.c2 FROM t1, t2 WHERE c1 = myudf(+42)");
assert_eq!(
case3.selection,
Some(Expr::BinaryOp {
left: Box::new(Expr::Identifier(Ident::new("c1"))),
op: BinaryOperator::Eq,
right: Box::new(Expr::Function(Function {
name: ObjectName(vec![Ident::new("myudf")]),
args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::UnaryOp {
op: UnaryOperator::Plus,
expr: Box::new(Expr::Value(number("42")))
}))],
filter: None,
null_treatment: None,
over: None,
distinct: false,
special: false,
order_by: vec![]
}))
})
);

// permissive with whitespace
snowflake().verified_only_select_with_canonical(
"SELECT t1.c1, t2.c2 FROM t1, t2 WHERE t1.c1 = t2.c2( + )",
"SELECT t1.c1, t2.c2 FROM t1, t2 WHERE t1.c1 = t2.c2 (+)",
);
}

0 comments on commit 6a9b6f5

Please sign in to comment.