Repository: ibis-project/ibis

Issue:
TypeError when calling `.order_by(ibis.desc(ibis._.x))`
Context: the `ibis._` deferred attribute resolver was introduced in #3804. However it does not always interact with other ibis constructs as one would have expected:

Reproducer:

The following works as expected (ascending `order_by` on deferred column name):

```python
>>> import pandas as pd
... import ibis
... from ibis import _
... 
... table = ibis.memtable(pd.DataFrame({"x": list("abbac"), "y": range(5)}))
... (
...    table
...    .group_by(_.x)
...    .aggregate(mean_y=_.y.mean())
...    .order_by(_.mean_y)
... ).execute()
   x  mean_y
0  a     1.5
1  b     1.5
2  c     4.0
```

but combining this with `ibis.desc` leads to:

```python
>>> (
...    table
...    .group_by(_.x)
...    .aggregate(mean_y=_.y.mean())
...    .order_by(ibis.desc(_.mean_y))
... ).execute()
Traceback (most recent call last):
  Input In [38] in <cell line: 2>
    .order_by(ibis.desc(_.mean_y))
  File ~/code/ibis/ibis/expr/types/relations.py:479 in order_by
    return self.op().order_by(by).to_expr()
  File ~/code/ibis/ibis/expr/operations/relations.py:474 in order_by
    keys = rlz.tuple_of(rlz.sort_key_from(rlz.just(self)), sort_exprs)
  File ~/mambaforge/envs/dev/lib/python3.10/site-packages/toolz/functoolz.py:304 in __call__
    return self._partial(*args, **kwargs)
  File ~/code/ibis/ibis/common/validators.py:166 in container_of
    return type(inner(item, **kwargs) for item in arg)
  File ~/code/ibis/ibis/common/validators.py:166 in <genexpr>
    return type(inner(item, **kwargs) for item in arg)
  File ~/code/ibis/ibis/expr/rules.py:88 in __call__
    result = super().__call__(*args, **kwargs)
  File ~/mambaforge/envs/dev/lib/python3.10/site-packages/toolz/functoolz.py:304 in __call__
    return self._partial(*args, **kwargs)
  File ~/code/ibis/ibis/expr/rules.py:126 in sort_key_from
    key = function_of(table_ref, key, **kwargs)
  File ~/code/ibis/ibis/expr/rules.py:88 in __call__
    result = super().__call__(*args, **kwargs)
  File ~/mambaforge/envs/dev/lib/python3.10/site-packages/toolz/functoolz.py:304 in __call__
    return self._partial(*args, **kwargs)
  File ~/code/ibis/ibis/expr/rules.py:518 in function_of
    arg = fn.resolve(arg)
  File ~/code/ibis/ibis/expr/deferred.py:32 in resolve
    resolved_expr = self.resolve(expr)
  File ~/code/ibis/ibis/expr/deferred.py:32 in resolve
    resolved_expr = self.resolve(expr)
  File ~/code/ibis/ibis/expr/deferred.py:37 in resolve
    return func(resolved_expr, *resolved_args, **resolved_kwargs)
  File ~/code/ibis/ibis/expr/deferred.py:49 in <lambda>
    lambda expr, *args, **kwargs: expr(*args, **kwargs),
TypeError: 'FloatingColumn' object is not callable
```

Replacing the deferred resolver by the str name works:

```python
>>> (
...    table
...    .group_by(_.x)
...    .aggregate(mean_y=_.y.mean())
...    .order_by(ibis.desc("mean_y"))
... ).execute()
   x  mean_y
0  c     4.0
1  a     1.5
2  b     1.5
```


Discussion:
Actually the `group_by` is not necessary. Here is a simpler reproducer:

