simplify: collapsing a CASE under an arithmetic operator drops the grouping of the branch
When simplify resolves a CASE to one of its branches, the branch replaces the CASE without parentheses. If the CASE is an operand of an arithmetic operator and the branch is itself a binary expression, the result changes meaning.
Fully reproducible code snippet
Checked on main (19077cc):
from sqlglot import parse_one
from sqlglot.optimizer.simplify import simplify
for sql in [
"SELECT x * CASE WHEN FALSE THEN NULL ELSE a - b END FROM t",
"SELECT x * (CASE WHEN 1 IS NULL THEN NULL ELSE 1 - b END) FROM t",
"SELECT x - CASE WHEN TRUE THEN a + b ELSE c END FROM t",
]:
print(simplify(parse_one(sql, read="duckdb")).sql(dialect="duckdb"))Output:
SELECT x * a - b FROM t
SELECT x * 1 - b FROM t
SELECT x - a + b FROM tExpected:
SELECT x * (a - b) FROM t
SELECT x * (1 - b) FROM t
SELECT x - (a + b) FROM tWith values, DuckDB gives SELECT 2 * CASE WHEN FALSE THEN NULL ELSE 10 - 5 END = 10, while the simplified SELECT 2 * 10 - 5 = 15.
The second case shows that parentheses written around the CASE don't help: they are removed while the node is still a CASE, and the CASE is collapsed afterwards.
Context
Found through SQLMesh's @SAFE_SUB macro, which builds CASE WHEN ... IS NULL THEN NULL ELSE a - b END. When an argument is a literal, the rendered model computes the wrong value (SQLMesh/sqlmesh#5649). SQLMesh can work around it by emitting an explicit paren around the branch (SQLMesh/sqlmesh#6074), but hand written CASE expressions hit the same thing.
Official Documentation
Operator precedence (PostgreSQL, same rules in DuckDB): https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-PRECEDENCE
Source: tobymao/sqlglot