Repository: ibis-project/ibis

Issue:
bug: assertion error after `filter` with `literal`, then `.distinct(on=...)`
### What happened?

Wow, there are a lot of pre-conditions necessary for this bug to crop up!

```python
import ibis

ex = ibis.examples.aids.fetch()
f = [
    ibis.literal(True),  # no assertion error if this isn't used
    (ex.delay > 5),
]
filtered = ex.filter(f)  # works
filtered.distinct()  # works
filtered.distinct(on="quarter")  # fails
```

### What version of ibis are you using?

5.0.0

### What backend(s) are you using, if any?

duckdb

### Relevant log output

```python stacktrace
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
Cell In[28], line 10
      8 filtered = ex.filter(f)  # works
      9 filtered.distinct()  # works
---> 10 filtered.distinct(on="quarter")  # fails

File [~/Library/Application](https://file+.vscode-resource.vscode-cdn.net/Users/nickcrews/Documents/projects/scg/alatna/.scratch/~/Library/Application) Support[/hatch/env/virtual/alatna/U4QVXPcf/alatna/lib/python3.10/site-packages/ibis/expr/types/relations.py:1082](https://file+.vscode-resource.vscode-cdn.net/hatch/env/virtual/alatna/U4QVXPcf/alatna/lib/python3.10/site-packages/ibis/expr/types/relations.py:1082), in Table.distinct(self, on, keep)
   1080 if having is not None:
   1081     gb = gb.having(having)
-> 1082 res = gb.agg(**aggs)
   1084 assert len(res.columns) == len(self.columns)
   1085 if res.columns != self.columns:

File [~/Library/Application](https://file+.vscode-resource.vscode-cdn.net/Users/nickcrews/Documents/projects/scg/alatna/.scratch/~/Library/Application) Support[/hatch/env/virtual/alatna/U4QVXPcf/alatna/lib/python3.10/site-packages/ibis/expr/types/groupby.py:104](https://file+.vscode-resource.vscode-cdn.net/hatch/env/virtual/alatna/U4QVXPcf/alatna/lib/python3.10/site-packages/ibis/expr/types/groupby.py:104), in GroupedTable.aggregate(self, metrics, **kwds)
    102 def aggregate(self, metrics=None, **kwds):
    103     """Compute aggregates over a group by."""
--> 104     return self.table.aggregate(metrics, by=self.by, having=self._having, **kwds)

File [~/Library/Application](https://file+.vscode-resource.vscode-cdn.net/Users/nickcrews/Documents/projects/scg/alatna/.scratch/~/Library/Application) Support[/hatch/env/virtual/alatna/U4QVXPcf/alatna/lib/python3.10/site-packages/ibis/expr/types/relations.py:915](https://file+.vscode-resource.vscode-cdn.net/hatch/env/virtual/alatna/U4QVXPcf/alatna/lib/python3.10/site-packages/ibis/expr/types/relations.py:915), in Table.aggregate(self, metrics, by, having, **kwargs)
    893 metrics = itertools.chain(
    894     itertools.chain.from_iterable(
    895         (
   (...)
    906     ),
    907 )
    909 agg = ops.Aggregation(
    910     self,
    911     metrics=list(metrics),
    912     by=util.promote_list(by),
    913     having=util.promote_list(having),
    914 )
--> 915 agg = an.simplify_aggregation(agg)
    917 return agg.to_expr()

File [~/Library/Application](https://file+.vscode-resource.vscode-cdn.net/Users/nickcrews/Documents/projects/scg/alatna/.scratch/~/Library/Application) Support[/hatch/env/virtual/alatna/U4QVXPcf/alatna/lib/python3.10/site-packages/ibis/expr/analysis.py:462](https://file+.vscode-resource.vscode-cdn.net/hatch/env/virtual/alatna/U4QVXPcf/alatna/lib/python3.10/site-packages/ibis/expr/analysis.py:462), in simplify_aggregation(agg)
    460     if metrics_valid and by_valid and having_valid:
    461         valid_lowered_sort_keys = frozenset(lowered_metrics).union(lowered_by)
--> 462         return ops.Aggregation(
    463             agg.table.table,
    464             lowered_metrics,
    465             by=lowered_by,
    466             having=lowered_having,
    467             predicates=agg.table.predicates,
    468             # only the sort keys that exist as grouping keys or metrics can
    469             # be included
    470             sort_keys=[
    471                 key
    472                 for key in agg.table.sort_keys
    473                 if key.expr in valid_lowered_sort_keys
    474             ],
    475         )
    477 return agg

File [~/Library/Application](https://file+.vscode-resource.vscode-cdn.net/Users/nickcrews/Documents/projects/scg/alatna/.scratch/~/Library/Application) Support[/hatch/env/virtual/alatna/U4QVXPcf/alatna/lib/python3.10/site-packages/ibis/common/grounds.py:25](https://file+.vscode-resource.vscode-cdn.net/hatch/env/virtual/alatna/U4QVXPcf/alatna/lib/python3.10/site-packages/ibis/common/grounds.py:25), in BaseMeta.__call__(cls, *args, **kwargs)
     24 def __call__(cls, *args, **kwargs) -> Base:
---> 25     return cls.__create__(*args, **kwargs)

File [~/Library/Application](https://file+.vscode-resource.vscode-cdn.net/Users/nickcrews/Documents/projects/scg/alatna/.scratch/~/Library/Application) Support[/hatch/env/virtual/alatna/U4QVXPcf/alatna/lib/python3.10/site-packages/ibis/common/grounds.py:99](https://file+.vscode-resource.vscode-cdn.net/hatch/env/virtual/alatna/U4QVXPcf/alatna/lib/python3.10/site-packages/ibis/common/grounds.py:99), in Annotable.__create__(cls, *args, **kwargs)
     95 @classmethod
     96 def __create__(cls, *args, **kwargs) -> Annotable:
     97     # construct the instance by passing the validated keyword arguments
     98     kwargs = cls.__signature__.validate(*args, **kwargs)
---> 99     return super().__create__(**kwargs)

File [~/Library/Application](https://file+.vscode-resource.vscode-cdn.net/Users/nickcrews/Documents/projects/scg/alatna/.scratch/~/Library/Application) Support[/hatch/env/virtual/alatna/U4QVXPcf/alatna/lib/python3.10/site-packages/ibis/common/grounds.py:33](https://file+.vscode-resource.vscode-cdn.net/hatch/env/virtual/alatna/U4QVXPcf/alatna/lib/python3.10/site-packages/ibis/common/grounds.py:33), in Base.__create__(cls, *args, **kwargs)
     31 @classmethod
     32 def __create__(cls, *args, **kwargs) -> Base:
---> 33     return type.__call__(cls, *args, **kwargs)

File [~/Library/Application](https://file+.vscode-resource.vscode-cdn.net/Users/nickcrews/Documents/projects/scg/alatna/.scratch/~/Library/Application) Support[/hatch/env/virtual/alatna/U4QVXPcf/alatna/lib/python3.10/site-packages/ibis/expr/operations/relations.py:482](https://file+.vscode-resource.vscode-cdn.net/hatch/env/virtual/alatna/U4QVXPcf/alatna/lib/python3.10/site-packages/ibis/expr/operations/relations.py:482), in Aggregation.__init__(self, table, metrics, by, having, predicates, sort_keys)
    475     raise com.RelationError(
    476         "Selection expressions don't fully originate from "
    477         "dependencies of the table expression."
    478     )
    480 # invariant due to Aggregation and AggregateSelection requiring a valid
    481 # Selection
--> 482 assert all(shares_some_roots(predicate, table) for predicate in predicates)
    484 if not by:
    485     sort_keys = tuple()

AssertionError:
```

