Repository navigation
Upgrade SqlGlot to Version 30.18 #568
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4ad9117
403aec1
0cf6142
a601efb
31577bf
9db4bb0
7b61402
b4e5afb
72f5db0
48cfc7d
fb82623
796d951
f8e071a
abc285b
7356003
b1e72f4
6783724
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -160,6 +160,11 @@ def apply_sqlglot_optimizer( | |
| if not isinstance(dialect, SnowflakeDialect): | ||
| kwargs["dialect"] = dialect | ||
|
|
||
| # Protect Oracle-reserved-word identifiers from being upper-cased by | ||
| # qualify's normalize_identifiers step, before quote_oracle_identifiers | ||
| # (below) has a chance to quote them with their original casing intact. | ||
| protect_oracle_reserved_identifiers(glot_expr, dialect) | ||
|
|
||
| # Rewrite sqlglot AST to have normalized and qualified tables and columns. | ||
| glot_expr = qualify(glot_expr, **kwargs) | ||
|
|
||
|
|
@@ -453,7 +458,7 @@ def remove_table_aliases_conditional(expr: SQLGlotExpression) -> None: | |
| if isinstance(expr, Select) and ( | ||
| expr.args.get("joins") is None or len(expr.args.get("joins")) == 0 | ||
| ): | ||
| from_clause = expr.args.get("from") | ||
| from_clause = expr.args.get("from_") | ||
| # Only remove aliases if there is a table in the FROM clause as opposed | ||
| # to a subquery. | ||
| if from_clause is not None and isinstance(from_clause.this, Table): | ||
|
|
@@ -603,12 +608,55 @@ def remove_tuple_row_values(expr: SQLGlotExpression) -> None: | |
| ) | ||
|
|
||
|
|
||
| def quote_oracle_identifiers(expr: SQLGlotExpression, dialect: SQLGlotDialect) -> None: | ||
| def _oracle_identifier_needs_quoting(name: str) -> bool: | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Refactored code to be able to use twice. |
||
| """ | ||
| Add quotes to Identifiers that are invalid as unquoted Oracle identifiers: | ||
| Checks whether a raw identifier name is invalid as an unquoted Oracle | ||
| identifier: | ||
| - Identifiers starting with '_' (Oracle syntax restriction) | ||
| - Oracle reserved words used as column aliases (e.g. COMMENT, KEY, SIZE) | ||
| which cause ORA-00923 parse errors, especially in CTAS statements. | ||
| """ | ||
| return name.startswith("_") or name.lower() in _ORACLE_RESERVED_ALIASES | ||
|
|
||
|
|
||
| def protect_oracle_reserved_identifiers( | ||
| expr: SQLGlotExpression, dialect: SQLGlotDialect | ||
| ) -> None: | ||
| """ | ||
| Marks Identifiers that will need Oracle quoting (see | ||
| `_oracle_identifier_needs_quoting`) as case-sensitive, so that the | ||
| `qualify` step's `normalize_identifiers` pass (which upper-cases any | ||
| identifier not already marked quoted/case-sensitive) does not clobber | ||
| their original casing before `quote_oracle_identifiers` gets a chance to | ||
| quote them. Must run before `qualify` for the same reason. | ||
|
|
||
| Note: This only is required for the Oracle dialect. | ||
|
|
||
| Args: | ||
| expr: The SQLGlot expression to visit. | ||
| dialect: The dialect being generated for. | ||
|
|
||
| Returns: | ||
| None (The AST is modified in place.) | ||
| """ | ||
| if not isinstance(dialect, OracleDialect): | ||
| return | ||
|
|
||
| if isinstance(expr, sqlglot_expressions.Identifier): | ||
| if not expr.quoted and _oracle_identifier_needs_quoting(expr.this): | ||
| expr.meta["case_sensitive"] = True | ||
| # Identifiers are leaf nodes, so no recursion is needed | ||
| return | ||
|
|
||
| # Recursively visit the subexpressions. | ||
| for arg in expr.iter_expressions(): | ||
| protect_oracle_reserved_identifiers(arg, dialect) | ||
|
|
||
|
|
||
| def quote_oracle_identifiers(expr: SQLGlotExpression, dialect: SQLGlotDialect) -> None: | ||
| """ | ||
| Add quotes to Identifiers that are invalid as unquoted Oracle identifiers | ||
| (see `_oracle_identifier_needs_quoting`). | ||
|
|
||
| Note: This only is required for the Oracle dialect. | ||
|
|
||
|
|
@@ -623,10 +671,7 @@ def quote_oracle_identifiers(expr: SQLGlotExpression, dialect: SQLGlotDialect) - | |
| return | ||
|
|
||
| if isinstance(expr, sqlglot_expressions.Identifier): | ||
| needs_quoting = expr.this.startswith("_") or ( | ||
| expr.this.lower() in _ORACLE_RESERVED_ALIASES | ||
| ) | ||
| if needs_quoting: | ||
| if _oracle_identifier_needs_quoting(expr.this): | ||
| new_identifier = sqlglot_expressions.Identifier(this=expr.this, quoted=True) | ||
| expr.replace(new_identifier) | ||
| # Identifiers are leaf nodes, so no recursion is needed | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,7 +34,7 @@ def canonicalize( | |
| def _canonicalize(expression: exp.Expression) -> exp.Expression: | ||
| expression = fix_expression_type(expression, dialect) # PyDough change | ||
| expression = add_text_to_concat(expression) | ||
| expression = replace_date_funcs(expression) | ||
| expression = replace_date_funcs(expression, dialect=dialect) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Passing dialect is now required |
||
| expression = coerce_type(expression, dialect.PROMOTE_TO_INFERRED_DATETIME_TYPE) | ||
| expression = remove_redundant_casts(expression) | ||
| expression = ensure_bools(expression, _replace_int_predicate) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -60,7 +60,7 @@ def merge_subqueries(expression: E, leave_tables_isolated: bool = False) -> E: | |
| # If a derived table has these Select args, it can't be merged | ||
| UNMERGABLE_ARGS = set(exp.Select.arg_types) - { | ||
| "expressions", | ||
| "from", | ||
| "from_", | ||
| "joins", | ||
| "where", | ||
| "order", | ||
|
|
@@ -135,7 +135,7 @@ def invalid_aggregate_convolution(inner_scope: Scope, outer_scope: Scope) -> boo | |
| # Temporarily remove the "with" context from the outer scope to avoid | ||
| # using CTEs when considering what the outer scope contains, then restore | ||
| # it before returning. | ||
| with_ctx = outer_scope.expression.args.pop("with", None) | ||
| with_ctx = outer_scope.expression.args.pop("with_", None) | ||
| result: bool = False | ||
| if inner_scope.expression.find(exp.AggFunc) and ( | ||
| outer_scope.expression.find(exp.AggFunc) | ||
|
|
@@ -184,7 +184,7 @@ def invalid_aggregate_convolution(inner_scope: Scope, outer_scope: Scope) -> boo | |
| if contains_match: | ||
| result = True | ||
| break | ||
| outer_scope.expression.args["with"] = with_ctx | ||
| outer_scope.expression.args["with_"] = with_ctx | ||
| return result | ||
|
|
||
|
|
||
|
|
@@ -198,7 +198,17 @@ def has_seq4_or_table(expr: Scope) -> bool: | |
| True if SEQ4() or TABLE() is found, False otherwise. | ||
| """ | ||
| for e in expr.walk(): | ||
| if isinstance(e, exp.Anonymous) and e.this.upper() in {"SEQ4", "TABLE"}: | ||
| # PYDOUGH CHANGE: sqlglot now parses SEQ4() and TABLE(...) into | ||
| # dedicated expression classes (Seq4, TableFromRows) instead of | ||
| # falling back to a generic Anonymous function call. By the time | ||
| # this runs, `apply_sqlglot_optimizer` has already round-tripped the | ||
| # expression through `.sql()` + `parse_one()` once, so even the | ||
| # occurrences PyDough constructs by hand as `exp.Anonymous(this="SEQ4")` | ||
| # (see `sf_transform_bindings.py`) have already been re-parsed into | ||
| # the dedicated classes by this point, making the old Anonymous-based | ||
| # check dead code (verified empirically: removing it doesn't change | ||
| # any reference SQL or break any test). | ||
| if isinstance(e, (exp.Seq4, exp.TableFromRows)): | ||
| return True | ||
|
Comment on lines
+201
to
+212
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not just replace the following condition then?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch. updated |
||
| return False | ||
|
|
||
|
|
@@ -271,7 +281,7 @@ def _outer_select_joins_on_inner_select_join(): | |
| if not on: | ||
| return False | ||
| selections = [c.name for c in on.find_all(exp.Column) if c.table == alias] | ||
| inner_from = inner_scope.expression.args.get("from") | ||
| inner_from = inner_scope.expression.args.get("from_") | ||
| if not inner_from: | ||
| return False | ||
| inner_from_table = inner_from.alias_or_name | ||
|
|
@@ -303,7 +313,7 @@ def _is_recursive(): | |
| and not outer_scope.expression.is_star | ||
| and isinstance(inner_select, exp.Select) | ||
| and not any(inner_select.args.get(arg) for arg in UNMERGABLE_ARGS) | ||
| and inner_select.args.get("from") is not None | ||
| and inner_select.args.get("from_") is not None | ||
| and not outer_scope.pivots | ||
| and not any(e.find(exp.Select, exp.Explode) for e in inner_select.expressions) | ||
| # PYDOUGH CHANGE: allow merging when the inner select has an | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,7 +21,7 @@ def contains_real_aggregate(expression) -> bool: | |
| is created by PyDough to account for such an edge case when pushing down | ||
| predicates. | ||
| """ | ||
| for agg_expr in find_all_in_scope(expression, exp.AggFunc, bfs=True): | ||
| for agg_expr in find_all_in_scope(expression, exp.AggFunc): | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. New version dropped |
||
| if ( | ||
| isinstance(agg_expr, (exp.Max, exp.Min)) | ||
| and len(agg_expr.args["expressions"]) > 0 | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the new SqlGlot version,
qualify()'s internal step order changed:normalize_identifiersnow runs before table/column qualification, whereas previously it ran after.This now caused a problem where PyDough's existing
quote_oracle_identifiers()(unmodified, pre-existing code) adds quotes around Oracle-reserved-word identifiers like key, date, comment, but it only runs at the very end of the optimizer pipeline. Under the new step order,normalize_identifiersnow upper-cases any not-yet-quoted identifier beforequote_oracle_identifiersgets a chance to run, so by the time quoting is added, the original casing (key) has already been destroyed (KEY), and the quoted result ("KEY") no longer matches the real column name (key) created earlier via a different code path that did preserve casing.