Repository: ibis-project/ibis

Issue:
bug: `first` returns different values across backends
### What happened?

I would expect `first` (or `last`) of a table column to return a singular value and not a pd.Series of the same value. 

``` python
import os
import sqlite3

import ibis
import pandas as pd


def create_sqlite(df, name):
    filename = "tmp.sqlite"
    os.remove(filename)
    con = sqlite3.Connection(filename)
    df.to_sql(name, con, index=False)
    return ibis.sqlite.connect(filename).table(name)


df = pd.DataFrame(
    {"string": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]},
)
sqlite_table = create_sqlite(df, "table")
pandas_table = ibis.pandas.connect({"df": df}).table("df")

pandas_table["string"], sqlite_table["string"]
pandas_table["string"].first().execute()
sqlite_table["string"].first().execute()
```

![image](https://user-images.githubusercontent.com/19758978/204336980-e5cfa5fb-674b-492c-9119-abdc3f19e449.png)


### What version of ibis are you using?

3.2.0

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

Pandas and Sqlite

### Relevant log output

_No response_

### Code of Conduct

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


Discussion:
Hi @Hoxbro, thanks for the issue!

The `first` and `last` APIs are based on the SQL window functions `FIRST_VALUE` and `LAST_VALUE`, which produce columns.

It sounds like you want `first` to handle both cases (single value in an aggregate setting, and window behavior in a window setting).

We have https://github.com/ibis-project/ibis/issues/4149 for that, so I'm going to close this out!

Thanks again for reporting!
@jcrist Pointed out to me that this is a slightly different issue: the backend behaviors don't match here. I think we can address this issue without addressing #4149, so I'll reopen this one.
Hey, I want to take this for a spin. Could you clarify which of the outputs is the correct one? I think it should be the pandas one, but not sure. Thanks! 
@mesejo Great!

The SQLite behavior is the correct one, because `first` currently maps to SQL's `first_value` window function.

Resolving patch:
diff --git a/ibis/backends/pandas/execution/window.py b/ibis/backends/pandas/execution/window.py
--- a/ibis/backends/pandas/execution/window.py
+++ b/ibis/backends/pandas/execution/window.py
@@ -500,7 +500,7 @@ def execute_series_lead_lag_timedelta(
 
 @execute_node.register(ops.FirstValue, pd.Series)
 def execute_series_first_value(op, data, **kwargs):
-    return data.values[0]
+    return data.iloc[np.repeat(0, len(data))]
 
 
 def _getter(x: pd.Series | np.ndarray, idx: int):
@@ -514,7 +514,7 @@ def execute_series_group_by_first_value(op, data, aggcontext=None, **kwargs):
 
 @execute_node.register(ops.LastValue, pd.Series)
 def execute_series_last_value(op, data, **kwargs):
-    return data.values[-1]
+    return data.iloc[np.repeat(-1, len(data))]
 
 
 @execute_node.register(ops.LastValue, SeriesGroupBy)


Test patch:
diff --git a/ibis/backends/pandas/tests/execution/test_window.py b/ibis/backends/pandas/tests/execution/test_window.py
--- a/ibis/backends/pandas/tests/execution/test_window.py
+++ b/ibis/backends/pandas/tests/execution/test_window.py
@@ -107,13 +107,15 @@ def test_lag_delta(t, df, range_offset, default, range_window):
 def test_first(t, df):
     expr = t.dup_strings.first()
     result = expr.execute()
-    assert result == df.dup_strings.iloc[0]
+    expected = df.dup_strings.iloc[np.repeat(0, len(df))].reset_index(drop=True)
+    tm.assert_series_equal(result, expected)
 
 
 def test_last(t, df):
     expr = t.dup_strings.last()
     result = expr.execute()
-    assert result == df.dup_strings.iloc[-1]
+    expected = df.dup_strings.iloc[np.repeat(-1, len(df))].reset_index(drop=True)
+    tm.assert_series_equal(result, expected)
 
 
 def test_group_by_mutate_analytic(t, df):