From 16f7d6cf64701dc160c4162ad79fed6ea7b9878c Mon Sep 17 00:00:00 2001 From: Dima Date: Mon, 23 Jun 2025 18:09:57 +0100 Subject: [PATCH 1/2] Fix join precedence for non-snowflake queries --- src/parser/mod.rs | 2 +- tests/sqlparser_common.rs | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 44bf58d0f..99aa3bd06 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -12467,7 +12467,7 @@ impl<'a> Parser<'a> { }; let mut relation = self.parse_table_factor()?; - if self.peek_parens_less_nested_join() { + if dialect_of!(self is SnowflakeDialect) && self.peek_parens_less_nested_join() { let joins = self.parse_joins()?; relation = TableFactor::NestedJoin { table_with_joins: Box::new(TableWithJoins { relation, joins }), diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 52054604d..71cf4398b 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -15356,3 +15356,25 @@ fn check_enforced() { "CREATE TABLE t (a INT, b INT, c INT, CHECK (a > 0) NOT ENFORCED, CHECK (b > 0) ENFORCED, CHECK (c > 0))", ); } + +#[test] +fn join_precedence() { + all_dialects_except(|d| d.is::()).verified_query_with_canonical( + "SELECT * + FROM t1 + NATURAL JOIN t5 + INNER JOIN t0 ON (t0.v1 + t5.v0) > 0 + WHERE t0.v1 = t1.v0", + // canonical string without parentheses + "SELECT * FROM t1 NATURAL JOIN t5 INNER JOIN t0 ON (t0.v1 + t5.v0) > 0 WHERE t0.v1 = t1.v0", + ); + TestedDialects::new(vec![Box::new(SnowflakeDialect {})]).verified_query_with_canonical( + "SELECT * + FROM t1 + NATURAL JOIN t5 + INNER JOIN t0 ON (t0.v1 + t5.v0) > 0 + WHERE t0.v1 = t1.v0", + // canonical string with parentheses + "SELECT * FROM t1 NATURAL JOIN (t5 INNER JOIN t0 ON (t0.v1 + t5.v0) > 0) WHERE t0.v1 = t1.v0", + ); +} From dae0a12f65d2c2b4118e9d866790c6183183a668 Mon Sep 17 00:00:00 2001 From: Dima Date: Sat, 28 Jun 2025 11:30:56 +0100 Subject: [PATCH 2/2] introduce dialect nested joins flag --- src/dialect/generic.rs | 4 ++++ src/dialect/mod.rs | 28 ++++++++++++++++++++++++++++ src/dialect/snowflake.rs | 4 ++++ src/parser/mod.rs | 6 +++++- tests/sqlparser_common.rs | 5 +++-- 5 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index 8f57e487f..0b8822474 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -52,6 +52,10 @@ impl Dialect for GenericDialect { true } + fn supports_left_associative_joins_without_parens(&self) -> bool { + true + } + fn supports_connect_by(&self) -> bool { true } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index a4c899e6b..eb3d6250b 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -278,6 +278,34 @@ pub trait Dialect: Debug + Any { false } + /// Indicates whether the dialect supports left-associative join parsing + /// by default when parentheses are omitted in nested joins. + /// + /// Most dialects (like MySQL or Postgres) assume **left-associative** precedence, + /// so a query like: + /// + /// ```sql + /// SELECT * FROM t1 NATURAL JOIN t5 INNER JOIN t0 ON ... + /// ``` + /// is interpreted as: + /// ```sql + /// ((t1 NATURAL JOIN t5) INNER JOIN t0 ON ...) + /// ``` + /// and internally represented as a **flat list** of joins. + /// + /// In contrast, some dialects (e.g. **Snowflake**) assume **right-associative** + /// precedence and interpret the same query as: + /// ```sql + /// (t1 NATURAL JOIN (t5 INNER JOIN t0 ON ...)) + /// ``` + /// which results in a **nested join** structure in the AST. + /// + /// If this method returns `false`, the parser must build nested join trees + /// even in the absence of parentheses to reflect the correct associativity + fn supports_left_associative_joins_without_parens(&self) -> bool { + true + } + /// Returns true if the dialect supports the `(+)` syntax for OUTER JOIN. fn supports_outer_join_operator(&self) -> bool { false diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 66e04ac24..e19ce8cf3 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -283,6 +283,10 @@ impl Dialect for SnowflakeDialect { true } + fn supports_left_associative_joins_without_parens(&self) -> bool { + false + } + fn is_reserved_for_identifier(&self, kw: Keyword) -> bool { // Unreserve some keywords that Snowflake accepts as identifiers // See: https://docs.snowflake.com/en/sql-reference/reserved-keywords diff --git a/src/parser/mod.rs b/src/parser/mod.rs index ae98e6722..c59addb2c 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -12486,7 +12486,11 @@ impl<'a> Parser<'a> { }; let mut relation = self.parse_table_factor()?; - if dialect_of!(self is SnowflakeDialect) && self.peek_parens_less_nested_join() { + if !self + .dialect + .supports_left_associative_joins_without_parens() + && self.peek_parens_less_nested_join() + { let joins = self.parse_joins()?; relation = TableFactor::NestedJoin { table_with_joins: Box::new(TableWithJoins { relation, joins }), diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 02b82bac8..77fe5757d 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -15359,7 +15359,8 @@ fn check_enforced() { #[test] fn join_precedence() { - all_dialects_except(|d| d.is::()).verified_query_with_canonical( + all_dialects_except(|d| !d.supports_left_associative_joins_without_parens()) + .verified_query_with_canonical( "SELECT * FROM t1 NATURAL JOIN t5 @@ -15368,7 +15369,7 @@ fn join_precedence() { // canonical string without parentheses "SELECT * FROM t1 NATURAL JOIN t5 INNER JOIN t0 ON (t0.v1 + t5.v0) > 0 WHERE t0.v1 = t1.v0", ); - TestedDialects::new(vec![Box::new(SnowflakeDialect {})]).verified_query_with_canonical( + all_dialects_except(|d| d.supports_left_associative_joins_without_parens()).verified_query_with_canonical( "SELECT * FROM t1 NATURAL JOIN t5