Repository: tobymao/sqlglot

Issue:
Optimizer alias expansion does not wrap expanded expressions
**Is your feature request related to a problem? Please describe.**
Lateral alias / derived column expansion interpolates the column SQL as-is. This results in surprising behavior as it messes up operator precedence which should be consistent **per expression**. When using this feature the alias should be equivalent to the result of the derived column. 

**Describe the solution you'd like**
Lateral alias expansion should wrap the interpolated column in parentheses.

**Describe alternatives you've considered**
Wrap things myself to ensure intended precedence. Error prone.

**Additional context**
```sql
select
  a.x + a.y as f,
  f * a.z
from a
```
Expands to
```sql
select
  a.x + a.y as f,
  a.x + a.y * a.z
from a
```

To get consistent results as other databases (snowflake, teradata, etc) you would require this, which is more correct
```sql
select
  a.x + a.y as f,
  (a.x + a.y) * a.z
from a
```

I can say this has bitten me in the foot already leaning on this optimizer feature in bigquery. 


Discussion:
Thanks for reporting this, Alex. I'll take a look shortly.

Resolving patch:
diff --git a/sqlglot/optimizer/qualify_columns.py b/sqlglot/optimizer/qualify_columns.py
--- a/sqlglot/optimizer/qualify_columns.py
+++ b/sqlglot/optimizer/qualify_columns.py
@@ -9,6 +9,7 @@
 from sqlglot.errors import OptimizeError
 from sqlglot.helper import seq_get
 from sqlglot.optimizer.scope import Scope, traverse_scope, walk_in_scope
+from sqlglot.optimizer.simplify import simplify_parens
 from sqlglot.schema import Schema, ensure_schema
 
 
@@ -183,6 +184,7 @@ def replace_columns(
         for column, *_ in walk_in_scope(node):
             if not isinstance(column, exp.Column):
                 continue
+
             table = resolver.get_table(column.name) if resolve_table and not column.table else None
             alias_expr, i = alias_to_expression.get(column.name, (None, 1))
             double_agg = (
@@ -198,7 +200,10 @@ def replace_columns(
                     if literal_index:
                         column.replace(exp.Literal.number(i))
                 else:
-                    column.replace(alias_expr.copy())
+                    column = column.replace(exp.paren(alias_expr))
+                    simplified = simplify_parens(column)
+                    if simplified is not column:
+                        column.replace(simplified)
 
     for i, projection in enumerate(scope.expression.selects):
         replace_columns(projection)


Test patch:
diff --git a/tests/fixtures/optimizer/qualify_columns.sql b/tests/fixtures/optimizer/qualify_columns.sql
--- a/tests/fixtures/optimizer/qualify_columns.sql
+++ b/tests/fixtures/optimizer/qualify_columns.sql
@@ -125,7 +125,7 @@ SELECT COALESCE(x.a) AS d FROM x JOIN y ON x.b = y.b GROUP BY d;
 SELECT COALESCE(x.a) AS d FROM x AS x JOIN y AS y ON x.b = y.b GROUP BY COALESCE(x.a);
 
 SELECT a + 1 AS d FROM x WHERE d > 1;
-SELECT x.a + 1 AS d FROM x AS x WHERE x.a + 1 > 1;
+SELECT x.a + 1 AS d FROM x AS x WHERE (x.a + 1) > 1;
 
 # execute: false
 SELECT a + 1 AS d, d + 2 FROM x;
@@ -444,7 +444,7 @@ SELECT x.a AS a, x.b AS b FROM x AS x QUALIFY COUNT(x.a) OVER (PARTITION BY x.b)
 -- Expand laterals
 --------------------------------------
 # execute: false
-select 2 AS d, d + 1 FROM x WHERE d = 2 GROUP BY d;
+SELECT 2 AS d, d + 1 FROM x WHERE d = 2 GROUP BY d;
 SELECT 2 AS d, 2 + 1 AS _col_1 FROM x AS x WHERE 2 = 2 GROUP BY 1;
 
 # title: expand alias reference
@@ -471,6 +471,16 @@ FROM (
 );
 SELECT _q_0.i AS i, _q_0.j AS j FROM (SELECT x.a + 1 AS i, x.a + 1 + 1 AS j FROM x AS x) AS _q_0;
 
+# title: wrap expanded alias to ensure operator precedence isn't broken
+# execute: false
+SELECT x.a + x.b AS f, f * x.b FROM x;
+SELECT x.a + x.b AS f, (x.a + x.b) * x.b AS _col_1 FROM x AS x;
+
+# title: no need to wrap expanded alias
+# execute: false
+SELECT x.a + x.b AS f, f, f + 5 FROM x;
+SELECT x.a + x.b AS f, x.a + x.b AS _col_1, x.a + x.b + 5 AS _col_2 FROM x AS x;
+
 --------------------------------------
 -- Wrapped tables / join constructs
 --------------------------------------
diff --git a/tests/test_optimizer.py b/tests/test_optimizer.py
--- a/tests/test_optimizer.py
+++ b/tests/test_optimizer.py
@@ -291,7 +291,7 @@ def test_expand_alias_refs(self):
         # check order of lateral expansion with no schema
         self.assertEqual(
             optimizer.optimize("SELECT a + 1 AS d, d + 1 AS e FROM x WHERE e > 1 GROUP BY e").sql(),
-            'SELECT "x"."a" + 1 AS "d", "x"."a" + 1 + 1 AS "e" FROM "x" AS "x" WHERE "x"."a" + 2 > 1 GROUP BY "x"."a" + 1 + 1',
+            'SELECT "x"."a" + 1 AS "d", "x"."a" + 1 + 1 AS "e" FROM "x" AS "x" WHERE ("x"."a" + 2) > 1 GROUP BY "x"."a" + 1 + 1',
         )
 
         self.assertEqual(