From 22182a2d6917f9fc39116e069acebbba9a0ce986 Mon Sep 17 00:00:00 2001 From: Yoav Cohen Date: Fri, 17 Jan 2025 13:45:48 +0100 Subject: [PATCH 1/2] Add support for qualified column names in JOIN ... USING --- src/ast/query.rs | 2 +- src/ast/spans.rs | 2 +- src/parser/mod.rs | 31 ++++++++++++++++++++++++++++--- tests/sqlparser_common.rs | 2 +- tests/sqlparser_snowflake.rs | 3 +++ 5 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index 9bcdc2e74..66c70b466 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -2050,7 +2050,7 @@ pub enum JoinOperator { #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum JoinConstraint { On(Expr), - Using(Vec), + Using(Vec), Natural, None, } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 2a5a75b49..1ddd47d7f 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -2008,7 +2008,7 @@ impl Spanned for JoinConstraint { fn span(&self) -> Span { match self { JoinConstraint::On(expr) => expr.span(), - JoinConstraint::Using(vec) => union_spans(vec.iter().map(|i| i.span)), + JoinConstraint::Using(vec) => union_spans(vec.iter().map(|i| i.span())), JoinConstraint::Natural => Span::empty(), JoinConstraint::None => Span::empty(), } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 5cacfda98..5821f77de 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9342,12 +9342,37 @@ impl<'a> Parser<'a> { optional: IsOptional, allow_empty: bool, ) -> Result, ParserError> { + self.parse_parenthesized_column_list_inner(optional, allow_empty, |p| p.parse_identifier()) + } + + /// Parses a parenthesized comma-separated list of unqualified, possibly quoted identifiers + pub fn parse_parenthesized_qualified_column_list( + &mut self, + optional: IsOptional, + allow_empty: bool, + ) -> Result, ParserError> { + self.parse_parenthesized_column_list_inner(optional, allow_empty, |p| { + p.parse_object_name(false) + }) + } + + /// Parses a parenthesized comma-separated list of columns using + /// the provided function to parse each element. + fn parse_parenthesized_column_list_inner( + &mut self, + optional: IsOptional, + allow_empty: bool, + mut f: F, + ) -> Result, ParserError> + where + F: FnMut(&mut Parser) -> Result, + { if self.consume_token(&Token::LParen) { if allow_empty && self.peek_token().token == Token::RParen { self.next_token(); Ok(vec![]) } else { - let cols = self.parse_comma_separated(|p| p.parse_identifier())?; + let cols = self.parse_comma_separated(|p| f(p))?; self.expect_token(&Token::RParen)?; Ok(cols) } @@ -9358,7 +9383,7 @@ impl<'a> Parser<'a> { } } - /// Parse a parenthesized comma-separated list of table alias column definitions. + /// Parses a parenthesized comma-separated list of table alias column definitions. fn parse_table_alias_column_defs(&mut self) -> Result, ParserError> { if self.consume_token(&Token::LParen) { let cols = self.parse_comma_separated(|p| { @@ -11853,7 +11878,7 @@ impl<'a> Parser<'a> { let constraint = self.parse_expr()?; Ok(JoinConstraint::On(constraint)) } else if self.parse_keyword(Keyword::USING) { - let columns = self.parse_parenthesized_column_list(Mandatory, false)?; + let columns = self.parse_parenthesized_qualified_column_list(Mandatory, false)?; Ok(JoinConstraint::Using(columns)) } else { Ok(JoinConstraint::None) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 49588a581..b3d70e9c5 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -6541,7 +6541,7 @@ fn parse_joins_using() { sample: None, }, global: false, - join_operator: f(JoinConstraint::Using(vec!["c1".into()])), + join_operator: f(JoinConstraint::Using(vec![ObjectName(vec!["c1".into()])])), } } // Test parsing of aliases diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index fe6439b36..65999eb3a 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -2650,6 +2650,9 @@ fn asof_joins() { "ON s.state = p.state ", "ORDER BY s.observed", )); + + // Snowflake allows fully-qualified column names inside USING + snowflake().verified_stmt("SELECT * FROM tbl1 AS t1 JOIN tbl2 AS t2 USING(t2.col1)"); } #[test] From 58b4fc644df85f448be4f155582b60f84d0abd6b Mon Sep 17 00:00:00 2001 From: Yoav Cohen Date: Sat, 18 Jan 2025 16:05:57 +0100 Subject: [PATCH 2/2] Code review comments --- src/parser/mod.rs | 8 +++++--- tests/sqlparser_common.rs | 1 + tests/sqlparser_snowflake.rs | 3 --- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 5821f77de..a3adb0235 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9336,7 +9336,8 @@ impl<'a> Parser<'a> { }) } - /// Parse a parenthesized comma-separated list of unqualified, possibly quoted identifiers + /// Parses a parenthesized comma-separated list of unqualified, possibly quoted identifiers. + /// For example: `(col1, "col 2", ...)` pub fn parse_parenthesized_column_list( &mut self, optional: IsOptional, @@ -9345,14 +9346,15 @@ impl<'a> Parser<'a> { self.parse_parenthesized_column_list_inner(optional, allow_empty, |p| p.parse_identifier()) } - /// Parses a parenthesized comma-separated list of unqualified, possibly quoted identifiers + /// Parses a parenthesized comma-separated list of qualified, possibly quoted identifiers. + /// For example: `(db1.sc1.tbl1.col1, db1.sc1.tbl1."col 2", ...)` pub fn parse_parenthesized_qualified_column_list( &mut self, optional: IsOptional, allow_empty: bool, ) -> Result, ParserError> { self.parse_parenthesized_column_list_inner(optional, allow_empty, |p| { - p.parse_object_name(false) + p.parse_object_name(true) }) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index b3d70e9c5..7271d6375 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -6598,6 +6598,7 @@ fn parse_joins_using() { only(&verified_only_select("SELECT * FROM t1 FULL JOIN t2 USING(c1)").from).joins, vec![join_with_constraint("t2", None, JoinOperator::FullOuter)] ); + verified_stmt("SELECT * FROM tbl1 AS t1 JOIN tbl2 AS t2 USING(t2.col1)"); } #[test] diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 65999eb3a..fe6439b36 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -2650,9 +2650,6 @@ fn asof_joins() { "ON s.state = p.state ", "ORDER BY s.observed", )); - - // Snowflake allows fully-qualified column names inside USING - snowflake().verified_stmt("SELECT * FROM tbl1 AS t1 JOIN tbl2 AS t2 USING(t2.col1)"); } #[test]