Repository: ibis-project/ibis

Issue:
feat: better error on `Table[list[bogus]]`
### Is your feature request related to a problem?

This isn't a huge problem, but a UX nice-to-have.

Passing in a bad element raises a not very clear assertion error:
```python
t = ibis.examples.penguins.fetch()
t[["species", ("island",)]]
```

```python-traceback
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
Cell In[100], line 1
----> 1 t[["species", ("island",)]]

File [~/Documents/projects/scg/social/.venv/lib/python3.11/site-packages/ibis/expr/types/relations.py:546](https://file+.vscode-resource.vscode-cdn.net/Users/nickcrews/Documents/projects/scg/social/notebooks/~/Documents/projects/scg/social/.venv/lib/python3.11/site-packages/ibis/expr/types/relations.py:546), in Table.__getitem__(self, what)
    542 what = bind_expr(self, what)
    544 if isinstance(what, (list, tuple, Table)):
    545     # Projection case
--> 546     return self.select(what)
    547 elif isinstance(what, BooleanValue):
    548     # Boolean predicate
    549     return self.filter([what])

File [~/Documents/projects/scg/social/.venv/lib/python3.11/site-packages/ibis/expr/types/relations.py:1698](https://file+.vscode-resource.vscode-cdn.net/Users/nickcrews/Documents/projects/scg/social/notebooks/~/Documents/projects/scg/social/.venv/lib/python3.11/site-packages/ibis/expr/types/relations.py:1698), in Table.select(self, *exprs, **named_exprs)
   1693 if not exprs:
   1694     raise com.IbisTypeError(
   1695         "You must select at least one column for a valid projection"
   1696     )
-> 1698 op = an.Projector(self, exprs).get_result()
   1700 return op.to_expr()

File [~/Documents/projects/scg/social/.venv/lib/python3.11/site-packages/ibis/expr/analysis.py:528](https://file+.vscode-resource.vscode-cdn.net/Users/nickcrews/Documents/projects/scg/social/notebooks/~/Documents/projects/scg/social/.venv/lib/python3.11/site-packages/ibis/expr/analysis.py:528), in Projector.__init__(self, parent, proj_exprs)
    525 self.resolved_exprs = [parent._ensure_expr(e) for e in proj_exprs]
    527 default_frame = ops.RowsWindowFrame(table=parent)
--> 528 self.clean_exprs = [
    529     windowize_function(expr, frame=default_frame)
    530     for expr in self.resolved_exprs
    531 ]

File [~/Documents/projects/scg/social/.venv/lib/python3.11/site-packages/ibis/expr/analysis.py:529](https://file+.vscode-resource.vscode-cdn.net/Users/nickcrews/Documents/projects/scg/social/notebooks/~/Documents/projects/scg/social/.venv/lib/python3.11/site-packages/ibis/expr/analysis.py:529), in (.0)
    525 self.resolved_exprs = [parent._ensure_expr(e) for e in proj_exprs]
    527 default_frame = ops.RowsWindowFrame(table=parent)
    528 self.clean_exprs = [
--> 529     windowize_function(expr, frame=default_frame)
    530     for expr in self.resolved_exprs
    531 ]

File [~/Documents/projects/scg/social/.venv/lib/python3.11/site-packages/ibis/expr/analysis.py:411](https://file+.vscode-resource.vscode-cdn.net/Users/nickcrews/Documents/projects/scg/social/notebooks/~/Documents/projects/scg/social/.venv/lib/python3.11/site-packages/ibis/expr/analysis.py:411), in windowize_function(expr, frame)
    410 def windowize_function(expr, frame):
--> 411     assert isinstance(expr, ir.Expr), type(expr)
    412     assert isinstance(frame, ops.WindowFrame)
    414     def _windowize(op, frame):

AssertionError:
```

### Describe the solution you'd like

A nicer, more actionable error

### What version of ibis are you running?

6.1.0

### What backend(s) are you using, if any?

NA

### Code of Conduct

- [X] I agree to follow this project's Code of Conduct


Discussion:
Thanks for opening this @NickCrews! I can confirm this issue, we can definitely validate inputs earlier and raise a better error here.

Resolving patch:
diff --git a/ibis/expr/types/relations.py b/ibis/expr/types/relations.py
--- a/ibis/expr/types/relations.py
+++ b/ibis/expr/types/relations.py
@@ -1710,23 +1710,29 @@ def select(
         import ibis.expr.analysis as an
         from ibis.selectors import Selector
 
-        exprs = list(
-            itertools.chain(
-                itertools.chain.from_iterable(
-                    util.promote_list(e.expand(self) if isinstance(e, Selector) else e)
-                    for e in exprs
-                ),
-                (
-                    self._ensure_expr(expr).name(name)
-                    for name, expr in named_exprs.items()
-                ),
+        exprs = [
+            e
+            for expr in exprs
+            for e in (
+                expr.expand(self)
+                if isinstance(expr, Selector)
+                else map(self._ensure_expr, util.promote_list(expr))
             )
+        ]
+        exprs.extend(
+            self._ensure_expr(expr).name(name) for name, expr in named_exprs.items()
         )
 
         if not exprs:
             raise com.IbisTypeError(
                 "You must select at least one column for a valid projection"
             )
+        for ex in exprs:
+            if not isinstance(ex, Expr):
+                raise com.IbisTypeError(
+                    "All arguments to `.select` must be coerceable to "
+                    f"expressions - got {type(ex)!r}"
+                )
 
         op = an.Projector(self, exprs).get_result()
 


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
@@ -243,6 +243,16 @@ def test_projection_no_expr(table, empty):
         table.select(empty)
 
 
+def test_projection_invalid_nested_list(table):
+    errmsg = "must be coerceable to expressions"
+    with pytest.raises(com.IbisTypeError, match=errmsg):
+        table.select(["a", ["b"]])
+    with pytest.raises(com.IbisTypeError, match=errmsg):
+        table[["a", ["b"]]]
+    with pytest.raises(com.IbisTypeError, match=errmsg):
+        table["a", ["b"]]
+
+
 def test_mutate(table):
     expr = table.mutate(
         [