Repository: ibis-project/ibis

Issue:
bug(sql): base SQL backend discards column names
For a ColumnExpr that isn't a TableColumn, the base SQL backend unconditionally uses the name 'tmp' for the resulting query, rather than retaining the name 

```
>>> import ibis.backends.base.sql
>>> simple_table = ibis.table(
...         [
...             ('int_col', 'int32'),
...         ],
...         'simple_table',
...     )
>>> expr = (simple_table.int_col + 3).name('foo')
>>> ibis.backends.base.sql.Compiler().to_sql(expr)
'SELECT `int_col` + 3 AS `tmp`\nFROM simple_table'
```

It seems like this should result in `'SELECT `int_col` + 3 AS `foo`\nFROM simple_table'`


Discussion:

Resolving patch:
diff --git a/ibis/backends/base/sql/compiler/select_builder.py b/ibis/backends/base/sql/compiler/select_builder.py
--- a/ibis/backends/base/sql/compiler/select_builder.py
+++ b/ibis/backends/base/sql/compiler/select_builder.py
@@ -317,8 +317,11 @@ def column_handler(results):
                 # Something more complicated.
                 base_table = L.find_source_table(expr)
 
-                table_expr = base_table.projection([expr.name('tmp')])
-                result_handler = _get_column('tmp')
+                if not expr.has_name():
+                    expr = expr.name('tmp')
+
+                table_expr = base_table.projection([expr])
+                result_handler = _get_column(expr.get_name())
 
             return table_expr, result_handler
         else:
diff --git a/ibis/backends/pyspark/__init__.py b/ibis/backends/pyspark/__init__.py
--- a/ibis/backends/pyspark/__init__.py
+++ b/ibis/backends/pyspark/__init__.py
@@ -177,10 +177,11 @@ def execute(
             return self.compile(expr, timecontext, params, **kwargs).toPandas()
         elif isinstance(expr, types.ColumnExpr):
             # expression must be named for the projection
-            expr = expr.name('tmp')
+            if not expr.has_name():
+                expr = expr.name("tmp")
             return self.compile(
                 expr.to_projection(), timecontext, params, **kwargs
-            ).toPandas()['tmp']
+            ).toPandas()[expr.get_name()]
         elif isinstance(expr, types.ScalarExpr):
             compiled = self.compile(expr, timecontext, params, **kwargs)
             if isinstance(compiled, Column):


Test patch:
diff --git a/ibis/backends/clickhouse/tests/test_select.py b/ibis/backends/clickhouse/tests/test_select.py
--- a/ibis/backends/clickhouse/tests/test_select.py
+++ b/ibis/backends/clickhouse/tests/test_select.py
@@ -192,13 +192,14 @@ def test_complex_array_expr_projection(db, alltypes):
     expr2 = expr.string_col.cast('double')
 
     query = ibis.clickhouse.compile(expr2)
-    expected = """SELECT CAST(`string_col` AS Float64) AS `tmp`
+    name = expr2.get_name()
+    expected = f"""SELECT CAST(`string_col` AS Float64) AS `{name}`
 FROM (
   SELECT `string_col`, count(*) AS `count`
-  FROM {0}.`functional_alltypes`
+  FROM {db.name}.`functional_alltypes`
   GROUP BY `string_col`
 ) t0"""
-    assert query == expected.format(db.name)
+    assert query == expected
 
 
 @pytest.mark.parametrize(
diff --git a/ibis/backends/postgres/tests/test_functions.py b/ibis/backends/postgres/tests/test_functions.py
--- a/ibis/backends/postgres/tests/test_functions.py
+++ b/ibis/backends/postgres/tests/test_functions.py
@@ -1546,12 +1546,13 @@ def test_string_to_binary_cast(con):
     t = con.table('functional_alltypes').limit(10)
     expr = t.string_col.cast('binary')
     result = expr.execute()
+    name = expr.get_name()
     sql_string = (
-        "SELECT decode(string_col, 'escape') AS tmp "
+        f"SELECT decode(string_col, 'escape') AS \"{name}\" "
         "FROM functional_alltypes LIMIT 10"
     )
     raw_data = [row[0][0] for row in con.raw_sql(sql_string).fetchall()]
-    expected = pd.Series(raw_data, name='tmp')
+    expected = pd.Series(raw_data, name=name)
     tm.assert_series_equal(result, expected)
 
 
@@ -1559,11 +1560,14 @@ def test_string_to_binary_round_trip(con):
     t = con.table('functional_alltypes').limit(10)
     expr = t.string_col.cast('binary').cast('string')
     result = expr.execute()
+    name = expr.get_name()
     sql_string = (
-        "SELECT encode(decode(string_col, 'escape'), 'escape') AS tmp "
+        "SELECT encode(decode(string_col, 'escape'), 'escape') AS "
+        f"\"{name}\""
         "FROM functional_alltypes LIMIT 10"
     )
     expected = pd.Series(
-        [row[0][0] for row in con.raw_sql(sql_string).fetchall()], name='tmp'
+        [row[0][0] for row in con.raw_sql(sql_string).fetchall()],
+        name=name,
     )
     tm.assert_series_equal(result, expected)
diff --git a/ibis/backends/tests/test_temporal.py b/ibis/backends/tests/test_temporal.py
--- a/ibis/backends/tests/test_temporal.py
+++ b/ibis/backends/tests/test_temporal.py
@@ -29,10 +29,9 @@ def test_date_extract(backend, alltypes, df, attr, expr_fn):
     expr = getattr(expr_fn(alltypes.timestamp_col), attr)()
     expected = getattr(df.timestamp_col.dt, attr).astype('int32')
 
-    result = expr.execute()
-    expected = backend.default_series_rename(expected)
+    result = expr.name(attr).execute()
 
-    backend.assert_series_equal(result, expected)
+    backend.assert_series_equal(result, expected.rename(attr))
 
 
 @pytest.mark.parametrize(
@@ -51,11 +50,11 @@ def test_date_extract(backend, alltypes, df, attr, expr_fn):
 @pytest.mark.notimpl(["datafusion"])
 def test_timestamp_extract(backend, alltypes, df, attr):
     method = getattr(alltypes.timestamp_col, attr)
-    expr = method()
+    expr = method().name(attr)
     result = expr.execute()
     expected = backend.default_series_rename(
         getattr(df.timestamp_col.dt, attr.replace('_', '')).astype('int32')
-    )
+    ).rename(attr)
     backend.assert_series_equal(result, expected)
 
 
@@ -66,7 +65,7 @@ def test_timestamp_extract_milliseconds(backend, alltypes, df):
     result = expr.execute()
     expected = backend.default_series_rename(
         (df.timestamp_col.dt.microsecond // 1_000).astype('int32')
-    )
+    ).rename("millisecond")
     backend.assert_series_equal(result, expected)
 
 
diff --git a/ibis/tests/sql/test_compiler.py b/ibis/tests/sql/test_compiler.py
--- a/ibis/tests/sql/test_compiler.py
+++ b/ibis/tests/sql/test_compiler.py
@@ -363,3 +363,31 @@ def test_subquery_where_location():
   GROUP BY 1
 ) t0"""
     assert result == expected
+
+
+def test_column_expr_retains_name():
+    t = ibis.table(
+        [
+            ('int_col', 'int32'),
+        ],
+        'int_col_table',
+    )
+    named_derived_expr = (t.int_col + 4).name('foo')
+    result = Compiler.to_sql(named_derived_expr)
+    expected = 'SELECT `int_col` + 4 AS `foo`\nFROM int_col_table'
+
+    assert result == expected
+
+
+def test_column_expr_default_name():
+    t = ibis.table(
+        [
+            ('int_col', 'int32'),
+        ],
+        'int_col_table',
+    )
+    named_derived_expr = t.int_col + 4
+    result = Compiler.to_sql(named_derived_expr)
+    expected = 'SELECT `int_col` + 4 AS `tmp`\nFROM int_col_table'
+
+    assert result == expected
diff --git a/ibis/tests/sql/test_non_tabular_results.py b/ibis/tests/sql/test_non_tabular_results.py
--- a/ibis/tests/sql/test_non_tabular_results.py
+++ b/ibis/tests/sql/test_non_tabular_results.py
@@ -106,7 +106,7 @@ def test_complex_array_expr_projection(alltypes):
 
     query = Compiler.to_sql(expr2)
     expected = """\
-SELECT CAST(`g` AS double) AS `tmp`
+SELECT CAST(`g` AS double) AS `cast(g, float64)`
 FROM (
   SELECT `g`, count(*) AS `count`
   FROM alltypes