Fix join precedence for non-snowflake queries (#1905)
Some checks failed
license / Release Audit Tool (RAT) (push) Has been cancelled
Rust / codestyle (push) Has been cancelled
Rust / lint (push) Has been cancelled
Rust / benchmark-lint (push) Has been cancelled
Rust / compile (push) Has been cancelled
Rust / docs (push) Has been cancelled
Rust / compile-no-std (push) Has been cancelled
Rust / test (beta) (push) Has been cancelled
Rust / test (nightly) (push) Has been cancelled
Rust / test (stable) (push) Has been cancelled

This commit is contained in:
Dima 2025-06-28 19:24:25 +01:00 committed by GitHub
parent 50c605a471
commit 3bc94234df
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 64 additions and 1 deletions

View file

@ -52,6 +52,10 @@ impl Dialect for GenericDialect {
true true
} }
fn supports_left_associative_joins_without_parens(&self) -> bool {
true
}
fn supports_connect_by(&self) -> bool { fn supports_connect_by(&self) -> bool {
true true
} }

View file

@ -278,6 +278,34 @@ pub trait Dialect: Debug + Any {
false 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. /// Returns true if the dialect supports the `(+)` syntax for OUTER JOIN.
fn supports_outer_join_operator(&self) -> bool { fn supports_outer_join_operator(&self) -> bool {
false false

View file

@ -283,6 +283,10 @@ impl Dialect for SnowflakeDialect {
true true
} }
fn supports_left_associative_joins_without_parens(&self) -> bool {
false
}
fn is_reserved_for_identifier(&self, kw: Keyword) -> bool { fn is_reserved_for_identifier(&self, kw: Keyword) -> bool {
// Unreserve some keywords that Snowflake accepts as identifiers // Unreserve some keywords that Snowflake accepts as identifiers
// See: https://docs.snowflake.com/en/sql-reference/reserved-keywords // See: https://docs.snowflake.com/en/sql-reference/reserved-keywords

View file

@ -12495,7 +12495,11 @@ impl<'a> Parser<'a> {
}; };
let mut relation = self.parse_table_factor()?; 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()?; let joins = self.parse_joins()?;
relation = TableFactor::NestedJoin { relation = TableFactor::NestedJoin {
table_with_joins: Box::new(TableWithJoins { relation, joins }), table_with_joins: Box::new(TableWithJoins { relation, joins }),

View file

@ -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] #[test]
fn parse_create_procedure_with_language() { fn parse_create_procedure_with_language() {
let sql = r#"CREATE PROCEDURE test_proc LANGUAGE sql AS BEGIN SELECT 1; END"#; let sql = r#"CREATE PROCEDURE test_proc LANGUAGE sql AS BEGIN SELECT 1; END"#;