From 6a9b6f547d9c442c4e925bcb77a5d86ebb8b736d Mon Sep 17 00:00:00 2001 From: Joey Hain Date: Wed, 28 Feb 2024 14:27:42 -0800 Subject: [PATCH] Support for `(+)` outer join syntax (#1145) --- src/ast/mod.rs | 18 ++++++++++ src/parser/mod.rs | 30 +++++++++++++++-- tests/sqlparser_snowflake.rs | 65 ++++++++++++++++++++++++++++++++++++ 3 files changed, 111 insertions(+), 2 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index a22dcc04..79848ad2 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -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 . + OuterJoin(Box), } impl fmt::Display for CastFormat { @@ -1174,6 +1189,9 @@ impl fmt::Display for Expr { Ok(()) } + Expr::OuterJoin(expr) => { + write!(f, "{expr} (+)") + } } } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 55e49b6d..ce28b6b6 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -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)) } @@ -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) { diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 64c47e25..ce2aaedc 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -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 (+)", + ); +}