diff --git a/crates/polyglot-sql/src/optimizer/qualify_tables.rs b/crates/polyglot-sql/src/optimizer/qualify_tables.rs index 8257070c..c108143b 100644 --- a/crates/polyglot-sql/src/optimizer/qualify_tables.rs +++ b/crates/polyglot-sql/src/optimizer/qualify_tables.rs @@ -868,9 +868,11 @@ mod tests { let qualified = qualify_tables(expr, &options); let sql = gen(&qualified); + // The parser keeps the set operation outside the derived table, so + // there is no wrapper left to normalize. assert_eq!( sql, - "SELECT * FROM (SELECT * FROM (SELECT * FROM tab_1 AS tab_1) AS _0 UNION ALL SELECT * FROM (SELECT * FROM tab_1 AS tab_1) AS _1) AS _2" + "SELECT * FROM (SELECT * FROM tab_1 AS tab_1) AS _0 UNION ALL SELECT * FROM (SELECT * FROM tab_1 AS tab_1) AS _1" ); } } diff --git a/crates/polyglot-sql/src/parser.rs b/crates/polyglot-sql/src/parser.rs index 709e3262..024cf415 100644 --- a/crates/polyglot-sql/src/parser.rs +++ b/crates/polyglot-sql/src/parser.rs @@ -4807,34 +4807,11 @@ impl Parser { }; self.expect(TokenType::RParen)?; let trailing = self.previous_trailing_comments().to_vec(); - // Check for set operations after parenthesized query - // If there's a set operation, wrap query in Subquery first to preserve parens - // e.g., (SELECT 1) UNION (SELECT 2) - the left operand needs Subquery wrapper - let result = if self.check(TokenType::Union) - || self.check(TokenType::Intersect) - || self.check(TokenType::Except) - { - let left = Expression::Subquery(Box::new(Subquery { - this: query, - alias: None, - column_aliases: Vec::new(), - alias_explicit_as: false, - alias_keyword: None, - order_by: None, - limit: None, - offset: None, - lateral: false, - modifiers_inside: false, - trailing_comments: Vec::new(), - distribute_by: None, - sort_by: None, - cluster_by: None, - inferred_type: None, - })); - self.parse_set_operation(left)? - } else { - query - }; + // A set operation after the closing paren is not part of this + // FROM item: it belongs to the enclosing query, or, inside + // nested parens (`((SELECT 1) UNION (SELECT 2))`), to the + // nested-paren branch, which parses it itself. + let result = query; Expression::Subquery(Box::new(Subquery { this: result, alias: None, @@ -4917,10 +4894,26 @@ impl Parser { || self.check(TokenType::Intersect) || self.check(TokenType::Except); let result = if had_set_operation { - // This is a set operation like ((SELECT 1) UNION (SELECT 2)) - // Wrap inner in a subquery-like expression and parse set operation + // This is a set operation like ((SELECT 1) UNION (SELECT 2)): + // the parens around it make it one derived table. let set_result = self.parse_set_operation(inner)?; - set_result + Expression::Subquery(Box::new(Subquery { + this: set_result, + alias: None, + column_aliases: Vec::new(), + alias_explicit_as: false, + alias_keyword: None, + order_by: None, + limit: None, + offset: None, + distribute_by: None, + sort_by: None, + cluster_by: None, + lateral: false, + modifiers_inside: false, + trailing_comments: Vec::new(), + inferred_type: None, + })) } else if self.check(TokenType::Cross) || self.check(TokenType::Outer) || self.check(TokenType::Inner) diff --git a/crates/polyglot-sql/tests/unaliased_derived_table_set_operation.rs b/crates/polyglot-sql/tests/unaliased_derived_table_set_operation.rs new file mode 100644 index 00000000..7f807d8c --- /dev/null +++ b/crates/polyglot-sql/tests/unaliased_derived_table_set_operation.rs @@ -0,0 +1,36 @@ +//! An unaliased derived table at the end of a set operation's first operand +//! belongs to that operand: the set operation after it is not part of it. + +use polyglot_sql::{transpile, DialectType}; + +fn reprint(sql: &str, dialect: DialectType) -> String { + transpile(sql, dialect, dialect) + .unwrap_or_else(|e| panic!("failed to transpile {sql:?}: {e}")) + .remove(0) +} + +#[test] +fn a_set_operation_after_an_unaliased_derived_table_stays_outside_it() { + for sql in [ + "SELECT 1 AS s, * FROM (SELECT x FROM a) UNION ALL SELECT 2, x FROM b", + "SELECT 'p' AS side, * FROM (SELECT * FROM a LIMIT 1) UNION ALL SELECT 'l' AS side, * FROM (SELECT * FROM b LIMIT 1) LIMIT 2", + "SELECT * FROM c, (SELECT x FROM a LIMIT 1) UNION ALL SELECT x FROM b", + "SELECT y, * FROM (SELECT x FROM a) EXCEPT SELECT y, x FROM b", + ] { + for dialect in [DialectType::Trino, DialectType::Generic] { + assert_eq!(reprint(sql, dialect), sql, "{dialect:?}"); + } + } +} + +#[test] +fn a_parenthesized_set_operation_keeps_its_own_modifiers_inside_its_parens() { + for sql in [ + "SELECT * FROM ((SELECT 1) UNION (SELECT 2))", + "SELECT * FROM ((SELECT 1) UNION SELECT 2)", + "SELECT * FROM (((SELECT 1) UNION SELECT 2) ORDER BY x LIMIT 1 OFFSET 1)", + "SELECT * FROM ((SELECT 1) AS a UNION ALL (SELECT 2) AS b)", + ] { + assert_eq!(reprint(sql, DialectType::Generic), sql); + } +}