Repository: ibis-project/ibis

Issue:
bug: inner Deferred binds to wrong thing in `Table.select(_.x.map(_.b))`
### What happened?

See https://github.com/ibis-project/ibis/pull/8552

I think what is happening:
1. ibis encounters the _.x when evaluating the .select
2. it tries to bind it to the parent expression, the Table
3. then it recurses down into the deferred to further evaluate it
4. It encounters the _.b
5. It isn't smart enough to understand that this now refers to each array element. It just binds it to that outer Table
6. raises AttributeError

This seems like it might be a doozy to solve. Would need to make the idea of a context much more nuanced, with nested scopes??

### What version of ibis are you using?

main

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

_No response_

### Relevant log output

_No response_

### Code of Conduct

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

Discussion:

Patch:
diff --git a/.github/workflows/check-generated-files.yml b/.github/workflows/check-generated-files.yml
index 9b3e5d3a9..f9da5cd1c 100644
--- a/.github/workflows/check-generated-files.yml
+++ b/.github/workflows/check-generated-files.yml
@@ -31,7 +31,7 @@ jobs:
         uses: actions/checkout@v4
 
       - name: install poetry
-        run: pipx install 'poetry==1.8.1'
+        run: pipx install 'poetry==1.8.2'
 
       - name: install python
         uses: actions/setup-python@v5
diff --git a/.github/workflows/ibis-backends-cloud.yml b/.github/workflows/ibis-backends-cloud.yml
index 18f552cd1..ed9d9f5a3 100644
--- a/.github/workflows/ibis-backends-cloud.yml
+++ b/.github/workflows/ibis-backends-cloud.yml
@@ -87,7 +87,7 @@ jobs:
           github_token: ${{ steps.generate_token.outputs.token }}
 
       - name: install poetry
-        run: pipx install 'poetry==1.8.1'
+        run: pipx install 'poetry==1.8.2'
 
       - name: install python
         uses: actions/setup-python@v5
diff --git a/.github/workflows/ibis-backends.yml b/.github/workflows/ibis-backends.yml
index 0cafdd05e..ee1745faf 100644
--- a/.github/workflows/ibis-backends.yml
+++ b/.github/workflows/ibis-backends.yml
@@ -366,7 +366,7 @@ jobs:
         run: docker compose up --wait ${{ join(matrix.backend.services, ' ') }}
 
       - name: install poetry
-        run: pipx install 'poetry==1.8.1'
+        run: pipx install 'poetry==1.8.2'
 
       - name: install python
         uses: actions/setup-python@v5
@@ -507,7 +507,7 @@ jobs:
           python-version: ${{ matrix.python-version }}
 
       - name: install poetry
-        run: python -m pip install --upgrade pip 'poetry==1.8.1'
+        run: python -m pip install --upgrade pip 'poetry==1.8.2'
 
       - name: remove lonboard
         # it requires a version of pandas that min versions are not compatible with
@@ -591,7 +591,7 @@ jobs:
           python-version: ${{ matrix.python-version }}
 
       - name: install poetry
-        run: python -m pip install --upgrade pip 'poetry==1.8.1'
+        run: python -m pip install --upgrade pip 'poetry==1.8.2'
 
       - name: remove lonboard
         # it requires a version of pandas that pyspark is not compatible with
diff --git a/.github/workflows/ibis-benchmarks.yml b/.github/workflows/ibis-benchmarks.yml
index a7652fa52..ec2a509db 100644
--- a/.github/workflows/ibis-benchmarks.yml
+++ b/.github/workflows/ibis-benchmarks.yml
@@ -24,7 +24,7 @@ jobs:
         uses: actions/checkout@v4
 
       - name: install poetry
-        run: pipx install 'poetry==1.8.1'
+        run: pipx install 'poetry==1.8.2'
 
       - name: install python
         uses: actions/setup-python@v5
diff --git a/.github/workflows/ibis-main.yml b/.github/workflows/ibis-main.yml
index 72265e34f..98ce30361 100644
--- a/.github/workflows/ibis-main.yml
+++ b/.github/workflows/ibis-main.yml
@@ -55,7 +55,7 @@ jobs:
         uses: actions/checkout@v4
 
       - name: install poetry
-        run: pipx install 'poetry==1.8.1'
+        run: pipx install 'poetry==1.8.2'
 
       - name: install python
         uses: actions/setup-python@v5
@@ -106,7 +106,7 @@ jobs:
         uses: actions/checkout@v4
 
       - name: install poetry
-        run: pipx install 'poetry==1.8.1'
+        run: pipx install 'poetry==1.8.2'
 
       - name: install python
         uses: actions/setup-python@v5
