#2745·WrenAI

[Bug] wren-core silently drops explicit ROWS window frame during transform_sql

Author: qiboyu301-cryptoCreated Sep 15, 2026Updated Sep 15, 2026
Labelsbug

Describe the bug

wren_core.SessionContext.transform_sql() can silently drop an explicitly specified ROWS window frame when the frame is:

sql
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW

For example:

sql
SELECT
    id,
    SUM(v) OVER (
        ORDER BY k
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS running_sum
FROM t

During 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.0
  • wren-core-py==0.7.6
  • DuckDB / local-file datasource

Create a minimal manifest:

python
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:

sql
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW

but 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 | 30

With the explicit ROWS frame:

sql
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  | 60

If 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  | 60

The 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:

sql
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW

and:

sql
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW

The 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.0
  • wren-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:

rust
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 ROW

but does not distinguish the frame unit:

  • ROWS
  • RANGE
  • GROUPS

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:

sql
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.