Skip to content

Emit EXCLUDE frame-exclusion for single-bound window frames#201

Merged
lelit merged 1 commit into
lelit:v7from
chuenchen309:fix/window-frame-exclude-single-bound
Jul 22, 2026
Merged

Emit EXCLUDE frame-exclusion for single-bound window frames#201
lelit merged 1 commit into
lelit:v7from
chuenchen309:fix/window-frame-exclude-single-bound

Conversation

@chuenchen309

Copy link
Copy Markdown
Contributor

RawStream / IndentedStream silently drop the EXCLUDE CURRENT ROW / EXCLUDE GROUP / EXCLUDE TIES frame-exclusion clause when a window frame uses a single bound (no BETWEEN), which changes the query's semantics:

from pglast import parse_sql
from pglast.stream import RawStream

sql = "SELECT sum(a) OVER (ORDER BY b ROWS 5 PRECEDING EXCLUDE CURRENT ROW) FROM t"
print(RawStream()(parse_sql(sql)))
# SELECT sum(a) OVER (ORDER BY b ROWS 5 PRECEDING) FROM t      <-- EXCLUDE CURRENT ROW dropped

BETWEEN frames are unaffected (ROWS BETWEEN 5 PRECEDING AND CURRENT ROW EXCLUDE CURRENT ROW round-trips fine), which is the tell: the exclusion bit is in the AST, it just isn't emitted for single-bound frames.

Root cause (pglast/printers/dml.py, window_def()): the EXCLUDE printing block was nested inside the if fo & FRAMEOPTION_BETWEEN: branch. But in the PostgreSQL grammar frame_exclusion follows frame_extent and applies to both the single frame_bound and the BETWEEN frame_bound AND frame_bound forms. So for single-bound frames the exclusion was never printed.

Fix: dedent the EXCLUDE block one level out of the if BETWEEN branch so it runs for every non-default frame. EXCLUDE NO OTHERS (the grammar default, no bit set) still prints nothing.

Verification (re-runnable from the diff): added four statements to tests/test_printers_roundtrip/dml/select.sql — single-bound ROWS/RANGE/GROUPS each with a different EXCLUDE, plus a BETWEEN ... EXCLUDE CURRENT ROW guard. The round-trip harness re-parses and asserts AST equality (and also checks IndentedStream). The three single-bound cases fail on the current tree and pass with the fix; the full suite is green (2120 passed in the round-trip module).


Disclosure: This PR was authored by an AI coding agent (Claude Code) running on this account: the AI found the bug, ran the repro, wrote the tests, and wrote this description. The human account holder reviews every change and is accountable for it. The verification above is real and re-runnable from the diff. If this isn't the kind of contribution you want, say so and I'll close it.

`window_def()` printed the `EXCLUDE CURRENT ROW`/`EXCLUDE GROUP`/`EXCLUDE TIES`
clause only inside the `if fo & FRAMEOPTION_BETWEEN:` branch, so a frame using a
single bound (e.g. `ROWS 5 PRECEDING EXCLUDE CURRENT ROW`) had the exclusion bit
set in the AST but silently dropped on reprint, changing the window's semantics.

Per the PostgreSQL grammar `frame_exclusion` follows `frame_extent` for both the
single `frame_bound` and the `BETWEEN frame_bound AND frame_bound` forms, so
dedent the EXCLUDE block one level to run for every non-default frame.
@lelit

lelit commented Jul 22, 2026

Copy link
Copy Markdown
Owner

Thank you for the report, and I appreciate the disclosure!

@lelit
lelit merged commit db7a84b into lelit:v7 Jul 22, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants