Skip to content

docs(sql): complete the window-function list, and guard it with a test - #246

Merged
fupelaqu merged 3 commits into
mainfrom
docs/window-functions-coverage
Aug 26, 2026
Merged

docs(sql): complete the window-function list, and guard it with a test#246
fupelaqu merged 3 commits into
mainfrom
docs/window-functions-coverage

Conversation

@fupelaqu

@fupelaqu fupelaqu commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

What

documentation/sql/dql_statements.md enumerates which functions work with OVER. That list had fallen four entries behind the parser: identifierWithWindowFunction accepts 14 forms, the list named 10 — omitting AVG, MIN, MAX and PERCENTILE_CONT / PERCENTILE_DISC.

The reference page (functions_aggregate.md) documents all of them, and so do keywords.md and the REPL help JSON. So this was an enumerated list going stale, not a missing feature — but the DQL page is where a reader goes to find out what works with OVER, and it was the one saying no.

Found because the same omission had propagated into a blog post.

Verified, not read

Every added form was run through the real parser before being written down, and each is asserted to round-tripstatement.sql must parse again. A round-trip that silently drops a clause is how a documented form becomes a wrong query downstream.

The percentile forms turned up something worth documenting on its own: all four spellings normalize to the same canonical rendering. Writing

PERCENTILE_CONT(0.95) OVER (PARTITION BY service ORDER BY ms)

gets you back

PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY ms) OVER (PARTITION BY service)

Same for the (column, p) shorthand. A statement round-tripped through the engine returns in a syntax the user did not type — that is now stated in the docs.

The test

WindowFunctionCoverageSpec32 tests, green (plus 532 sql tests and scalafmtCheck).

Every form is pinned to its exact rendering, not to "it parses". That distinction is the whole point, and the first revision of this PR got it wrong: Parser(rendered).isRight proves a rendering parses, not that it is faithful — and the suite already contained the counter-example. ARRAY_AGG's inline LIMIT is parsed, kept on the AST, and silently dropped when rendering (emitsLimitInOver is false for everything but ranking windows). The weaker assertion was green on precisely the corruption it was written to catch.

  • 28 documented forms, each pinned to the string the engine gives back
  • 1 named test for the ARRAY_AGG LIMIT loss, so the day it is fixed the suite says so instead of staying quietly green
  • 3 rejections, each asserting why. ROW_NUMBER() OVER (PARTITION BY d) and ROW_NUMBER() AS rn are rejected with the same generic message, so isLeft alone could not tell "the ANSI rule fired" from "ROW_NUMBER stopped being a window function"; it is now paired with the ORDER BY variant that must parse. The percentile rejections assert the message names the [0,1] bound.

Corrections from review

  • PERCENTILE_DISC(p) OVER (...) was listed and does not parse — the value column is required, so ORDER BY inside OVER is not optional
  • SUM/AVG/MIN/MAX/COUNT now show OVER (PARTITION BY ...): an ORDER BY inside a non-ranking OVER is accepted and then discarded (COUNT(x) OVER (PARTITION BY c ORDER BY ts) renders back without it)
  • the percentile "See" link pointed at another page while this file has its own percentile section
  • the first commit did not pass sql/Test/scalafmtCheck — verified by restoring it and running the gate

Not included

softclient4es-web's src/content/docs/sql/dql.mdx carries the same section and the same gap. That is a separate PR on the web repo.

🤖 Generated with Claude Code

The "Supported window functions include:" list in dql_statements.md had
fallen four entries behind the parser. `identifierWithWindowFunction`
accepts 14 forms; the list named 10, omitting AVG, MIN, MAX and
PERCENTILE_CONT/PERCENTILE_DISC. The reference page (functions_aggregate.md)
documents all of them, so this was the enumerated list going stale, not a
missing feature -- but the DQL page is where a reader looks to find out
what works with OVER.

Every added form was run through the real parser before being written
down, and each also round-trips: statement.sql parses again. The percentile
forms revealed something worth documenting on its own -- all four
spellings (OVER, WITHIN GROUP, both, and the (column, p) shorthand)
normalize to the same canonical rendering, so a statement round-tripped
through the engine comes back in a syntax the user did not type.

WindowFunctionCoverageSpec locks the surface down: 26 documented forms,
each asserted to parse and to re-parse from its own rendering, plus 3
rejections the docs state explicitly (a ranking window with no ORDER BY,
and a percentile fraction outside [0,1]). Nothing asserted the documented
surface before, which is why the list could drift unnoticed.
…g it guards

Independent review of the first commit; every finding below was confirmed
against the real parser before being acted on.

The test was weaker than its own scaladoc claimed. `Parser(rendered).isRight`
proves a rendering parses, not that it is faithful -- and the suite already
contained a case that proves the difference: ARRAY_AGG's inline LIMIT is
parsed, kept on the AST, and silently dropped when rendering
(emitsLimitInOver is false for everything but ranking windows). The old
assertion was green on exactly the corruption it was written to catch.

Every form is now pinned to its exact rendering, which is the only
assertion that catches a dropped clause. That also turns the PR's headline
finding into something asserted rather than merely asserted-in-prose: the
five percentile spellings are each pinned to the canonical WITHIN GROUP
form they normalize to. The ARRAY_AGG loss gets its own named test so the
day it is fixed, the suite says so instead of staying quietly green.

Negative tests now assert WHY. `ROW_NUMBER() OVER (PARTITION BY d)` and
`ROW_NUMBER() AS rn` are rejected with the SAME generic message, so
isLeft alone could not tell "the ANSI rule fired" from "ROW_NUMBER stopped
being a window function" -- it is now paired with the ORDER BY variant that
must parse. The percentile rejections assert the message names the [0,1]
bound.

Docs: `PERCENTILE_DISC(p) OVER (...)` was listed but does not parse -- the
value column is required, so it needs ORDER BY inside OVER. SUM/AVG/MIN/
MAX/COUNT now show `OVER (PARTITION BY ...)` rather than `OVER (...)`,
because an ORDER BY inside a non-ranking OVER is accepted and then
discarded. The percentile "See" link pointed at another page while this
file has its own percentile section; it now points at both.

Also: scalafmt. The first commit did NOT pass sql/Test/scalafmtCheck --
verified by restoring it and running the gate -- so CI would have failed.

32 tests green; 532 sql tests green; scalafmtCheck green.
The known-loss test carried a dangling 'See SoftClient4ES issue' with no
number. #247 now records it: the render drops the inline LIMIT, and
ArrayAgg.update falls back to request.limit, so the statement's own LIMIT
is substituted for the inline one on the next parse -- a different answer
rather than an error.
@fupelaqu
fupelaqu merged commit 044a9f0 into main Aug 26, 2026
4 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.

1 participant