[Bug] wren-core silently drops explicit ROWS window frame during transform_sql
Describe the bug
wren_core.SessionContext.transform_sql() can silently drop an explicitly specified ROWS window frame when the frame is:
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROWFor example:
SELECT
id,
SUM(v) OVER (
ORDER BY k
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS running_sum
FROM tDuring SQL planning/unparsing, the explicit ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW clause can be omitted.
This is not always semantics-preserving.
For DuckDB, when ORDER BY is present and no explicit window frame is specified, the default behavior uses RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW.
ROWS and RANGE behave differently when the ORDER BY expression contains duplicate values (peer rows).
As a result, Wren can produce SQL that executes successfully but returns a different result from the original SQL.
This is particularly dangerous for analytical / agent-generated SQL because there is no syntax or execution error indicating that the computation semantics changed.
To Reproduce
Using:
wrenai==0.14.0wren-core-py==0.7.6- DuckDB / local-file datasource
Create a minimal manifest:
import base64
import json
from wren.mdl import get_session_context
manifest = {
"catalog": "wren",
"schema": "main",
"models": [
{
"name": "t",
"tableReference": {
"schema": "main",
"table": "t",
},
"columns": [
{"name": "id", "type": "BIGINT"},
{"name": "k", "type": "BIGINT"},
{"name": "v", "type": "BIGINT"},
],
}
],
"relationships": [],
"views": [],
}
manifest_str = base64.b64encode(
json.dumps(manifest).encode()
).decode()
ctx = get_session_context(
manifest_str,
None,
None,
"duckdb",
)
sql = """
SELECT
id,
SUM(v) OVER (
ORDER BY k
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS running_sum
FROM t
"""
planned_sql = ctx.transform_sql(sql)
print("INPUT SQL:")
print(sql)
print("PLANNED SQL:")
print(planned_sql)The input explicitly contains:
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROWbut the planned SQL drops that explicit frame.
The semantic difference can be demonstrated with:
id | k | v
---+---+---
1 | 1 | 10
2 | 1 | 20
3 | 2 | 30With the explicit ROWS frame:
SELECT
id,
SUM(v) OVER (
ORDER BY k
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS running_sum
FROM t;the expected result is:
id | running_sum
---+------------
1 | 10
2 | 30
3 | 60If the explicit frame is omitted and the backend applies RANGE semantics, the first two rows are peers because both have k = 1:
id | running_sum
---+------------
1 | 30
2 | 30
3 | 60The SQL remains valid, but its semantics have changed.
Expected behavior
An explicitly specified window frame should be preserved during transform_sql() whenever removing it can change query semantics.
In particular, Wren should preserve the distinction between:
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROWand:
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROWThe SQL planner should not silently replace an explicitly authored ROWS frame with the target database's implicit/default frame.
Screenshots
Not applicable.
This issue is reproducible through wren-core-py / SessionContext.transform_sql() and does not require the UI.
Desktop (please complete the following information):
- OS: Linux
- Browser: Not applicable
- Python: 3.11+
Wren AI Information
wrenai:0.14.0wren-core-py:0.7.6- DataFusion: 53
- Datasource: DuckDB / local file
Additional context
The relevant implementation appears to be:
core/wren-core/core/src/mdl/dialect/wren_dialect.rs
Current logic:
fn window_func_support_window_frame(
&self,
func_name: &str,
start_bound: &WindowFrameBound,
end_bound: &WindowFrameBound,
) -> bool {
if matches!(start_bound, WindowFrameBound::Preceding(None))
&& matches!(end_bound, WindowFrameBound::CurrentRow)
{
false
} else {
self.inner_dialect.window_func_support_window_frame(
func_name,
start_bound,
end_bound,
)
}
}The special case checks only the frame bounds:
UNBOUNDED PRECEDING -> CURRENT ROWbut does not distinguish the frame unit:
ROWSRANGEGROUPS
Therefore an explicitly authored ROWS frame may be omitted even though the target engine's implicit frame has different semantics.
A possible fix would be to avoid suppressing an explicitly specified frame, or at least preserve it whenever dropping it would change the frame unit or peer-row semantics.
It would also be useful to add a regression test with duplicate ORDER BY values, because unique ordering values can hide the semantic difference.
For example:
SUM(v) OVER (
ORDER BY k
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
)with duplicate k values should retain the explicit ROWS clause after planning.
Relevant log output
Not applicable.
This reproduction uses wren-core-py directly and does not require config.yaml or the legacy Docker services.
Source: Canner/WrenAI