[Bug] transform_sql silently drops IGNORE NULLS from window functions
Describe the bug
wren_core.SessionContext.transform_sql() can silently remove the IGNORE NULLS clause from window functions such as LAST_VALUE() and FIRST_VALUE().
For example, the following SQL:
SELECT
id,
LAST_VALUE(v IGNORE NULLS) OVER (
ORDER BY id
ROWS BETWEEN 1 PRECEDING AND CURRENT ROW
) AS last_non_null
FROM tcan be transformed into SQL equivalent to:
SELECT
id,
LAST_VALUE(v) OVER (
ORDER BY id
ROWS BETWEEN 1 PRECEDING AND CURRENT ROW
) AS last_non_null
FROM tThe transformed SQL remains syntactically valid, but its semantics are different.
This is especially dangerous because the query can execute successfully while returning incorrect analytical results.
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": "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,
LAST_VALUE(v IGNORE NULLS) OVER (
ORDER BY id
ROWS BETWEEN 1 PRECEDING AND CURRENT ROW
) AS last_non_null
FROM t
"""
planned_sql = ctx.transform_sql(sql)
print("INPUT SQL:")
print(sql)
print("PLANNED SQL:")
print(planned_sql)The input SQL explicitly contains:
LAST_VALUE(v IGNORE NULLS)but the planned SQL drops IGNORE NULLS.
The semantic difference can be demonstrated with:
id | v
---+------
1 | 10
2 | NULL
3 | 30With the original SQL:
SELECT
id,
LAST_VALUE(v IGNORE NULLS) OVER (
ORDER BY id
ROWS BETWEEN 1 PRECEDING AND CURRENT ROW
) AS last_non_null
FROM t;the expected result is:
id | last_non_null
---+--------------
1 | 10
2 | 10
3 | 30If IGNORE NULLS is removed:
SELECT
id,
LAST_VALUE(v) OVER (
ORDER BY id
ROWS BETWEEN 1 PRECEDING AND CURRENT ROW
) AS last_non_null
FROM t;the result becomes:
id | last_non_null
---+--------------
1 | 10
2 | NULL
3 | 30So the planned SQL executes successfully but no longer represents the semantics of the input SQL.
Expected behavior
transform_sql() should preserve explicit null-treatment clauses on window functions.
In particular:
LAST_VALUE(v IGNORE NULLS)must not be silently rewritten as:
LAST_VALUE(v)If the target dialect does not support the requested null-treatment semantics, planning should fail explicitly rather than silently changing the query.
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
This appears to be caused by the SQL unparse path used by wren-core.
Wren AI 0.14.0 uses DataFusion 53. DataFusion's logical window expression retains null-treatment information internally, but the SQL unparser constructs the output window function with:
null_treatment: NoneAs a result, IGNORE NULLS is lost when the logical plan is converted back to SQL.
This matters to Wren even if the underlying behavior originates in DataFusion, because SessionContext.transform_sql() exposes the transformed SQL as the planned query and callers reasonably expect it to preserve query semantics.
The current CTE-based WrenEngine path reduces exposure to whole-query transformation, but the legacy/fallback path can still call:
self.session_context.transform_sql(sql)for the entire user query.
Therefore this bug can still affect valid user SQL when that path is reached.
This issue is separate from:
https://github.com/Canner/WrenAI/issues/2745
Issue #2745 tracks explicit ROWS window-frame loss. This issue specifically tracks loss of the IGNORE NULLS null-treatment clause.
A regression test should compare both the planned SQL and the execution result for a window containing NULL values.
For example:
LAST_VALUE(v IGNORE NULLS) OVER (
ORDER BY id
ROWS BETWEEN 1 PRECEDING AND CURRENT ROW
)should retain IGNORE NULLS 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