Repository: ibis-project/ibis

Issue:
SUPP: Verify requirements for rows_with_max_lookback
When `rows_with_max_lookback` is used in a window, we should verify that the window contains exactly one `order_by` column and that the `.type()` of that column is `timestamp`.


Discussion:

Resolving patch:
diff --git a/ibis/expr/operations.py b/ibis/expr/operations.py
--- a/ibis/expr/operations.py
+++ b/ibis/expr/operations.py
@@ -1025,6 +1025,15 @@ def __init__(self, expr, window):
         if table is not None:
             window = window.bind(table)
 
+        if window.max_lookback is not None:
+            error_msg = ("'max lookback' windows must be ordered "
+                         "by a timestamp column")
+            if len(window._order_by) != 1:
+                raise com.IbisInputError(error_msg)
+            order_var = window._order_by[0].op().args[0]
+            if not isinstance(order_var.type(), dt.Timestamp):
+                raise com.IbisInputError(error_msg)
+
         expr = propagate_down_window(expr, window)
         super().__init__(expr, window)
 


Test patch:
diff --git a/ibis/expr/tests/test_window_functions.py b/ibis/expr/tests/test_window_functions.py
--- a/ibis/expr/tests/test_window_functions.py
+++ b/ibis/expr/tests/test_window_functions.py
@@ -17,6 +17,7 @@
 import pytest
 
 import ibis
+import ibis.common as com
 from ibis.expr.window import _determine_how, rows_with_max_lookback
 from ibis.tests.util import assert_equal
 
@@ -223,6 +224,25 @@ def test_preceding_following_validate(alltypes):
             case()
 
 
+def test_max_rows_with_lookback_validate(alltypes):
+    t = alltypes
+    mlb = rows_with_max_lookback(3, ibis.interval(days=5))
+    window = ibis.trailing_window(mlb, order_by=t.i)
+    t.f.lag().over(window)
+
+    window = ibis.trailing_window(mlb)
+    with pytest.raises(com.IbisInputError):
+        t.f.lag().over(window)
+
+    window = ibis.trailing_window(mlb, order_by=t.a)
+    with pytest.raises(com.IbisInputError):
+        t.f.lag().over(window)
+
+    window = ibis.trailing_window(mlb, order_by=[t.i, t.a])
+    with pytest.raises(com.IbisInputError):
+        t.f.lag().over(window)
+
+
 def test_window_equals(alltypes):
     t = alltypes
     w1 = ibis.window(
diff --git a/ibis/impala/tests/test_window.py b/ibis/impala/tests/test_window.py
--- a/ibis/impala/tests/test_window.py
+++ b/ibis/impala/tests/test_window.py
@@ -126,7 +126,7 @@ def test_window_frame_specs(con, window, frame):
 def test_window_rows_with_max_lookback(con):
     t = con.table('alltypes')
     mlb = rows_with_max_lookback(3, ibis.interval(days=3))
-    w = ibis.trailing_window(mlb)
+    w = ibis.trailing_window(mlb, order_by=t.i)
     expr = t.a.sum().over(w)
     with pytest.raises(NotImplementedError):
         to_sql(expr)
diff --git a/ibis/pandas/execution/tests/test_window.py b/ibis/pandas/execution/tests/test_window.py
--- a/ibis/pandas/execution/tests/test_window.py
+++ b/ibis/pandas/execution/tests/test_window.py
@@ -295,25 +295,6 @@ def test_batting_rolling(batting, batting_df, sort_kind):
     tm.assert_frame_equal(result[expected.columns], expected)
 
 
-def test_batting_rolling_with_mlb(batting, batting_df, sort_kind):
-    rows_with_mlb = rows_with_max_lookback(5, ibis.interval(days=10))
-    expr = batting.mutate(
-        more_values=lambda t: t.G.sum().over(
-            ibis.trailing_window(rows_with_mlb, order_by=t.yearID)
-        )
-    )
-    with pytest.raises(NotImplementedError):
-        expr.execute()
-
-    rows_with_mlb = rows_with_max_lookback(5, 10)
-    with pytest.raises(com.IbisInputError):
-        batting.mutate(
-            more_values=lambda t: t.G.sum().over(
-                ibis.trailing_window(rows_with_mlb, order_by=t.yearID)
-            )
-        )
-
-
 def test_batting_rolling_partitioned(batting, batting_df, sort_kind):
     t = batting
     group_by = 'playerID'
@@ -472,6 +453,31 @@ def test_window_with_preceding_expr():
     tm.assert_series_equal(result, expected)
 
 
+def test_window_with_mlb():
+    index = pd.date_range('20170501', '20170507')
+    data = np.random.randn(len(index), 3)
+    df = (pd.DataFrame(data, columns=list('abc'), index=index)
+          .rename_axis('time').reset_index(drop=False))
+    client = ibis.pandas.connect({'df': df})
+    t = client.table('df')
+    rows_with_mlb = rows_with_max_lookback(5, ibis.interval(days=10))
+    expr = t.mutate(
+        sum=lambda df: df.a.sum().over(
+            ibis.trailing_window(rows_with_mlb, order_by='time')
+        )
+    )
+    with pytest.raises(NotImplementedError):
+        expr.execute()
+
+    rows_with_mlb = rows_with_max_lookback(5, 10)
+    with pytest.raises(com.IbisInputError):
+        t.mutate(
+            sum=lambda df: df.a.sum().over(
+                ibis.trailing_window(rows_with_mlb, order_by='time')
+            )
+        )
+
+
 def test_window_has_pre_execute_scope():
     signature = ops.Lag, ibis.pandas.PandasClient
     called = [0]
diff --git a/ibis/sql/postgres/tests/test_functions.py b/ibis/sql/postgres/tests/test_functions.py
--- a/ibis/sql/postgres/tests/test_functions.py
+++ b/ibis/sql/postgres/tests/test_functions.py
@@ -890,7 +890,8 @@ def test_rolling_window(alltypes, func, df):
 def test_rolling_window_with_mlb(alltypes):
     t = alltypes
     window = ibis.trailing_window(
-        rows_with_max_lookback(3, ibis.interval(days=5))
+        preceding=rows_with_max_lookback(3, ibis.interval(days=5)),
+        order_by=t.timestamp_col
     )
     expr = t['double_col'].sum().over(window)
     with pytest.raises(NotImplementedError):