Skip to content

fix(mysql)!: qualify column reference in NULLS LAST CASE simulation#7641

Merged
georgesittas merged 4 commits into
tobymao:mainfrom
brdbry:mysql-nulls-last-qualify-column
May 19, 2026
Merged

fix(mysql)!: qualify column reference in NULLS LAST CASE simulation#7641
georgesittas merged 4 commits into
tobymao:mainfrom
brdbry:mysql-nulls-last-qualify-column

Conversation

@brdbry
Copy link
Copy Markdown
Contributor

@brdbry brdbry commented May 12, 2026

Problem

When transpiling ORDER BY <col> from a NULL_ORDERING = "nulls_are_last" source dialect (e.g. DuckDB) into MySQL, the generator emits a CASE WHEN <col> IS NULL THEN 1 ELSE 0 END, <col> shim to simulate NULLS LAST (MySQL has no native NULLS FIRST/LAST syntax). The shim inlines the raw ORDER BY expression verbatim — typically a bare column name.

That's fine for a single-table query, but in a multi-table FROM where the same unqualified column name exists in more than one table, MySQL raises:

ERROR 1052 (23000): Column '<name>' in order clause is ambiguous

…even when there is an unambiguous SELECT-list projection. MySQL's ORDER BY does apply alias-first column resolution against the SELECT list — but inside the CASE expression in ORDER BY, that alias-first rule does not apply. The bare reference falls through to base-table resolution and collides.

Reproducer

from sqlglot import parse_one

src = (
    "SELECT e.employee_id FROM employees e "
    "LEFT JOIN employee_positions ep ON e.employee_id = ep.employee_id "
    "ORDER BY employee_id"
)
print(parse_one(src, read="duckdb").sql(dialect="mysql"))

Output today (rejected by MySQL 8.0):

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 employee_id IS NULL THEN 1 ELSE 0 END, employee_id

After this PR:

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

Verified against a running MySQL 8.0 container: the pre-fix SQL errors with 1052, the post-fix SQL succeeds.

Fix

New helper Generator._qualified_for_null_ordering_simulation on the base generator walks from the Ordered node to its enclosing Select and looks for a single qualified-column projection whose output_name matches the bare ORDER BY column. If exactly one is found (whether the projection is plain e.employee_id or e.employee_id AS <alias>), the simulation substitutes that qualified column inside both the CASE and the trailing sort key. In every other case (already-qualified ORDER BY, positional/ordinal ORDER BY, expression ORDER BY, no enclosing Select, ambiguous projection match, inside a window function), the helper returns None and the existing behaviour is preserved.

Single change: sqlglot/generator.py.

Test

Added TestMySQL.test_null_ordering_simulation_qualifies_ambiguous_columns in tests/dialects/test_mysql.py covering three scenarios: the reproducer (multi-table LEFT JOIN, unqualified ORDER BY), an aliased projection (SELECT e.employee_id AS emp ... ORDER BY emp), and an already-qualified ORDER BY (no change in output).

Full suite passes locally (1107 passed, 18002 subtests passed).

Scope / out-of-scope

The fix lives in the base Generator.ordered_sql simulation branch, so any dialect with NULL_ORDERING_SUPPORTED is None (currently MySQL and MSSQL/TSQL) benefits in one place. I've only added a MySQL-focused test and PR title because the reported bug is specifically the MySQL 1052 symptom; happy to broaden the test coverage to TSQL or any other affected dialect in a follow-up if maintainers want it.

The fix is deliberately conservative: it only qualifies when a single qualified-column projection unambiguously matches, and never strips or rewrites already-qualified ORDER BY references. I did not modify the parser to track explicit-vs-implicit NULL ordering on Ordered nodes (broader ripple) or add any per-dialect toggle.

Context

Same area of the codebase as #6655 (UPDATE … FROM → UPDATE … JOIN translation) which I authored earlier this year.

… avoid 1052 ambiguity

When transpiling ORDER BY <col> from a NULLS-LAST default dialect (e.g. DuckDB)
to MySQL, the simulated CASE WHEN <col> IS NULL THEN 1 ELSE 0 END, <col> inlines
the raw expression. Inside the CASE, MySQL does not apply ORDER BY's alias-first
column resolution, so unqualified references collide with same-named columns in
joined base tables (error 1052). Resolve the column against the enclosing SELECT
projection and substitute the qualified source where one uniquely exists.
@georgesittas
Copy link
Copy Markdown
Collaborator

georgesittas commented May 14, 2026

Hey @brdbry, thank you for the PR.

The transpilation bug is legit, but I don't like the solution because it is quite complicated and only solves a single instance of the problem: a simple qualified Column as the projection.

Shouldn't we replace the ORDER BY refrence with the full sub-AST corresponding to the matching projection? That way you'd also handle queries like: SELECT (-1) * col AS col FROM t1 LEFT JOIN t2 USING(id) ORDER BY col.

@georgesittas
Copy link
Copy Markdown
Collaborator

Closing this due to inactivity, feel free to reopen when ready.

… simulation

Generalises the NULLS FIRST/LAST CASE WHEN <expr> IS NULL simulation to
replace a bare ORDER BY name with the full sub-AST of the matching SELECT
projection (Alias-stripped), not just qualified-Column projections. Fixes
MySQL 1052 ambiguity and also handles expression projections aliased to
the same name (e.g. SELECT (-1) * col AS col ... ORDER BY col) which the
prior fix left unresolved. Per @georgesittas review on tobymao#7641.
@brdbry
Copy link
Copy Markdown
Contributor Author

brdbry commented May 18, 2026

Hi @georgesittas, sorry for the delay. I pushed a new commit but don't seem to be able to reopen

@georgesittas georgesittas reopened this May 18, 2026
@georgesittas
Copy link
Copy Markdown
Collaborator

No worries, happy to continue iterating. Reopened it.

@brdbry
Copy link
Copy Markdown
Contributor Author

brdbry commented May 18, 2026

Thanks - how does it look to you now - anything else you'd like to change?

@georgesittas
Copy link
Copy Markdown
Collaborator

I'll take another look soon.

Copy link
Copy Markdown
Collaborator

@georgesittas georgesittas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you verified whether the same bug exists in T-SQL?

Comment thread sqlglot/generator.py Outdated
Comment thread sqlglot/generator.py Outdated
… TSQL coverage

Per @georgesittas review on tobymao#7641:
- Window-ancestor guard removed; ORDER BY-in-window resolves in the same FROM scope, no carve-out needed (all existing window-context simulation tests still pass).
- Projection lookup rewritten as a comprehension + uniqueness check.
- TSQL test mirrors the MySQL subtests; the base-generator fix already covers it since TSQL has NULL_ORDERING_SUPPORTED = None.
@brdbry
Copy link
Copy Markdown
Contributor Author

brdbry commented May 19, 2026

thanks @georgesittas, changes pushed. test for TSQL added

Copy link
Copy Markdown
Collaborator

@georgesittas georgesittas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll get this in and clean it up. Thanks for the PR!

Comment thread sqlglot/generator.py Outdated
@georgesittas georgesittas changed the title fix(mysql): qualify column reference in NULLS LAST CASE simulation fix(mysql)!: qualify column reference in NULLS LAST CASE simulation May 19, 2026
@georgesittas georgesittas merged commit 0e9e0a4 into tobymao:main May 19, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants