Repository: ibis-project/ibis

Issue:
bug: generating apparently incorrect code when sort by non table column expression
### What happened?

```
In [55]: con = ibis.duckdb.connect("/data/kaggle/wow.ddb")

In [56]: t = con.tables.wowah_data

In [57]: print(t[t.charclass == 'Hunter'][_.guild != -1].order_by(_.guild + 1).compile())
```

Fails with

```
CompileError: Can't resolve label reference for ORDER BY / GROUP BY / DISTINCT etc. Textual SQL expression 'Add(guild, 1)' should be explicitly declared as text('Add(guild, 1)')
```

### What version of ibis are you using?

`master`

### What backend(s) are you using, if any?

DuckDB

### Relevant log output

_No response_

### Code of Conduct

- [X] I agree to follow this project's Code of Conduct


Discussion:

Resolving patch:
diff --git a/ibis/backends/base/sql/alchemy/query_builder.py b/ibis/backends/base/sql/alchemy/query_builder.py
--- a/ibis/backends/base/sql/alchemy/query_builder.py
+++ b/ibis/backends/base/sql/alchemy/query_builder.py
@@ -5,7 +5,6 @@
 import sqlalchemy as sa
 import sqlalchemy.sql as sql
 
-import ibis.expr.analysis as an
 import ibis.expr.operations as ops
 import ibis.expr.schema as sch
 from ibis.backends.base.sql.alchemy.database import AlchemyTable
@@ -138,30 +137,6 @@ def _format_table(self, op):
         return result
 
 
-def _can_lower_sort_column(table_set, expr):
-    # TODO(wesm): This code is pending removal through cleaner internal
-    # semantics
-
-    # we can currently sort by just-appeared aggregate metrics, but the way
-    # these are references in the expression DSL is as a SortBy (blocking
-    # table operation) on an aggregation. There's a hack in _collect_SortBy
-    # in the generic SQL compiler that "fuses" the sort with the
-    # aggregation so they appear in same query. It's generally for
-    # cosmetics and doesn't really affect query semantics.
-    bases = an.find_immediate_parent_tables(expr)
-    if len(bases) != 1:
-        return False
-
-    base = bases[0]
-
-    if isinstance(base, ops.Aggregation):
-        return base.table.equals(table_set)
-    elif isinstance(base, ops.Selection):
-        return base.equals(table_set)
-    else:
-        return False
-
-
 class AlchemySelect(Select):
     def __init__(self, *args, **kwargs):
         self.exists = kwargs.pop('exists', False)
@@ -309,14 +284,7 @@ def _add_order_by(self, fragment):
         clauses = []
         for key in self.order_by:
             sort_expr = key.expr
-
-            # here we have to determine if key.expr is in the select set (as it
-            # will be in the case of order_by fused with an aggregation
-            if _can_lower_sort_column(self.table_set, sort_expr):
-                arg = sort_expr.name
-            else:
-                arg = self._translate(sort_expr)
-
+            arg = self._translate(sort_expr)
             fn = sa.asc if key.ascending else sa.desc
 
             clauses.append(fn(arg))


Test patch:
diff --git a/ibis/tests/sql/snapshots/test_sqlalchemy/test_order_by_expr/out.sql b/ibis/tests/sql/snapshots/test_sqlalchemy/test_order_by_expr/out.sql
new file mode 100644
--- /dev/null
+++ b/ibis/tests/sql/snapshots/test_sqlalchemy/test_order_by_expr/out.sql
@@ -0,0 +1,4 @@
+SELECT t0.a, t0.b 
+FROM (SELECT t1.a AS a, t1.b AS b 
+FROM t AS t1 
+WHERE t1.a = 1) AS t0 ORDER BY concat(t0.b, 'a') ASC
\ No newline at end of file
diff --git a/ibis/tests/sql/test_sqlalchemy.py b/ibis/tests/sql/test_sqlalchemy.py
--- a/ibis/tests/sql/test_sqlalchemy.py
+++ b/ibis/tests/sql/test_sqlalchemy.py
@@ -1169,3 +1169,12 @@ def test_no_cart_join(con, snapshot):
 
     out = str(con.compile(ob).compile(compile_kwargs=dict(literal_binds=True)))
     snapshot.assert_match(out, "out.sql")
+
+
+def test_order_by_expr(con, snapshot):
+    from ibis import _
+
+    t = ibis.table(dict(a="int", b="string"), name="t")
+    expr = t[_.a == 1].order_by(_.b + "a")
+    out = str(con.compile(expr).compile(compile_kwargs=dict(literal_binds=True)))
+    snapshot.assert_match(out, "out.sql")