@@ -146,7 +146,7 @@ jobs:
         uses: actions/checkout@v4
 
       - name: install poetry
-        run: pipx install 'poetry==1.8.1'
+        run: pipx install 'poetry==1.8.2'
 
       - name: install python
         uses: actions/setup-python@v5
diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml
index 1d129a5b6..ab7b53bec 100644
--- a/.github/workflows/pre-release.yml
+++ b/.github/workflows/pre-release.yml
@@ -33,7 +33,7 @@ jobs:
         run: python -m pip install --upgrade pip
 
       - name: install poetry
-        run: python -m pip install 'poetry==1.8.1' poetry-dynamic-versioning
+        run: python -m pip install 'poetry==1.8.2' poetry-dynamic-versioning
 
       - name: compute ibis version
         id: get_version
diff --git a/compose.yaml b/compose.yaml
index 22e4d1190..7982114de 100644
--- a/compose.yaml
+++ b/compose.yaml
@@ -156,7 +156,7 @@ services:
       test:
         - CMD-SHELL
         - trino --output-format null --execute 'show schemas in hive; show schemas in memory'
-    image: trinodb/trino:439
+    image: trinodb/trino:440
     ports:
       - 8080:8080
     networks:
diff --git a/docs/contribute/01_environment.qmd b/docs/contribute/01_environment.qmd
index 8b7f9d382..b4a95bf1d 100644
--- a/docs/contribute/01_environment.qmd
+++ b/docs/contribute/01_environment.qmd
@@ -193,7 +193,7 @@ For a better development experience see the `conda/mamba` or `nix` setup instruc
 1. Install development dependencies
 
    ```sh
-   pip install 'poetry==1.8.1'
+   pip install 'poetry==1.8.2'
    pip install -r requirements-dev.txt
    ```
 
diff --git a/flake.lock b/flake.lock
index 71a2d5fbd..b8b05dc84 100644
--- a/flake.lock
+++ b/flake.lock
@@ -61,11 +61,11 @@
     },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1709455474,
