Repository: narwhals-dev/narwhals

Issue:
[Bug]: should `_create_native_series` return narwhals series?
### Describe the bug

I was trying to increase coverage by adding a test that calls [`ArrowNamespace._create_native_series`](https://github.com/narwhals-dev/narwhals/blob/main/narwhals/_arrow/namespace.py#L87): such method is accessed in `_expression_parsing.py` module [`parse_into_expr` function](https://github.com/narwhals-dev/narwhals/blob/main/narwhals/_expression_parsing.py#L153), and deals with the case of receiving a numpy array.

The result is fed into `namespace._create_expr_from_series`, which from type hint seems to expect a narwhals Series (ArrowSeries, PandasLikeSeries, and so on).

So far for pandas it resulted in no issues - my assumption is because numpy arrays implements most of the methods of the Series, or at least those that it is been used in.

However this is not true for pyarrow array: as one can check by running the `all_horizontal` test in the branch I am working on ([link](https://github.com/narwhals-dev/narwhals/blob/enh/increae-test-coverage/tests/expr/all_horizontal_test.py#L10))

### Steps or code to reproduce the bug

```py
import numpy as np
import narwhals as nw
import pyarrow as pa
import pandas as pd

arr = np.array([False, False, True]
data = {"b": [False, True, True]}

df_pd = pd.DataFrame(data)
df_pa = pa.table(data)

@nw.narwhalify
def func(dframe):
    return dframe.select(all=nw.all_horizontal(arr, nw.col("b")))

func(df_pd) # no errors
func(df_pa) # AttributeError: 'pyarrow.lib.ChunkedArray' object has no attribute '__and__'. Did you mean: '__ne__'?
```

### Expected results

```py
pa.table({"all": [False, False, True]})
```

### Actual results

> AttributeError: 'pyarrow.lib.ChunkedArray' object has no attribute '__and__'. Did you mean: '__ne__'?

### Please run narwhals.show_version() and enter the output below.

```shell
Python dependencies:
     narwhals: 1.0.6
       pandas: 2.2.2
       polars: 1.0.0
         cudf: 
        modin: 0.31.0
      pyarrow: 16.1.0
        numpy: 2.0.0
```


### Relevant log output

_No response_

Discussion:

Patch:
diff --git a/narwhals/_arrow/dataframe.py b/narwhals/_arrow/dataframe.py
index 768fcb32..b3ef89fc 100644
--- a/narwhals/_arrow/dataframe.py
+++ b/narwhals/_arrow/dataframe.py
@@ -3,6 +3,7 @@ from __future__ import annotations
 from typing import TYPE_CHECKING
 from typing import Any
 from typing import Iterable
+from typing import Literal
 from typing import Sequence
 from typing import overload
 
@@ -183,6 +184,41 @@ class ArrowDataFrame:
         df = self._native_dataframe.__class__.from_arrays(to_concat, names=output_names)
         return self._from_native_dataframe(df)
 
+    def join(
+        self,
+        other: Self,
+        *,
+        how: Literal["inner"] = "inner",
+        left_on: str | list[str] | None,
+        right_on: str | list[str] | None,
+    ) -> Self:
+        if isinstance(left_on, str):
+            left_on = [left_on]
+        if isinstance(right_on, str):
+            right_on = [right_on]
+
+        if how == "cross":  # type: ignore[comparison-overlap]
+            raise NotImplementedError
+
+        if how == "anti":  # type: ignore[comparison-overlap]
+            raise NotImplementedError
+
+        if how == "semi":  # type: ignore[comparison-overlap]
+            raise NotImplementedError
+
+        if how == "left":  # type: ignore[comparison-overlap]
+            raise NotImplementedError
+
+        return self._from_native_dataframe(
+            self._native_dataframe.join(
+                other._native_dataframe,
+                keys=left_on,
+                right_keys=right_on,
+                join_type=how,
+                right_suffix="_right",
+            ),
+        )
+
     def drop(self, *columns: str | Iterable[str]) -> Self:
         return self._from_native_dataframe(
             self._native_dataframe.drop(list(flatten(columns)))
diff --git a/narwhals/_arrow/namespace.py b/narwhals/_arrow/namespace.py
index eb7107f3..306ecb77 100644
--- a/narwhals/_arrow/namespace.py
+++ b/narwhals/_arrow/namespace.py
@@ -84,9 +84,15 @@ class ArrowNamespace:
             backend_version=self._backend_version,
         )
 
-    def _create_native_series(self, value: Any) -> Any:  # pragma: no cover (todo!)
+    def _create_compliant_series(self, value: Any) -> ArrowSeries:
+        from narwhals._arrow.series import ArrowSeries
+
         pa = get_pyarrow()
-        return pa.chunked_array([value])
+        return ArrowSeries(
+            native_series=pa.chunked_array([value]),
+            name="",
+            backend_version=self._backend_version,
+        )
 
     # --- not in spec ---
     def __init__(self, *, backend_version: tuple[int, ...]) -> None:
diff --git a/narwhals/_expression_parsing.py b/narwhals/_expression_parsing.py
index 14801534..726e3bb6 100644
--- a/narwhals/_expression_parsing.py
+++ b/narwhals/_expression_parsing.py
@@ -150,8 +150,8 @@ def parse_into_expr(
     if isinstance(into_expr, str):
         return namespace.col(into_expr)
     if (np := get_numpy()) is not None and isinstance(into_expr, np.ndarray):
-        series = namespace._create_native_series(into_expr)
-        return namespace._create_expr_from_series(series)
+        series = namespace._create_compliant_series(into_expr)
+        return namespace._create_expr_from_series(series)  # type: ignore[arg-type]
     msg = f"Expected IntoExpr, got {type(into_expr)}"  # pragma: no cover
     raise AssertionError(msg)
 
diff --git a/narwhals/_pandas_like/namespace.py b/narwhals/_pandas_like/namespace.py
index da237aa0..dbae3bbb 100644
--- a/narwhals/_pandas_like/namespace.py
+++ b/narwhals/_pandas_like/namespace.py
@@ -97,7 +97,7 @@ class PandasLikeNamespace:
             backend_version=self._backend_version,
         )
 
-    def _create_native_series(self, value: Any) -> Any:
+    def _create_compliant_series(self, value: Any) -> PandasLikeSeries:
         return create_native_series(
             value,
             implementation=self._implementation,
diff --git a/utils/check_backend_completeness.py b/utils/check_backend_completeness.py
index 2ca15a69..0cd6335b 100644
--- a/utils/check_backend_completeness.py
+++ b/utils/check_backend_completeness.py
@@ -18,7 +18,6 @@ MISSING = [
     "DataFrame.is_unique",
     "DataFrame.item",
     "DataFrame.iter_rows",
-    "DataFrame.join",
     "DataFrame.pipe",
     "DataFrame.rename",
     "DataFrame.unique",


Test patch:
diff --git a/tests/frame/join_test.py b/tests/frame/join_test.py
index c248a081..ebe7bacc 100644
--- a/tests/frame/join_test.py
+++ b/tests/frame/join_test.py
@@ -11,21 +11,20 @@ from narwhals._pandas_like.utils import Implementation
 from tests.utils import compare_dicts
 
 
-def test_inner_join_two_keys(constructor: Any) -> None:
+def test_inner_join_two_keys(constructor_with_pyarrow: Any) -> None:
     data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
-    df = nw.from_native(constructor(data), eager_only=True)
+    df = nw.from_native(constructor_with_pyarrow(data), eager_only=True)
     df_right = df
     result = df.join(df_right, left_on=["a", "b"], right_on=["a", "b"], how="inner")
     expected = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9], "z_right": [7.0, 8, 9]}
     compare_dicts(result, expected)
 
 
-def test_inner_join_single_key(constructor: Any) -> None:
+def test_inner_join_single_key(constructor_with_pyarrow: Any) -> None:
     data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
-    df = nw.from_native(constructor(data), eager_only=True)
+    df = nw.from_native(constructor_with_pyarrow(data), eager_only=True)
     df_right = df
     result = df.join(df_right, left_on="a", right_on="a", how="inner")
-    result_native = nw.to_native(result)
     expected = {
         "a": [1, 3, 2],
         "b": [4, 4, 6],
@@ -33,7 +32,7 @@ def test_inner_join_single_key(constructor: Any) -> None:
         "z": [7.0, 8, 9],
         "z_right": [7.0, 8, 9],
     }
-    compare_dicts(result_native, expected)
+    compare_dicts(result, expected)
 
 
 def test_cross_join(constructor: Any) -> None:
diff --git a/tests/frame/with_columns_sequence_test.py b/tests/frame/with_columns_sequence_test.py
index d44a493d..0801e6c0 100644
--- a/tests/frame/with_columns_sequence_test.py
+++ b/tests/frame/with_columns_sequence_test.py
@@ -11,7 +11,12 @@ data = {
 }
 
 
-def test_with_columns(constructor: Any) -> None:
-    result = nw.from_native(constructor(data)).with_columns(d=np.array([4, 5]))
-    expected = {"a": ["foo", "bars"], "ab": ["foo", "bars"], "d": [4, 5]}
+def test_with_columns(constructor_with_pyarrow: Any) -> None:
+    result = (
+        nw.from_native(constructor_with_pyarrow(data))
+        .with_columns(d=np.array([4, 5]))
+        .with_columns(e=nw.col("d") + 1)
+        .select("d", "e")
+    )
+    expected = {"d": [4, 5], "e": [5, 6]}
     compare_dicts(result, expected)