From 0259fdb1c5a725cd9f6b587b80298baf9e3b6220 Mon Sep 17 00:00:00 2001 From: Haibo Yan Date: Mon, 6 Apr 2026 23:50:50 -0700 Subject: [PATCH v1 1/5] Support parsing and deparsing DISTINCT in window aggregate calls PostgreSQL has long rejected syntax like count(DISTINCT x) OVER (...) sum(DISTINCT x) OVER (...) at parse time with: DISTINCT is not implemented for window functions That parser-time rejection makes it impossible to carry the syntax through parse analysis, node serialization, ruleutils deparse, or view round-tripping, and it also forces all future work on execution support to start by reopening the parser decision. This patch does not implement execution semantics for DISTINCT in window aggregates yet. Instead, it performs the first, intentionally narrow, step: preserve the syntax in the parse tree and move the rejection to executor initialization. Specifically, this patch: * adds a new boolean field, `windistinct`, to `WindowFunc`, recording whether DISTINCT was syntactically specified on a window aggregate call; * removes the existing parser-time FEATURE_NOT_SUPPORTED error in `parse_func.c` for DISTINCT on window aggregates, and sets `wfunc->windistinct` instead; * teaches `ruleutils.c` to print DISTINCT for `WindowFunc` nodes with `windistinct = true`, so stored rules and `pg_get_viewdef()` preserve the original syntax; * adds a temporary executor-side FEATURE_NOT_SUPPORTED error in the WindowAgg initialization path: DISTINCT is not yet implemented for window aggregates so execution still fails cleanly until real executor support is added. As a result, queries using DISTINCT in window aggregate calls now parse successfully, can be preserved in stored representations, and deparse correctly, but still fail at execution time rather than parse time. This patch is intentionally executor-strategy-agnostic. In particular, it does not yet decide any of the following: * whether v1 execution support should be limited to whole-partition frames, or also include grow-only frames; * whether DISTINCT tracking should be sort-based, hash-based, or a hybrid depending on frame shape and type capabilities; * whether any future execution path should impose hashability restrictions for some frame classes. Deferring those questions keeps this patch small and reviewable, while creating the minimal infrastructure needed for follow-up executor work. Regression tests cover: * successful parsing of a DISTINCT window aggregate call; * preservation of DISTINCT through deparse / view definition output; * the new executor-side FEATURE_NOT_SUPPORTED failure. Future work will replace the temporary executor-side rejection with real WindowAgg support for DISTINCT semantics. --- src/backend/executor/nodeWindowAgg.c | 9 +++++++++ src/backend/optimizer/util/clauses.c | 1 + src/backend/parser/parse_func.c | 6 ++++-- src/backend/utils/adt/ruleutils.c | 2 ++ src/include/nodes/primnodes.h | 2 ++ src/test/regress/expected/window.out | 23 +++++++++++++++++++++++ src/test/regress/sql/window.sql | 18 ++++++++++++++++++ 7 files changed, 59 insertions(+), 2 deletions(-) diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c index 784ceeb8246..9431cae9ae0 100644 --- a/src/backend/executor/nodeWindowAgg.c +++ b/src/backend/executor/nodeWindowAgg.c @@ -2908,6 +2908,15 @@ initialize_peragg(WindowAggState *winstate, WindowFunc *wfunc, int i; ListCell *lc; + /* + * Temporary: reject DISTINCT window aggregates until executor support + * lands. Patch 2 will replace this with actual DISTINCT handling. + */ + if (wfunc->windistinct) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("DISTINCT is not yet implemented for window aggregates"))); + numArguments = list_length(wfunc->args); i = 0; diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index 9fb266d089d..9749056e2d3 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -2814,6 +2814,7 @@ eval_const_expressions_mutator(Node *node, newexpr->winref = expr->winref; newexpr->winstar = expr->winstar; newexpr->winagg = expr->winagg; + newexpr->windistinct = expr->windistinct; newexpr->ignore_nulls = expr->ignore_nulls; newexpr->location = expr->location; diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c index 35ff6427147..d216d53e530 100644 --- a/src/backend/parser/parse_func.c +++ b/src/backend/parser/parse_func.c @@ -847,6 +847,7 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs, /* winref will be set by transformWindowFuncCall */ wfunc->winstar = agg_star; wfunc->winagg = (fdresult == FUNCDETAIL_AGGREGATE); + wfunc->windistinct = agg_distinct; wfunc->aggfilter = agg_filter; wfunc->ignore_nulls = ignore_nulls; wfunc->runCondition = NIL; @@ -854,11 +855,12 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs, /* * agg_star is allowed for aggregate functions but distinct isn't + * allowed for non-aggregate window functions. */ - if (agg_distinct) + if (agg_distinct && !wfunc->winagg) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("DISTINCT is not implemented for window functions"), + errmsg("DISTINCT is not implemented for non-aggregate window functions"), parser_errposition(pstate, location))); /* diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 35083fcc733..602446eefad 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -11659,6 +11659,8 @@ get_windowfunc_expr_helper(WindowFunc *wfunc, deparse_context *context, appendStringInfoChar(buf, '*'); else { + if (wfunc->windistinct) + appendStringInfoString(buf, "DISTINCT "); if (is_json_objectagg) { get_rule_expr((Node *) linitial(wfunc->args), context, false); diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 6dfc946c20b..1e984dfbfda 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -614,6 +614,8 @@ typedef struct WindowFunc bool winstar pg_node_attr(query_jumble_ignore); /* is function a simple aggregate? */ bool winagg pg_node_attr(query_jumble_ignore); + /* true if aggregate arguments were marked DISTINCT */ + bool windistinct; /* ignore nulls. One of the Null Treatment options */ int ignore_nulls; /* token location, or -1 if unknown */ diff --git a/src/test/regress/expected/window.out b/src/test/regress/expected/window.out index 7a04d3a7a9f..0f4dc2fe96f 100644 --- a/src/test/regress/expected/window.out +++ b/src/test/regress/expected/window.out @@ -5876,3 +5876,26 @@ WINDOW w AS (ORDER BY x ROWS BETWEEN 2 PRECEDING AND 2 FOLLOWING); --cleanup DROP TABLE planets CASCADE; NOTICE: drop cascades to view planets_view +-- +-- Test DISTINCT in window aggregates (parse/deparse plumbing only; +-- execution support is not yet implemented) +-- +-- Should parse successfully and round-trip through a view definition +CREATE TEMP VIEW window_distinct_view AS +SELECT count(DISTINCT four) OVER (PARTITION BY ten) AS cnt +FROM tenk1; +SELECT pg_get_viewdef('window_distinct_view') LIKE '%DISTINCT%' AS has_distinct; + has_distinct +-------------- + t +(1 row) + +DROP VIEW window_distinct_view; +-- DISTINCT on a non-aggregate window function is still a parse error +SELECT ntile(DISTINCT 4) OVER () FROM tenk1; -- error +ERROR: DISTINCT is not implemented for non-aggregate window functions +LINE 1: SELECT ntile(DISTINCT 4) OVER () FROM tenk1; + ^ +-- Execution fails with a clear executor-side error +SELECT count(DISTINCT four) OVER (PARTITION BY ten) FROM tenk1; -- error +ERROR: DISTINCT is not yet implemented for window aggregates diff --git a/src/test/regress/sql/window.sql b/src/test/regress/sql/window.sql index 37d837a2f66..be45bd5f14f 100644 --- a/src/test/regress/sql/window.sql +++ b/src/test/regress/sql/window.sql @@ -2133,3 +2133,21 @@ WINDOW w AS (ORDER BY x ROWS BETWEEN 2 PRECEDING AND 2 FOLLOWING); --cleanup DROP TABLE planets CASCADE; + +-- +-- Test DISTINCT in window aggregates (parse/deparse plumbing only; +-- execution support is not yet implemented) +-- + +-- Should parse successfully and round-trip through a view definition +CREATE TEMP VIEW window_distinct_view AS +SELECT count(DISTINCT four) OVER (PARTITION BY ten) AS cnt +FROM tenk1; +SELECT pg_get_viewdef('window_distinct_view') LIKE '%DISTINCT%' AS has_distinct; +DROP VIEW window_distinct_view; + +-- DISTINCT on a non-aggregate window function is still a parse error +SELECT ntile(DISTINCT 4) OVER () FROM tenk1; -- error + +-- Execution fails with a clear executor-side error +SELECT count(DISTINCT four) OVER (PARTITION BY ten) FROM tenk1; -- error -- 2.52.0