-        "narHash": "sha256-UzfJcBjXUySRokQiI8QQZp/Xpneiu7A5iaLGU09NiQ0=",
+        "lastModified": 1709943332,
+        "narHash": "sha256-MCdZ9lXvCSTg23WUo9+uK9Zcw+VykCiYqF6x8Vy1Dyo=",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "768bb879887ae70e62d7628c82d825bbbf873b4b",
+        "rev": "b046a81451dc41d818e8f22736e7d5e8d7d67699",
         "type": "github"
       },
       "original": {
diff --git a/ibis/expr/types/arrays.py b/ibis/expr/types/arrays.py
index 9eb6f9410..6d526061b 100644
--- a/ibis/expr/types/arrays.py
+++ b/ibis/expr/types/arrays.py
@@ -445,15 +445,19 @@ class ArrayValue(Value):
         """
         if isinstance(func, Deferred):
             name = "_"
-        else:
+            resolve = func.resolve
+        elif callable(func):
             name = next(iter(inspect.signature(func).parameters.keys()))
+            resolve = func
+        else:
+            raise TypeError(
+                f"`func` must be a Deferred or Callable, got `{type(func).__name__}`"
+            )
+
         parameter = ops.Argument(
             name=name, shape=self.op().shape, dtype=self.type().value_type
         )
-        if isinstance(func, Deferred):
-            body = func.resolve(parameter.to_expr())
-        else:
-            body = func(parameter.to_expr())
+        body = resolve(parameter.to_expr())
         return ops.ArrayMap(self, param=parameter.param, body=body).to_expr()
 
     def filter(
@@ -545,17 +549,20 @@ class ArrayValue(Value):
         """
         if isinstance(predicate, Deferred):
             name = "_"
-        else:
+            resolve = predicate.resolve
+        elif callable(predicate):
             name = next(iter(inspect.signature(predicate).parameters.keys()))
+            resolve = predicate
+        else:
+            raise TypeError(
+                f"`predicate` must be a Deferred or Callable, got `{type(predicate).__name__}`"
+            )
         parameter = ops.Argument(
             name=name,
             shape=self.op().shape,
             dtype=self.type().value_type,
         )
-        if isinstance(predicate, Deferred):
-            body = predicate.resolve(parameter.to_expr())
-        else:
-            body = predicate(parameter.to_expr())
+        body = resolve(parameter.to_expr())
         return ops.ArrayFilter(self, param=parameter.param, body=body).to_expr()
 
     def contains(self, other: ir.Value) -> ir.BooleanValue:
diff --git a/justfile b/justfile
index 27929b3ae..174986a8e 100644
--- a/justfile
+++ b/justfile
@@ -9,7 +9,7 @@ clean:
 # lock dependencies without updating existing versions
 lock:
     #!/usr/bin/env bash
-    required_version="1.8.1"
+    required_version="1.8.2"
     version="$(poetry --version)"
     if ! grep -qF "${required_version}" <<< "${version}"; then
         >&2 echo "poetry version must be ${required_version}, got ${version}"
diff --git a/poetry.lock b/poetry.lock
index b8d59d270..28d473db4 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 1.8.1 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
 
 [[package]]
 name = "aiohttp"
@@ -4342,6 +4342,8 @@ files = [
     {file = "psycopg2-2.9.9-cp310-cp310-win_amd64.whl", hash = "sha256:426f9f29bde126913a20a96ff8ce7d73fd8a216cfb323b1f04da402d452853c3"},
     {file = "psycopg2-2.9.9-cp311-cp311-win32.whl", hash = "sha256:ade01303ccf7ae12c356a5e10911c9e1c51136003a9a1d92f7aa9d010fb98372"},
     {file = "psycopg2-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:121081ea2e76729acfb0673ff33755e8703d45e926e416cb59bae3a86c6a4981"},
+    {file = "psycopg2-2.9.9-cp312-cp312-win32.whl", hash = "sha256:d735786acc7dd25815e89cc4ad529a43af779db2e25aa7c626de864127e5a024"},
+    {file = "psycopg2-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:a7653d00b732afb6fc597e29c50ad28087dcb4fbfb28e86092277a559ae4e693"},
     {file = "psycopg2-2.9.9-cp37-cp37m-win32.whl", hash = "sha256:5e0d98cade4f0e0304d7d6f25bbfbc5bd186e07b38eac65379309c4ca3193efa"},
     {file = "psycopg2-2.9.9-cp37-cp37m-win_amd64.whl", hash = "sha256:7e2dacf8b009a1c1e843b5213a87f7c544b2b042476ed7755be813eaf4e8347a"},
     {file = "psycopg2-2.9.9-cp38-cp38-win32.whl", hash = "sha256:ff432630e510709564c01dafdbe996cb552e0b9f3f065eb89bdce5bd31fabf4c"},


Test patch:
diff --git a/ibis/tests/expr/test_value_exprs.py b/ibis/tests/expr/test_value_exprs.py
index aec143083..140cf4e03 100644
--- a/ibis/tests/expr/test_value_exprs.py
+++ b/ibis/tests/expr/test_value_exprs.py
@@ -1533,52 +1533,36 @@ def test_array_length_scalar():
     assert isinstance(expr.op(), ops.ArrayLength)
 
 
-def double_int(x):
-    return x * 2
-
-
-def double_float(x):
-    return x * 2.0
-
-
-def is_negative(x):
-    return x < 0
-
-
 def test_array_map():
     arr = ibis.array([1, 2, 3])
 
-    result_int = arr.map(double_int)
-    result_float = arr.map(double_float)
+    r1 = arr.map(_ * 2)
+    r2 = arr.map(lambda x: x * 2.0)
+    r3 = arr.map(functools.partial(lambda a, b: a + b, b=2))
 
-    assert result_int.type() == dt.Array(dt.int16)
-    assert result_float.type() == dt.Array(dt.float64)
+    assert r1.type() == dt.Array(dt.int16)
+    assert r2.type() == dt.Array(dt.float64)
+    assert r3.type() == dt.Array(dt.int16)
 
-
-def test_array_map_partial():
-    arr = ibis.array([1, 2, 3])
-
-    def add(x, y):
-        return x + y
-
-    result = arr.map(functools.partial(add, y=2))
-    assert result.type() == dt.Array(dt.int16)
+    with pytest.raises(TypeError, match="must be a Deferred or Callable"):
+        # Non-deferred expressions aren't allowed
+        arr.map(arr[0])
 
 
 def test_array_filter():
     arr = ibis.array([1, 2, 3])
-    result = arr.filter(is_negative)
-    assert result.type() == arr.type()
-
 
-def test_array_filter_partial():
-    arr = ibis.array([1, 2, 3])
+    r1 = arr.filter(lambda x: x < 0)
+    r2 = arr.filter(_ < 0)
+    r3 = arr.filter(functools.partial(lambda a, b: a == b, b=2))
 
-    def equal(x, y):
-        return x == y
+    assert r1.type() == arr.type()
+    assert r2.type() == arr.type()
+    assert r3.type() == arr.type()
 
-    result = arr.filter(functools.partial(equal, y=2))
-    assert result.type() == arr.type()
+    with pytest.raises(TypeError, match="must be a Deferred or Callable"):
+        # Non-deferred expressions aren't allowed
+        arr.filter(arr[0])
 
 
 @pytest.mark.parametrize(