Repository: ibis-project/ibis

Issue:
bug(bigquery): rank with windowed order by with multiple columns lists columns in the wrong order (regression in 7.2.0, working in 7.1.0) 
### What happened?

```python
import ibis
bq = ibis.bigquery.connect(project_id="swast-scratch")
import pandas
df = pandas.DataFrame({"bool_col": [True, False, False, None, True]})
table = ibis.memtable(df)
window = ibis.window(
    order_by=[
        ibis.asc(table["bool_col"].isnull()),
        ibis.asc(table["bool_col"]),
    ],
)
rank = table["bool_col"].rank().over(window)
expr = table.select([
    rank, table["bool_col"]
])
bq.execute(expr)
print(bq.compile(expr))
```

This works fine in 7.1.0:

```
In [2]: bq.execute(expr)
Out[2]: 
   MinRank() bool_col
0          0    False
1          4     None
2          0    False
3          2     True
4          2     True

In [3]: print(bq.compile(expr))
SELECT
  (
    rank() OVER (ORDER BY t0.`bool_col` IS NULL ASC, t0.`bool_col` ASC) - 1
  ) AS `MinRank`,
  t0.`bool_col`
FROM swast-scratch._63cfa399614a54153cc386c27d6c0c6fdb249f9e._ibis_pandas_memtable_45ft72ov35gvleackhzyaenloi AS t0
```


In 7.2.0, the IS NULL column expression appears second, which changes where NULL values appear in the ranking:

```
In [3]: bq.execute(expr)
Out[3]: 
   MinRank() bool_col
0          3     True
1          1    False
2          1    False
3          0     None
4          3     True
```

```sql
SELECT
  (
    -- IS NULL column expression should appear first. :-(
    rank() OVER (ORDER BY t0.`bool_col` ASC, t0.`bool_col` IS NULL ASC) - 1
  ) AS `MinRank`,
  t0.`bool_col`
FROM swast-scratch._63cfa399614a54153cc386c27d6c0c6fdb249f9e.ibis_pandas_memtable_ctis2cnl65dplgtuqh7eqozqwy AS t0
```

### What version of ibis are you using?

7.2.0

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

BigQuery

### Relevant log output

_No response_

### Code of Conduct

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


Discussion:

Resolving patch:
diff --git a/ibis/expr/analysis.py b/ibis/expr/analysis.py
--- a/ibis/expr/analysis.py
+++ b/ibis/expr/analysis.py
@@ -182,7 +182,14 @@ def merge_windows(_, default_frame):
     group_by = tuple(toolz.unique(_.frame.group_by + default_frame.group_by))
 
     order_by = {}
-    for sort_key in _.frame.order_by + default_frame.order_by:
+    # iterate in the order of the existing keys followed by the new keys
+    #
+    # this allows duplicates to be overridden with no effect on the original
+    # position
+    #
+    # see https://github.com/ibis-project/ibis/issues/7940 for how this
+    # originally manifested
+    for sort_key in default_frame.order_by + _.frame.order_by:
         order_by[sort_key.expr] = sort_key.ascending
     order_by = tuple(ops.SortKey(k, v) for k, v in order_by.items())
 
diff --git a/ibis/expr/types/generic.py b/ibis/expr/types/generic.py
--- a/ibis/expr/types/generic.py
+++ b/ibis/expr/types/generic.py
@@ -765,7 +765,6 @@ def bind(table):
                 )
             return expr
 
-        op = self.op()
         if isinstance(window, bl.WindowBuilder):
             if table := an.find_first_base_table(self.op()):
                 return bind(table)


Test patch:
diff --git a/ibis/backends/tests/test_window.py b/ibis/backends/tests/test_window.py
--- a/ibis/backends/tests/test_window.py
+++ b/ibis/backends/tests/test_window.py
@@ -1251,3 +1251,36 @@ def test_rank_followed_by_over_call_merge_frames(backend, alltypes, df):
     )
 
     backend.assert_series_equal(result, expected)
+
+
+@pytest.mark.notyet(
+    ["pandas", "dask"],
+    reason="multiple ordering keys in a window function not supported for ranking",
+    raises=ValueError,
+)
+@pytest.mark.notyet(
+    ["mssql"],
+    reason="IS NULL not valid syntax for mssql",
+    raises=sa.exc.ProgrammingError,
+)
+@pytest.mark.notimpl(["polars"], raises=com.OperationNotDefinedError)
+@pytest.mark.notyet(["flink"], raises=com.UnsupportedOperationError)
+@pytest.mark.broken(
+    ["pyspark"], reason="pyspark requires CURRENT ROW", raises=PySparkAnalysisException
+)
+def test_ordering_order(con):
+    table = ibis.memtable({"bool_col": [True, False, False, None, True]})
+    window = ibis.window(
+        order_by=[
+            ibis.asc(table["bool_col"].isnull()),
+            ibis.asc(table["bool_col"]),
+        ],
+    )
+    expr = table.select(
+        rank=table["bool_col"].rank().over(window),
+        bool_col=table["bool_col"],
+    )
+
+    result = con.execute(expr)
+    value = result.bool_col.loc[result["rank"] == 4].item()
+    assert pd.isna(value)