diff --git a/sqlglot/generator.py b/sqlglot/generator.py index 73979846e7..1281038b8e 100644 --- a/sqlglot/generator.py +++ b/sqlglot/generator.py @@ -2955,6 +2955,42 @@ def distribute_sql(self, expression: exp.Distribute) -> str: def sort_sql(self, expression: exp.Sort) -> str: return self.op_expressions("SORT BY", expression) + def _resolve_ordered_for_null_ordering_simulation( + self, expression: exp.Ordered + ) -> exp.Expr | None: + """Resolve a bare ORDER BY name against the enclosing SELECT projection. + + Returns the underlying expression of the uniquely-matching projection + (Alias-stripped) for substitution into the NULLS FIRST/LAST CASE + simulation, since the CASE is evaluated in FROM-clause scope rather + than alias scope (MySQL error 1052). Returns None if no safe + substitution applies, leaving the original behaviour unchanged. + """ + this = expression.this + if not (isinstance(this, exp.Column) and not this.table): + return None + + ancestor = expression.find_ancestor(exp.Window, exp.Select) + if not isinstance(ancestor, exp.Select): + return None + + column_name = this.name + match: exp.Expr | None = None + for projection in ancestor.selects: + if projection.output_name != column_name: + continue + if match is not None: + # Multiple projections share this output name; not safe to pick one. + return None + match = projection.this if isinstance(projection, exp.Alias) else projection + + # Skip the substitution when it would be identical to the existing + # reference (e.g. ``SELECT col FROM t ORDER BY col``). + if isinstance(match, exp.Column) and not match.table and match.name == column_name: + return None + + return match + def ordered_sql(self, expression: exp.Ordered) -> str: desc = expression.args.get("desc") asc = not desc @@ -3025,10 +3061,10 @@ def ordered_sql(self, expression: exp.Ordered) -> str: f"'{nulls_sort_change.strip()}' translation not supported with positional ordering" ) elif not isinstance(expression.this, exp.Rand): + resolved = self._resolve_ordered_for_null_ordering_simulation(expression) + target = self.sql(resolved) if resolved is not None else this null_sort_order = " DESC" if nulls_sort_change == " NULLS FIRST" else "" - this = ( - f"CASE WHEN {this} IS NULL THEN 1 ELSE 0 END{null_sort_order}, {this}" - ) + this = f"CASE WHEN {target} IS NULL THEN 1 ELSE 0 END{null_sort_order}, {target}" nulls_sort_change = "" with_fill = self.sql(expression, "with_fill") diff --git a/tests/dialects/test_mysql.py b/tests/dialects/test_mysql.py index 3c315224dd..6f901a4d09 100644 --- a/tests/dialects/test_mysql.py +++ b/tests/dialects/test_mysql.py @@ -1760,6 +1760,56 @@ def test_ignore_respect_nulls(self): }, ) + def test_null_ordering_simulation_resolves_ordered_against_projection(self): + # NULLS LAST simulation substitutes the matching projection's sub-AST + # into the CASE so it resolves in FROM-clause scope (MySQL error 1052). + self.validate_all( + "SELECT e.employee_id FROM employees AS e LEFT JOIN employee_positions AS ep" + " ON e.employee_id = ep.employee_id" + " ORDER BY CASE WHEN e.employee_id IS NULL THEN 1 ELSE 0 END, e.employee_id", + read={ + "duckdb": ( + "SELECT e.employee_id FROM employees e" + " LEFT JOIN employee_positions ep ON e.employee_id = ep.employee_id" + " ORDER BY employee_id" + ), + }, + ) + self.validate_all( + "SELECT e.employee_id AS emp FROM employees AS e LEFT JOIN employee_positions AS ep" + " ON TRUE ORDER BY CASE WHEN e.employee_id IS NULL THEN 1 ELSE 0 END, e.employee_id", + read={ + "duckdb": ( + "SELECT e.employee_id AS emp FROM employees e" + " LEFT JOIN employee_positions ep ON TRUE ORDER BY emp" + ), + }, + ) + self.validate_all( + "SELECT e.employee_id FROM employees AS e LEFT JOIN employee_positions AS ep ON TRUE" + " ORDER BY CASE WHEN e.employee_id IS NULL THEN 1 ELSE 0 END, e.employee_id", + read={ + "duckdb": ( + "SELECT e.employee_id FROM employees e" + " LEFT JOIN employee_positions ep ON TRUE ORDER BY e.employee_id" + ), + }, + ) + self.validate_all( + "SELECT (-1) * col AS col FROM t1 LEFT JOIN t2 USING (id)" + " ORDER BY CASE WHEN (-1) * col IS NULL THEN 1 ELSE 0 END, (-1) * col", + read={ + "duckdb": "SELECT (-1) * col AS col FROM t1 LEFT JOIN t2 USING(id) ORDER BY col", + }, + ) + self.validate_all( + "SELECT t1.x + t2.y AS s FROM t1 JOIN t2 ON t1.id = t2.id" + " ORDER BY CASE WHEN t1.x + t2.y IS NULL THEN 1 ELSE 0 END, t1.x + t2.y", + read={ + "duckdb": "SELECT t1.x + t2.y AS s FROM t1 JOIN t2 ON t1.id = t2.id ORDER BY s", + }, + ) + def test_invisible_column(self): expr = self.parse_one("CREATE TABLE t (c INT INVISIBLE)") self.assertIsNotNone(expr.find(exp.InvisibleColumnConstraint))