fix(macros): keep SAFE_ADD, SAFE_SUB and SAFE_DIV grouped inside surrounding operators - #6074
Rodrigo-Palma wants to merge 1 commit into
Conversation
…ounding operators The optimizer collapses the CASE built by SAFE_ADD/SAFE_SUB into its ELSE branch without parentheses, so an enclosing operator swallows an operand. Emit the ELSE arithmetic as an explicit Paren. SAFE_DIV now returns its quotient wrapped in a Paren so x / @SAFE_DIV(a, b) keeps its grouping. Closes SQLMesh#5649 Signed-off-by: Rodrigo-Palma <email.rodrigopalma@gmail.com>
|
Upstream now has a fix for the root cause. tobymao/sqlglot#8384, against the issue I filed there (tobymao/sqlglot#8380), parenthesizes the promoted branch when a collapsed Two things that don't change because of it:
Three ways to go, whichever you prefer:
Happy to do any of the three. |
|
Correcting my comment above: tobymao/sqlglot#8384 was closed without merging, about four hours after I linked it. georgesittas closed it with "I don't think this is the right approach. Closing and will take this on myself." My issue there (tobymao/sqlglot#8380) is still open, and there is no replacement PR open at the moment. I checked the current state rather than assuming, against
So the root cause is still live upstream, with no version to wait for: no open PR, and the maintainer reimplementing it on his own schedule. That removes the timing argument from my third option (close this and track it behind the bump), since there is nothing concrete to track it behind. The rest of the comment stands. |
|
Update: the root cause is fixed upstream. georgesittas opened tobymao/sqlglot#8389 himself and merged it today (2026-09-19, I verified it rather than taking the title for it, on sqlglot
All four are correct now, so the timing argument I withdrew yesterday is back: there is something concrete to wait for. Two things still hold:
So the same three options, with the third one now viable:
Whichever you prefer. Note that CI here never ran: the workflows are sitting in |
|
Ran this repository's full CI on my fork, on the same commit as this PR ( https://github.com/Rodrigo-Palma/sqlmesh/actions/runs/35449729558 28 jobs, all green: This supersedes the last line of my previous comment, which said CI had never run here. |
Description
Closes #5649.
@SAFE_ADDand@SAFE_SUBbuildCASE WHEN <all fields> IS NULL THEN NULL ELSE <arithmetic> END. When a field is a literal, the optimizer resolves1 IS NULLto false and sqlglot'ssimplify_conditionalsreplaces the whole CASE with its ELSE branch. The branch is inserted as a bareSub/Addnode, so an enclosing operator swallows its left operand. Parentheses written around the macro call don't help:simplify_parensdrops them in the same pass because the node they wrap is still a CASE at that point.With the model from the issue, the projection renders as
and DuckDB returns 99.8, 175 and 489 instead of 80, 175 and 0.
The collapse itself is sqlglot behaviour and reproduces without SQLMesh on 30.8.0 (the current pin), 30.18.0 and sqlglot main:
That deserves an upstream fix (wrapping the promoted branch when its new parent is an operator), and it also affects hand written CASE expressions. This PR makes the macros correct under the current pin regardless of that: the ELSE arithmetic is emitted as an explicit
Paren, which is what the AST should hold for a grouped value, and the optimizer keeps it wherever it is needed.@SAFE_DIVhad a related defect that does not involve the optimizer. It returns a bareDiv, and after the evaluator serializes and reparses the query, the quotient is no longer a single operand:x / @SAFE_DIV(a, b)rendered asx / a / NULLIF(b, 0), which is(x / a) / b. The macro now returns the quotient wrapped in aParen.Output changes to be aware of:
@SAFE_ADD/@SAFE_SUBrenderELSE (COALESCE(a, 0) + COALESCE(b, 0)) ENDin every usage.@SAFE_DIVused as a standalone projection keeps its outer parentheses when the query is not optimized (for example, dependencies missing from the schema). The optimizer removes them otherwise.The doctests and the three examples in
docs/concepts/macros/sqlmesh_macros.mdwere updated to the new output.I also tried adding the parentheses generically in
MacroEvaluator.transformfor any binary result placed under an operator. It changes unrelated output (for exampletest_merge_filter_macrogains parentheses around a predicate) and would need full precedence handling, so it is left out of this fix.Test Plan
test_safe_arithmetic_macros_keep_precedence_after_optimizationintests/core/test_macros.py, parametrized over the issue example and sibling cases (x * @SAFE_SUB(1, y),-@SAFE_SUB(1, y),x - @SAFE_ADD(1, y),x * @SAFE_ADD(1, y), nested@SAFE_DIV(@SAFE_SUB(...), x),x / @SAFE_DIV(...),x * @SAFE_DIV(...), plus two standalone usages). It renders a model through the optimizer and asserts the projection SQL. 9 of the 10 cases fail on main; the remaining standalone@SAFE_DIVcase passes on both and guards against the optimizer keeping redundant parentheses.make py-style: ruff, ruff-format, mypy and migrations check pass.tests/core/test_macros.py: 152 passed (143 passed, 9 failed against main).make doc-test: 29 passed.make fast-test: 2634 passed, 4 skipped, then 5, 1 and 161 passed in the isolated steps.make cicd-test(Python 3.12, macOS): 3393 passed, 22 skipped in the main step. The isolated, registry and dialect steps pass (10, 1, 161). The pyspark step passes (3) onceSPARK_HOMEpoints at pyspark's bundled Spark instead of a missing local install. It fails the same way on main with that local setting.Checklist
make styleand fixed any issuesmake fast-test)git commit -s) per the DCO