Repository: pola-rs/polars (language: python)

Issue:
Confusion about `&` when mixing `cs.by_name` and `pl.col`
### Checks

- [X] I have checked that this issue has not already been reported.
- [X] I have confirmed this bug exists on the [latest version](https://pypi.org/project/polars/) of Polars.

### Reproducible example

```python
import polars as pl
import polars.selectors as cs

df = pl.DataFrame({
    "a": [1],
    "b": [2]
})

df.select(cs.by_name("a") | pl.col("b"))
# shape: (1, 2)
# ┌─────┬─────┐
# │ a   ┆ b   │
# │ --- ┆ --- │
# │ i64 ┆ i64 │
# ╞═════╪═════╡
# │ 1   ┆ 2   │
# └─────┴─────┘

df.select(cs.by_name("a") & pl.col("b"))
# shape: (1, 2)
# ┌─────┬─────┐
# │ a   ┆ b   │
# │ --- ┆ --- │
# │ i64 ┆ i64 │
# ╞═════╪═════╡
# │ 1   ┆ 2   │
# └─────┴─────┘
```


### Log output

_No response_

### Issue description

I don't understand some behavior of `&` to intersect selectors and expressions. 

In the example above, `cs.by_name("a") | pl.col("b")` returns both columns, which makes sense since both match the condition that they are either "a" or "b". My issue is that `cs.by_name("a") & pl.col("b")` also returns both columns, but IMO it shouldn't since none of the columns is "a" and "b" at the same time. This comes from this `if` condition:

https://github.com/pola-rs/polars/blob/7f0b3e00845c013222ac8b1094d693af582258c2/py-polars/polars/selectors.py#L386-L392

I understand there are some issues with selectors, e.g. https://github.com/pola-rs/polars/issues/13757. Is this also an issue or am I misunderstanding?

### Expected behavior

`df.select(cs.by_name("a") & pl.col("b"))` should return 0 cols.

### Installed versions

<details>

```
--------Version info---------
Polars:              1.12.0
Index type:          UInt32
Platform:            Linux-6.8.0-47-generic-x86_64-with-glibc2.39
Python:              3.12.3 (main, Sep 11 2024, 14:17:37) [GCC 13.2.0]
LTS CPU:             False

----Optional dependencies----
adbc_driver_manager  <not installed>
altair               <not installed>
cloudpickle          <not installed>
connectorx           <not installed>
deltalake            <not installed>
fastexcel            <not installed>
fsspec               <not installed>
gevent               <not installed>
great_tables         <not installed>
matplotlib           <not installed>
nest_asyncio         <not installed>
numpy                2.1.2
openpyxl             <not installed>
pandas               <not installed>
pyarrow              <not installed>
pydantic             <not installed>
pyiceberg            <not installed>
sqlalchemy           <not installed>
torch                <not installed>
xlsx2csv             <not installed>
xlsxwriter           <not installed>
```

</details>



Discussion / hints:
This seems like a bug to me.
> This seems like a bug to me.

Yup, looks like it - I'll go fix this one.

Fix patch:
diff --git a/py-polars/polars/selectors.py b/py-polars/polars/selectors.py
index 4cb11506b3f6..9d3cedb47e85 100644
--- a/py-polars/polars/selectors.py
+++ b/py-polars/polars/selectors.py
@@ -385,10 +385,6 @@ def __and__(self, other: Any) -> Expr: ...
     def __and__(self, other: Any) -> SelectorType | Expr:
         if is_column(other):
             colname = other.meta.output_name()
-            if self._attrs["name"] == "by_name" and (
-                params := self._attrs["params"]
-            ).get("require_all", True):
-                return by_name(*params["*names"], colname)
             other = by_name(colname)
         if is_selector(other):
             return _selector_proxy_(
@@ -399,6 +395,12 @@ def __and__(self, other: Any) -> SelectorType | Expr:
         else:
             return self.as_expr().__and__(other)
 
+    def __rand__(self, other: Any) -> Expr:
+        if is_column(other):
+            colname = other.meta.output_name()
+            return by_name(colname) & self
+        return self.as_expr().__rand__(other)
+
     @overload
     def __or__(self, other: SelectorType) -> SelectorType: ...
 
@@ -417,6 +419,11 @@ def __or__(self, other: Any) -> SelectorType | Expr:
         else:
             return self.as_expr().__or__(other)
 
+    def __ror__(self, other: Any) -> Expr:
+        if is_column(other):
+            other = by_name(other.meta.output_name())
+        return self.as_expr().__ror__(other)
+
     @overload
     def __xor__(self, other: SelectorType) -> SelectorType: ...
 
@@ -435,21 +442,6 @@ def __xor__(self, other: Any) -> SelectorType | Expr:
         else:
             return self.as_expr().__or__(other)
 
-    def __rand__(self, other: Any) -> Expr:
-        if is_column(other):
-            colname = other.meta.output_name()
-            if self._attrs["name"] == "by_name" and (
-                params := self._attrs["params"]
-            ).get("require_all", True):
-                return by_name(colname, *params["*names"])
-            other = by_name(colname)
-        return self.as_expr().__rand__(other)
-
-    def __ror__(self, other: Any) -> Expr:
-        if is_column(other):
-            other = by_name(other.meta.output_name())
-        return self.as_expr().__ror__(other)
-
     def __rxor__(self, other: Any) -> Expr:
         if is_column(other):
             other = by_name(other.meta.output_name())


Test patch:
diff --git a/py-polars/tests/unit/test_selectors.py b/py-polars/tests/unit/test_selectors.py
index dd2c415c9a13..f4e29e9194c6 100644
--- a/py-polars/tests/unit/test_selectors.py
+++ b/py-polars/tests/unit/test_selectors.py
@@ -182,11 +182,17 @@ def test_selector_by_name(df: pl.DataFrame) -> None:
 
     # check "by_name & col"
     for selector_expr, expected in (
-        (cs.by_name("abc", "cde") & pl.col("ghi"), ["abc", "cde", "ghi"]),
-        (pl.col("ghi") & cs.by_name("cde", "abc"), ["ghi", "cde", "abc"]),
+        (cs.by_name("abc", "cde") & pl.col("ghi"), []),
+        (cs.by_name("abc", "cde") & pl.col("cde"), ["cde"]),
+        (pl.col("cde") & cs.by_name("cde", "abc"), ["cde"]),
     ):
         assert df.select(selector_expr).columns == expected
 
+    # check "by_name & by_name"
+    assert df.select(
+        cs.by_name("abc", "cde", "def", "eee") & cs.by_name("cde", "eee", "fgg")
+    ).columns == ["cde", "eee"]
+
     # expected errors
     with pytest.raises(ColumnNotFoundError, match="xxx"):
         df.select(cs.by_name("xxx", "fgg", "!!!"))