#2746·WrenAI

[Bug] transform_sql silently drops IGNORE NULLS from window functions

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

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:

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

can be transformed into SQL equivalent to:

sql
SELECT
    id,
    LAST_VALUE(v) OVER (
        ORDER BY id
        ROWS BETWEEN 1 PRECEDING AND CURRENT ROW
    ) AS last_non_null
FROM t

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

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

With the original SQL:

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

If IGNORE NULLS is removed:

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

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

sql
LAST_VALUE(v IGNORE NULLS)

must not be silently rewritten as:

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

rust
null_treatment: None

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

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

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