Repository: ibis-project/ibis

Issue:
bug: when the start and end of window boundaries span across data types, ibis forces casting
### What happened?

When the start and end of window boundaries span across data types, ibis forces casting in the window boundaries: https://github.com/ibis-project/ibis/blob/17be43a1081c34582408d4c627191263ec948ada/ibis/expr/builders.py#L177-L182
and this is not executable in 5 backends (with the addition of Flink streaming, which I am working on).

Steps to reproduce this error:
https://github.com/ibis-project/ibis/blob/764c835598944c89cfca55802101d7effd1a6c78/ibis/backends/tests/test_window.py#L489-L520

Example:
```python
alltypes.mutate(val=alltypes.double_col.sum().over(ibis.window(preceding=1000, following=0, group_by=[t.string_col], order_by=[t.id]))
```
The window boundaries here span across integer data types, and the IR generated for this involves a casting. The generated SQL query looks something like
```sql
SELECT
  SUM(`t0`.`double_col`) OVER (ORDER BY `t0`.`id` ASC NULLS LAST ROWS BETWEEN 1000 preceding AND CAST(0 AS SMALLINT) following) AS `Sum(double_col)`
FROM `alltypes` AS `t0`
```
and the `CAST[...] following` syntax is not recognized on certain backends.

The same happens when using range intervals.

### What version of ibis are you using?

8.0

### What backend(s) are you using, if any?

All backends

### Relevant log output

```sh
# MySQL
pymysql.err.ProgrammingError: (1064, "You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'CAST(0 AS SMALLINT) following) AS `val`\nFROM (\n  SELECT\n    `t0`.`id`,\n    `t...' at line 15")
# RisingWave
sql parser error: Expected literal int, found: CAST at line:15, column:122
Near " BETWEEN 1000 preceding AND CAST"
# Impala
impala.error.HiveServer2Error: AnalysisException: For ROWS window, the value of a PRECEDING/FOLLOWING offset must be a constant positive integer: CAST(0 AS SMALLINT) FOLLOWING
# MS SQL
pyodbc.ProgrammingError: ('42000', "[42000] [FreeTDS][SQL Server]Incorrect syntax near 'CAST'. (102) (SQLExecDirectW)")
```


### Code of Conduct

- [X] I agree to follow this project's Code of Conduct


Discussion:
Wondering if the ideal solution here is to get rid of the force casting step in building windows. If we want to be more failure-proof, we can add a check in individual backends that don't support this in addition, so that an error is thrown before engine runtime

Resolving patch:
diff --git a/ibis/expr/builders.py b/ibis/expr/builders.py
--- a/ibis/expr/builders.py
+++ b/ibis/expr/builders.py
@@ -168,17 +168,12 @@ def _table(self):
         else:
             raise IbisInputError("Window frame can only depend on a single relation")
 
-    def _maybe_cast_boundary(self, boundary, dtype):
-        if boundary.dtype == dtype:
-            return boundary
-        value = ops.Cast(boundary.value, dtype)
-        return boundary.copy(value=value)
-
     def _maybe_cast_boundaries(self, start, end):
         if start and end:
-            dtype = dt.higher_precedence(start.dtype, end.dtype)
-            start = self._maybe_cast_boundary(start, dtype)
-            end = self._maybe_cast_boundary(end, dtype)
+            if start.dtype.is_interval() and end.dtype.is_numeric():
+                return start, ops.Cast(end.value, start.dtype)
+            elif start.dtype.is_numeric() and end.dtype.is_interval():
+                return ops.Cast(start.value, end.dtype), end
         return start, end
 
     def _determine_how(self, start, end):
diff --git a/ibis/expr/operations/window.py b/ibis/expr/operations/window.py
--- a/ibis/expr/operations/window.py
+++ b/ibis/expr/operations/window.py
@@ -71,10 +71,14 @@ class WindowFrame(Value):
     shape = ds.columnar
 
     def __init__(self, start, end, **kwargs):
-        if start and end and start.dtype != end.dtype:
-            raise com.IbisTypeError(
-                "Window frame start and end boundaries must have the same datatype"
-            )
+        if start and end:
+            if not (
+                (start.dtype.is_interval() and end.dtype.is_interval())
+                or (start.dtype.is_numeric() and end.dtype.is_numeric())
+            ):
+                raise com.IbisTypeError(
+                    "Window frame start and end boundaries must have the same datatype"
+                )
         super().__init__(start=start, end=end, **kwargs)
 
     def dtype(self) -> dt.DataType:


Test patch:
diff --git a/ibis/backends/flink/tests/snapshots/test_window/test_rows_window/out.sql b/ibis/backends/flink/tests/snapshots/test_window/test_rows_window/out.sql
--- a/ibis/backends/flink/tests/snapshots/test_window/test_rows_window/out.sql
+++ b/ibis/backends/flink/tests/snapshots/test_window/test_rows_window/out.sql
@@ -1,3 +1,3 @@
 SELECT
-  SUM(`t0`.`f`) OVER (ORDER BY `t0`.`f` ASC NULLS LAST ROWS BETWEEN 1000 preceding AND CAST(0 AS SMALLINT) following) AS `Sum(f)`
+  SUM(`t0`.`f`) OVER (ORDER BY `t0`.`f` ASC NULLS LAST ROWS BETWEEN 1000 preceding AND CURRENT ROW) AS `Sum(f)`
 FROM `table` AS `t0`
\ No newline at end of file
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
@@ -453,7 +453,7 @@ def test_grouped_bounded_following_window(backend, alltypes, df, preceding, foll
 
 
 @pytest.mark.parametrize(
-    "window_fn",
+    "window_fn, window_size",
     [
         param(
             lambda t: ibis.window(
@@ -462,6 +462,7 @@ def test_grouped_bounded_following_window(backend, alltypes, df, preceding, foll
                 group_by=[t.string_col],
                 order_by=[t.id],
             ),
+            3,
             id="preceding-2-following-0",
         ),
         param(
@@ -471,18 +472,29 @@ def test_grouped_bounded_following_window(backend, alltypes, df, preceding, foll
                 group_by=[t.string_col],
                 order_by=[t.id],
             ),
+            3,
             id="preceding-2-following-0-tuple",
         ),
         param(
             lambda t: ibis.trailing_window(
                 preceding=2, group_by=[t.string_col], order_by=[t.id]
             ),
+            3,
             id="trailing-2",
         ),
+        param(
+            lambda t: ibis.window(
+                preceding=1000, following=0, group_by=[t.string_col], order_by=[t.id]
+            ),
+            1001,
+            id="large-preceding-1000-following-0",
+        ),
     ],
 )
 @pytest.mark.notimpl(["polars"], raises=com.OperationNotDefinedError)
-def test_grouped_bounded_preceding_window(backend, alltypes, df, window_fn):
+def test_grouped_bounded_preceding_window(
+    backend, alltypes, df, window_fn, window_size
+):
     window = window_fn(alltypes)
     expr = alltypes.mutate(val=alltypes.double_col.sum().over(window))
 
@@ -490,7 +502,7 @@ def test_grouped_bounded_preceding_window(backend, alltypes, df, window_fn):
     gdf = df.sort_values("id").groupby("string_col")
     expected = (
         df.assign(
-            val=gdf.double_col.rolling(3, min_periods=1)
+            val=gdf.double_col.rolling(window_size, min_periods=1)
             .sum()
             .sort_index(level=1)
             .reset_index(drop=True)