```python
>>> import pandas as pd
... import ibis
... from ibis import _
... 
... ibis.memtable(pd.DataFrame({"x": range(5)})).order_by(ibis.desc(_.x)).execute()
Traceback (most recent call last):
  Input In [45] in <cell line: 5>
    ibis.memtable(pd.DataFrame({"x": range(5)})).order_by(ibis.desc(_.x)).execute()
  File ~/code/ibis/ibis/expr/types/relations.py:479 in order_by
    return self.op().order_by(by).to_expr()
  File ~/code/ibis/ibis/expr/operations/relations.py:30 in order_by
    return Selection(self, [], sort_keys=sort_exprs)
  File ~/code/ibis/ibis/common/grounds.py:26 in __call__
    return cls.__create__(*args, **kwargs)
  File ~/code/ibis/ibis/common/grounds.py:105 in __create__
    kwargs = cls.__signature__.validate(*args, **kwargs)
  File ~/code/ibis/ibis/common/annotations.py:271 in validate
    this[name] = param.validate(value, this=this)
  File ~/code/ibis/ibis/common/annotations.py:148 in validate
    return self.annotation(arg, this=this)
  File ~/mambaforge/envs/dev/lib/python3.10/site-packages/toolz/functoolz.py:304 in __call__
    return self._partial(*args, **kwargs)
  File ~/code/ibis/ibis/common/validators.py:83 in option
    return inner(arg, **kwargs)
  File ~/code/ibis/ibis/expr/rules.py:88 in __call__
    result = super().__call__(*args, **kwargs)
  File ~/mambaforge/envs/dev/lib/python3.10/site-packages/toolz/functoolz.py:304 in __call__
    return self._partial(*args, **kwargs)
  File ~/code/ibis/ibis/expr/rules.py:106 in nodes_of
    values = tuple_of(inner, arg, **kwargs)
  File ~/mambaforge/envs/dev/lib/python3.10/site-packages/toolz/functoolz.py:304 in __call__
    return self._partial(*args, **kwargs)
  File ~/code/ibis/ibis/common/validators.py:166 in container_of
    return type(inner(item, **kwargs) for item in arg)
  File ~/code/ibis/ibis/common/validators.py:166 in <genexpr>
    return type(inner(item, **kwargs) for item in arg)
  File ~/code/ibis/ibis/expr/rules.py:88 in __call__
    result = super().__call__(*args, **kwargs)
  File ~/mambaforge/envs/dev/lib/python3.10/site-packages/toolz/functoolz.py:304 in __call__
    return self._partial(*args, **kwargs)
  File ~/code/ibis/ibis/expr/rules.py:126 in sort_key_from
    key = function_of(table_ref, key, **kwargs)
  File ~/code/ibis/ibis/expr/rules.py:88 in __call__
    result = super().__call__(*args, **kwargs)
  File ~/mambaforge/envs/dev/lib/python3.10/site-packages/toolz/functoolz.py:304 in __call__
    return self._partial(*args, **kwargs)
  File ~/code/ibis/ibis/expr/rules.py:518 in function_of
    arg = fn.resolve(arg)
  File ~/code/ibis/ibis/expr/deferred.py:32 in resolve
    resolved_expr = self.resolve(expr)
  File ~/code/ibis/ibis/expr/deferred.py:32 in resolve
    resolved_expr = self.resolve(expr)
  File ~/code/ibis/ibis/expr/deferred.py:37 in resolve
    return func(resolved_expr, *resolved_args, **resolved_kwargs)
  File ~/code/ibis/ibis/expr/deferred.py:49 in <lambda>
    lambda expr, *args, **kwargs: expr(*args, **kwargs),
TypeError: 'IntegerColumn' object is not callable
```



Hi @ogrisel, thanks for reporting this.  I can confirm this is an issue with `ibis.desc()`.  In the meantime, this seems to be a functional workaround:

`t.order_by(_.x.desc())`
Thanks, I did not know about `.desc()` on expressions. This seems cleaner that using the module level function in this case.

Resolving patch:
diff --git a/ibis/expr/api.py b/ibis/expr/api.py
--- a/ibis/expr/api.py
+++ b/ibis/expr/api.py
@@ -6,7 +6,7 @@
 import functools
 import itertools
 import operator
-from typing import Iterable, Mapping, Sequence
+from typing import Iterable, Literal, Mapping, Sequence
 from typing import Tuple as _Tuple
 from typing import TypeVar
 from typing import Union as _Union
@@ -481,6 +481,19 @@ def _memtable_from_dataframe(
     return op.to_expr()
 
 
+def _sort_order(expr, order: Literal["desc"] | Literal["asc"]):
+    method = operator.methodcaller(order)
+    if isinstance(expr, str):
+        value = _[expr]
+    elif isinstance(expr, Deferred):
+        value = expr
+    elif callable(expr):
+        value = expr(_)
+    else:
+        value = expr
+    return method(value)
+
+
 def desc(expr: ir.Column | str) -> ir.Value:
     """Create a descending sort key from `expr` or column name.
 
@@ -510,12 +523,7 @@ def desc(expr: ir.Column | str) -> ir.Value:
     ir.ValueExpr
         An expression
     """
-    if isinstance(expr, str):
-        return _[expr].desc()
-    elif callable(expr):
-        return expr(_).desc()
-    else:
-        return expr.desc()
+    return _sort_order(expr, "desc")
 
 
 def asc(expr: ir.Column | str) -> ir.Value:
@@ -547,12 +555,7 @@ def asc(expr: ir.Column | str) -> ir.Value:
     ir.ValueExpr
         An expression
     """
-    if isinstance(expr, str):
-        return _[expr].asc()
-    elif callable(expr):
-        return expr(_).asc()
-    else:
-        return expr.asc()
+    return _sort_order(expr, "asc")
 
 
 def and_(*predicates: ir.BooleanValue) -> ir.BooleanValue:


Test patch:
diff --git a/ibis/tests/expr/test_value_exprs.py b/ibis/tests/expr/test_value_exprs.py
--- a/ibis/tests/expr/test_value_exprs.py
+++ b/ibis/tests/expr/test_value_exprs.py
@@ -1649,3 +1649,12 @@ def test_case_rlz_incompatible_error(table):
         table.g.case().when(case == result, result).end()
 
     assert "a:int8 and Literal(foo):string" in str(exc_info.value)
+
+
+@pytest.mark.parametrize("func", [ibis.asc, ibis.desc])
+def test_group_by_order_by_deferred(func):
+    from ibis import _
+
+    table = ibis.table(dict(x="string", y="int"), name="t")
+    expr = table.group_by(_.x).aggregate(mean_y=_.y.mean()).order_by(func(_.mean_y))
+    assert isinstance(expr, ir.Table)