### Code of Conduct

- [X] I agree to follow this project's Code of Conduct


Discussion:
Thanks for the nice reproducible example, I can confirm that this does reproduce the issue above. Hopefully shouldn't be too bad of a fix.

Resolving patch:
diff --git a/ibis/expr/analysis.py b/ibis/expr/analysis.py
--- a/ibis/expr/analysis.py
+++ b/ibis/expr/analysis.py
@@ -620,7 +620,8 @@ def shares_some_roots(exprs, parents):
     # unique table dependencies of exprs and parents
     exprs_deps = set(g.traverse(_find_projections, exprs))
     parents_deps = set(g.traverse(_find_projections, parents))
-    return bool(exprs_deps & parents_deps)
+    # Also return True if exprs has no roots (e.g. literal-only expressions)
+    return bool(exprs_deps & parents_deps) or not exprs_deps
 
 
 def flatten_predicate(node):


Test patch:
diff --git a/ibis/tests/expr/test_table.py b/ibis/tests/expr/test_table.py
--- a/ibis/tests/expr/test_table.py
+++ b/ibis/tests/expr/test_table.py
@@ -712,6 +712,12 @@ def test_filter_aggregate_pushdown_predicate(table):
     assert_equal(filtered, expected)
 
 
+def test_filter_on_literal_then_aggregate(table):
+    # Mostly just a smoketest, this used to error on construction
+    expr = table.filter(ibis.literal(True)).agg(lambda t: t.a.sum().name("total"))
+    assert expr.columns == ["total"]
+
+
 @pytest.mark.parametrize(
     "case_fn",
     [