diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c index 62746fc33bd..a8c7b03d53b 100644 --- a/src/backend/parser/parse_rpr.c +++ b/src/backend/parser/parse_rpr.c @@ -56,6 +56,7 @@ typedef struct ParseState *pstate; List **targetlist; List *groupExprs; /* expressions GROUP BY computes */ + bool hasJoinRTEs; /* true if pstate->p_rtable has a join RTE */ } DefinePlantCtx; /* Forward declarations */ @@ -63,6 +64,7 @@ static void validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node, List **varNames); static List *transformDefineClause(ParseState *pstate, WindowDef *windef, List **targetlist, List *groupClause); +static Node *define_flatten_join_alias(ParseState *pstate, Node *node); static bool define_plant_walker(Node *node, void *context); static bool define_walker(Node *node, void *context); static bool rpr_frame_is_supported(int frameOptions); @@ -254,6 +256,7 @@ transformDefineClause(ParseState *pstate, WindowDef *windef, List *defineClause = NIL; List *patternVarNames = NIL; List *groupExprs = NIL; + bool hasJoinRTEs = false; /* * Collect what GROUP BY computes, so that the planting below can stop at @@ -267,6 +270,24 @@ transformDefineClause(ParseState *pstate, WindowDef *windef, groupExprs = lappend(groupExprs, tle->expr); } + /* + * A join's merged column can reach GROUP BY spelled either as the join's + * own name or as that name's expansion. define_plant_walker() only + * matches a subexpression GROUP BY computes when both sides are spelled + * alike, so flatten join alias Vars out of groupExprs here, once, for + * every DEFINE variable below to reuse. + */ + foreach_node(RangeTblEntry, rte, pstate->p_rtable) + { + if (rte->rtekind == RTE_JOIN) + { + hasJoinRTEs = true; + break; + } + } + if (hasJoinRTEs && groupExprs != NIL) + groupExprs = (List *) define_flatten_join_alias(pstate, (Node *) groupExprs); + /* * The grammar builds an RPCommonSyntax only for a window specification * that carries DEFINE, so the list is never empty here. @@ -393,6 +414,7 @@ transformDefineClause(ParseState *pstate, WindowDef *windef, ctx.pstate = pstate; ctx.targetlist = targetlist; ctx.groupExprs = groupExprs; + ctx.hasJoinRTEs = hasJoinRTEs; (void) define_plant_walker(expr, &ctx); } pstate->p_rpr_pattern_vars = NIL; @@ -416,6 +438,28 @@ transformDefineClause(ParseState *pstate, WindowDef *windef, return defineClause; } +/* + * define_flatten_join_alias + * Flatten join alias Vars out of a DEFINE-related expression, so a + * join's merged column and its expanded spelling compare equal. + * + * No Query exists yet to hand flatten_join_alias_for_parser() at this point + * in parsing, so a stand-in carrying just the fields it reads is enough -- + * hasSubLinks included only for shape, since DEFINE cannot contain one. + */ +static Node * +define_flatten_join_alias(ParseState *pstate, Node *node) +{ + Query stub; + + memset(&stub, 0, sizeof(stub)); + stub.type = T_Query; + stub.rtable = pstate->p_rtable; + stub.hasSubLinks = pstate->p_hasSubLinks; + + return flatten_join_alias_for_parser(&stub, node, 0); +} + /* * define_plant_walker * Plant in the target list what a DEFINE expression reads. @@ -428,14 +472,19 @@ static bool define_plant_walker(Node *node, void *context) { DefinePlantCtx *ctx = (DefinePlantCtx *) context; + Node *cmpnode = node; if (node == NULL) return false; + /* Compare in the same flattened shape groupExprs was put in above. */ + if (ctx->hasJoinRTEs) + cmpnode = define_flatten_join_alias(ctx->pstate, node); + /* A subexpression GROUP BY computes needs nothing planted for it. */ foreach_ptr(Node, gexpr, ctx->groupExprs) { - if (equal(node, gexpr)) + if (equal(cmpnode, gexpr)) return false; } diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index ef6c05be399..3000498b3c2 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -447,6 +447,7 @@ static void append_pattern_quantifier(StringInfo buf, RPRPatternNode *node); static void get_rule_pattern_node(RPRPatternNode *node, deparse_context *context); static void get_rule_pattern(RPRPatternNode *rpPattern, deparse_context *context); static void get_rule_define(List *defineClause, deparse_context *context); +static Node *collapse_define_join_vars_mutator(Node *node, List *rtable); static void get_rule_windowclause(Query *query, deparse_context *context); static void get_rule_windowspec(WindowClause *wc, List *targetList, deparse_context *context); @@ -5829,15 +5830,23 @@ get_query_def(Query *query, StringInfo buf, List *parentnamespace, /* * A row pattern DEFINE clause carries GROUP Vars of its own; expand * them, or the deparsed text would name the grouping step rather than - * the expression the user wrote, and the view would not re-parse. + * the expression the user wrote. For a FULL JOIN's USING column, + * that expansion is a CoalesceExpr DEFINE cannot print (XIV-5 forbids + * qualifiers) -- fold it back into a plain join-column reference; see + * collapse_define_join_vars_mutator(). */ foreach(lc, query->windowClause) { WindowClause *wc = lfirst_node(WindowClause, lc); if (wc->defineClause != NIL) + { wc->defineClause = (List *) flatten_group_exprs(NULL, query, (Node *) wc->defineClause); + wc->defineClause = (List *) + collapse_define_join_vars_mutator((Node *) wc->defineClause, + query->rtable); + } } } @@ -7096,6 +7105,49 @@ get_rule_define(List *defineClause, deparse_context *context) context->inRPRDefine = save_inrprdefine; } +/* + * collapse_define_join_vars_mutator + * Fold a subexpression that exactly matches some join RTE's merged + * column definition back into a plain reference to that join column; + * see the caller for why. + */ +static Node * +collapse_define_join_vars_mutator(Node *node, List *rtable) +{ + ListCell *lc; + int varno; + + if (node == NULL) + return NULL; + + varno = 0; + foreach(lc, rtable) + { + RangeTblEntry *rte = lfirst_node(RangeTblEntry, lc); + ListCell *lc2; + int attno; + + varno++; + if (rte->rtekind != RTE_JOIN) + continue; + + attno = 0; + foreach(lc2, rte->joinaliasvars) + { + Node *aliasvar = (Node *) lfirst(lc2); + + attno++; + if (aliasvar != NULL && equal(node, aliasvar)) + return (Node *) makeVar(varno, attno, + exprType(node), exprTypmod(node), + exprCollation(node), 0); + } + } + + return expression_tree_mutator(node, collapse_define_join_vars_mutator, + rtable); +} + /* * Display a WINDOW clause. * diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out index 539a4fb4bc7..9d44cc8545e 100644 --- a/src/test/regress/expected/rpr_base.out +++ b/src/test/regress/expected/rpr_base.out @@ -9530,6 +9530,87 @@ WINDOW w AS ( 1 (1 row) +-- GROUP BY spelled as the merged column's COALESCE expansion, rather than +-- the join's own name, while DEFINE reads that same column: the two spellings +-- must compare equal despite the different tree shapes. +SELECT id + 1 AS b, count(*) OVER w AS cnt +FROM rpr_grp FULL JOIN rpr_sort USING (id) +GROUP BY COALESCE(rpr_grp.id, rpr_sort.id) + 1 +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A) DEFINE A AS (id + 1) > 0) +ORDER BY 1; + b | cnt +---+----- + 2 | 1 + 3 | 1 + 4 | 1 + 5 | 1 + 6 | 1 + 7 | 1 +(6 rows) + +-- Same construct as a view: the DEFINE clause must deparse to the plain +-- join column, not the two-sided COALESCE GROUP BY computed, or the printed +-- text would not re-parse. +CREATE VIEW rpr_fjcoal_v AS +SELECT COALESCE(rpr_grp.id, rpr_sort.id) + 1 AS idp1, count(*) OVER w AS cnt +FROM rpr_grp FULL JOIN rpr_sort USING (id) +GROUP BY COALESCE(rpr_grp.id, rpr_sort.id) + 1 +WINDOW w AS ( + ORDER BY COALESCE(rpr_grp.id, rpr_sort.id) + 1 + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS id + 1 > 0); +SELECT pg_get_viewdef('rpr_fjcoal_v'::regclass, true); + pg_get_viewdef +------------------------------------------------------------------------------------------------------------------ + SELECT COALESCE(rpr_grp.id, rpr_sort.id) + 1 AS idp1, + + count(*) OVER w AS cnt + + FROM rpr_grp + + FULL JOIN rpr_sort USING (id) + + GROUP BY (COALESCE(rpr_grp.id, rpr_sort.id) + 1) + + WINDOW w AS (ORDER BY (COALESCE(rpr_grp.id, rpr_sort.id) + 1) ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING+ + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a+) + + DEFINE + + a AS (id + 1) > 0); +(1 row) + +SELECT * FROM rpr_fjcoal_v ORDER BY 1; + idp1 | cnt +------+----- + 2 | 6 + 3 | 0 + 4 | 0 + 5 | 0 + 6 | 0 + 7 | 0 +(6 rows) + +-- The deparsed definition re-parses into an identical view. +CREATE VIEW rpr_fjcoal_v2 AS + SELECT COALESCE(rpr_grp.id, rpr_sort.id) + 1 AS idp1, + count(*) OVER w AS cnt + FROM rpr_grp + FULL JOIN rpr_sort USING (id) + GROUP BY (COALESCE(rpr_grp.id, rpr_sort.id) + 1) + WINDOW w AS (ORDER BY (COALESCE(rpr_grp.id, rpr_sort.id) + 1) ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (a+) + DEFINE + a AS (id + 1) > 0); +SELECT pg_get_viewdef('rpr_fjcoal_v2'::regclass, true) = + pg_get_viewdef('rpr_fjcoal_v'::regclass, true) AS same_definition; + same_definition +----------------- + t +(1 row) + +DROP VIEW rpr_fjcoal_v2; +DROP VIEW rpr_fjcoal_v; DROP TABLE rpr_grp; DROP TABLE rpr_sort; -- SQL function inlining: $1 in DEFINE must be substituted by diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql index db6afdcc257..ac046740ac2 100644 --- a/src/test/regress/sql/rpr_base.sql +++ b/src/test/regress/sql/rpr_base.sql @@ -5778,6 +5778,53 @@ WINDOW w AS ( PATTERN (A) DEFINE A AS true); +-- GROUP BY spelled as the merged column's COALESCE expansion, rather than +-- the join's own name, while DEFINE reads that same column: the two spellings +-- must compare equal despite the different tree shapes. +SELECT id + 1 AS b, count(*) OVER w AS cnt +FROM rpr_grp FULL JOIN rpr_sort USING (id) +GROUP BY COALESCE(rpr_grp.id, rpr_sort.id) + 1 +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A) DEFINE A AS (id + 1) > 0) +ORDER BY 1; + +-- Same construct as a view: the DEFINE clause must deparse to the plain +-- join column, not the two-sided COALESCE GROUP BY computed, or the printed +-- text would not re-parse. +CREATE VIEW rpr_fjcoal_v AS +SELECT COALESCE(rpr_grp.id, rpr_sort.id) + 1 AS idp1, count(*) OVER w AS cnt +FROM rpr_grp FULL JOIN rpr_sort USING (id) +GROUP BY COALESCE(rpr_grp.id, rpr_sort.id) + 1 +WINDOW w AS ( + ORDER BY COALESCE(rpr_grp.id, rpr_sort.id) + 1 + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS id + 1 > 0); + +SELECT pg_get_viewdef('rpr_fjcoal_v'::regclass, true); +SELECT * FROM rpr_fjcoal_v ORDER BY 1; + +-- The deparsed definition re-parses into an identical view. +CREATE VIEW rpr_fjcoal_v2 AS + SELECT COALESCE(rpr_grp.id, rpr_sort.id) + 1 AS idp1, + count(*) OVER w AS cnt + FROM rpr_grp + FULL JOIN rpr_sort USING (id) + GROUP BY (COALESCE(rpr_grp.id, rpr_sort.id) + 1) + WINDOW w AS (ORDER BY (COALESCE(rpr_grp.id, rpr_sort.id) + 1) ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (a+) + DEFINE + a AS (id + 1) > 0); + +SELECT pg_get_viewdef('rpr_fjcoal_v2'::regclass, true) = + pg_get_viewdef('rpr_fjcoal_v'::regclass, true) AS same_definition; + +DROP VIEW rpr_fjcoal_v2; +DROP VIEW rpr_fjcoal_v; + DROP TABLE rpr_grp; DROP TABLE rpr_sort;