This is an automated email from the ASF dual-hosted git repository.

iffyio pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion-sqlparser-rs.git


The following commit(s) were added to refs/heads/main by this push:
     new 3bc94234 Fix join precedence for non-snowflake queries (#1905)
3bc94234 is described below

commit 3bc94234dfcf73d091c592edd1dab3fdd075ab82
Author: Dima <111751109+dimchi...@users.noreply.github.com>
AuthorDate: Sat Jun 28 19:24:25 2025 +0100

    Fix join precedence for non-snowflake queries (#1905)
---
 src/dialect/generic.rs    |  4 ++++
 src/dialect/mod.rs        | 28 ++++++++++++++++++++++++++++
 src/dialect/snowflake.rs  |  4 ++++
 src/parser/mod.rs         |  6 +++++-
 tests/sqlparser_common.rs | 23 +++++++++++++++++++++++
 5 files changed, 64 insertions(+), 1 deletion(-)

diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs
index 8f57e487..0b882247 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 c79b4517..028aa58a 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 5ebb7e37..ba28a8ec 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 adf50a8f..a079488f 100644
--- a/src/parser/mod.rs
+++ b/src/parser/mod.rs
@@ -12495,7 +12495,11 @@ impl<'a> Parser<'a> {
                 };
                 let mut relation = self.parse_table_factor()?;
 
-                if 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 17b46e6f..f6f51459 100644
--- a/tests/sqlparser_common.rs
+++ b/tests/sqlparser_common.rs
@@ -15359,6 +15359,29 @@ fn check_enforced() {
     );
 }
 
+#[test]
+fn join_precedence() {
+    all_dialects_except(|d| 
!d.supports_left_associative_joins_without_parens())
+        .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",
+    );
+    all_dialects_except(|d| 
d.supports_left_associative_joins_without_parens()).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",
+    );
+}
+
 #[test]
 fn parse_create_procedure_with_language() {
     let sql = r#"CREATE PROCEDURE test_proc LANGUAGE sql AS BEGIN SELECT 1; 
END"#;


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscr...@datafusion.apache.org
For additional commands, e-mail: commits-h...@datafusion.apache.org

Reply via email to