v50-0008-refactor-define.patch
application/octet-stream
Filename: v50-0008-refactor-define.patch
Type: application/octet-stream
Part: 16
Message:
Re: Row pattern recognition
Patch
Same data as JSON:
GET /api/v1/attachments/:id/patch
the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes.
API reference →
Format: format-patch
Series: patch v50-0008
Subject: Refactor transformDefineClause in row pattern recognition
| File | + | − |
|---|---|---|
| src/backend/parser/parse_rpr.c | 73 | 90 |
| src/test/regress/expected/rpr_base.out | 77 | 1 |
| src/test/regress/sql/rpr_base.sql | 59 | 0 |
From 60939a2278dcb1d6a66ef906254ae9f1abb15986 Mon Sep 17 00:00:00 2001
From: Henson Choi <assam258@gmail.com>
Date: Wed, 17 Jun 2026 14:55:51 +0900
Subject: [PATCH v50 17/29] Refactor transformDefineClause in row pattern
recognition
Two related cleanups to DEFINE clause parse analysis, with no change to
planner or executor output beyond one error-cursor position:
- Hoist the "DEFINE variable not used in PATTERN" cross-check out of the
recursive validateRPRPatternVarCount() into its caller. The check only
needs to run once, so the rpDefs argument and its NULL-sentinel gating
are gone, and the recursive routine now only counts pattern variables.
- Reorder per-variable DEFINE processing to transformExpr ->
coerce_to_boolean -> pull_var_clause and drop the separate second
coercion pass, so pull_var_clause always operates on the final coerced
expression and a type mismatch is reported before the targetlist is
touched. The duplicate-variable check moves to its own leading loop
and now reports at the later (duplicate) definition.
Add regression coverage for DEFINE coercion and Var propagation: a
boolean-domain predicate (the one case where coerce_to_boolean is not a
no-op), a Var referenced only inside a navigation operation, and
rejection of a non-boolean DEFINE expression.
---
src/backend/parser/parse_rpr.c | 163 +++++++++++--------------
src/test/regress/expected/rpr_base.out | 78 +++++++++++-
src/test/regress/sql/rpr_base.sql | 59 +++++++++
3 files changed, 209 insertions(+), 91 deletions(-)
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 3e6b2e579a3..116cd206e39 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -53,7 +53,7 @@ typedef struct
/* Forward declarations */
static void validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
- List *rpDefs, List **varNames);
+ List **varNames);
static List *transformDefineClause(ParseState *pstate, WindowDef *windef,
List **targetlist);
static bool define_walker(Node *node, void *context);
@@ -192,15 +192,14 @@ transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
* Throws an error if the number of unique variables would require a varId
* greater than RPR_VARID_MAX.
*
- * If rpDefs is non-NULL, each DEFINE variable name is also validated against
- * varNames; any DEFINE name not present in PATTERN is rejected with an error.
- * varNames itself is not extended by this step -- it carries only PATTERN
- * variable names, which is what transformColumnRef checks via
- * p_rpr_pattern_vars to identify pattern variable qualifiers.
+ * varNames collects the unique PATTERN variable names, which is what
+ * transformColumnRef checks via p_rpr_pattern_vars to identify pattern
+ * variable qualifiers. Cross-checking DEFINE variable names against this
+ * list is the caller's responsibility, since it only needs to run once.
*/
static void
validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
- List *rpDefs, List **varNames)
+ List **varNames)
{
/* Pattern node must exist - parser always provides non-NULL root */
Assert(node != NULL);
@@ -255,39 +254,10 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
/* Recurse into children */
foreach_node(RPRPatternNode, child, node->children)
{
- validateRPRPatternVarCount(pstate, child, NULL, varNames);
+ validateRPRPatternVarCount(pstate, child, varNames);
}
break;
}
-
- /*
- * After the top-level call, validate that every DEFINE variable name is
- * present in the PATTERN variable list; reject names not used in PATTERN.
- * This is only done once at the outermost recursion level, detected by
- * rpDefs being non-NULL (recursive calls pass NULL).
- */
- if (rpDefs)
- {
- foreach_node(ResTarget, rt, rpDefs)
- {
- bool found = false;
-
- foreach_node(String, varname, *varNames)
- {
- if (strcmp(strVal(varname), rt->name) == 0)
- {
- found = true;
- break;
- }
- }
- if (!found)
- ereport(ERROR,
- errcode(ERRCODE_SYNTAX_ERROR),
- errmsg("DEFINE variable \"%s\" is not used in PATTERN",
- rt->name),
- parser_errposition(pstate, rt->location));
- }
- }
}
/*
@@ -296,14 +266,16 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
*
* First:
* 1. Validates PATTERN variable count and collects RPR variable names
+ * 2. Rejects DEFINE variables not used in PATTERN
+ * 3. Checks for duplicate variable names in DEFINE clause
*
* Then for each DEFINE variable:
- * 2. Checks for duplicate variable names in DEFINE clause
- * 3. Transforms expression via transformExpr() and ensures referenced
- * Var nodes are present in the query targetlist (via pull_var_clause)
- * 4. Creates defineClause entry with proper resname (pattern variable name)
- * 5. Coerces expressions to boolean type
- * 6. Marks column origins and assigns collation information
+ * 4. Transforms expression via transformExpr() and coerces it to boolean
+ * 5. Creates defineClause entry with proper resname (pattern variable name)
+ * 6. Ensures referenced Var nodes are present in the query targetlist (via
+ * pull_var_clause)
+ *
+ * Finally marks column origins and assigns collation information.
*
* Note: Variables not in DEFINE are evaluated as TRUE by the executor.
* Variables in DEFINE but not in PATTERN are rejected as an error.
@@ -316,9 +288,7 @@ static List *
transformDefineClause(ParseState *pstate, WindowDef *windef,
List **targetlist)
{
- List *restargets;
List *defineClause = NIL;
- char *name;
List *patternVarNames = NIL;
/*
@@ -328,56 +298,86 @@ transformDefineClause(ParseState *pstate, WindowDef *windef,
Assert(windef->rpCommonSyntax->rpDefs != NULL);
/*
- * Validate PATTERN variable count, reject DEFINE variables not used in
- * PATTERN, and collect PATTERN variable names for transformColumnRef.
+ * Validate PATTERN variable count and collect the PATTERN variable names
+ * for transformColumnRef.
*/
validateRPRPatternVarCount(pstate, windef->rpCommonSyntax->rpPattern,
- windef->rpCommonSyntax->rpDefs,
&patternVarNames);
pstate->p_rpr_pattern_vars = patternVarNames;
+ /*
+ * Reject any DEFINE variable whose name does not appear in PATTERN. This
+ * cross-check only needs to run once, so it lives here in the caller
+ * rather than in the recursive validateRPRPatternVarCount().
+ */
+ foreach_node(ResTarget, rt, windef->rpCommonSyntax->rpDefs)
+ {
+ bool found = false;
+
+ foreach_node(String, varname, patternVarNames)
+ {
+ if (strcmp(strVal(varname), rt->name) == 0)
+ {
+ found = true;
+ break;
+ }
+ }
+ if (!found)
+ ereport(ERROR,
+ errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("DEFINE variable \"%s\" is not used in PATTERN",
+ rt->name),
+ parser_errposition(pstate, rt->location));
+ }
+
/*
* Check for duplicate row pattern definition variables. The standard
* requires that no two row pattern definition variable names shall be
- * equivalent.
+ * equivalent. Report the error at the later (duplicate) definition.
*/
- restargets = NIL;
foreach_node(ResTarget, restarget, windef->rpCommonSyntax->rpDefs)
{
- TargetEntry *teDefine;
- Node *expr;
- List *vars;
-
- name = restarget->name;
-
- foreach_node(ResTarget, r, restargets)
+ foreach_node(ResTarget, prior, windef->rpCommonSyntax->rpDefs)
{
- char *n;
-
- n = r->name;
-
- if (!strcmp(n, name))
+ if (prior == restarget)
+ break;
+ if (strcmp(prior->name, restarget->name) == 0)
ereport(ERROR,
errcode(ERRCODE_SYNTAX_ERROR),
errmsg("DEFINE variable \"%s\" appears more than once",
- name),
- parser_errposition(pstate, exprLocation((Node *) r)));
+ restarget->name),
+ parser_errposition(pstate,
+ exprLocation((Node *) restarget)));
}
+ }
- restargets = lappend(restargets, restarget);
+ foreach_node(ResTarget, restarget, windef->rpCommonSyntax->rpDefs)
+ {
+ TargetEntry *teDefine;
+ Node *expr;
+ List *vars;
/*
- * Transform the DEFINE expression. We must NOT add the whole
- * expression to the query targetlist, because it may contain
- * RPRNavExpr nodes (PREV/NEXT/FIRST/LAST) that can only be evaluated
- * inside the owning WindowAgg.
- *
- * Instead, we transform the expression directly and only ensure that
- * the individual Var nodes it references are present in the
- * targetlist, so the planner can propagate the referenced columns.
+ * Transform the DEFINE expression and coerce it to boolean. We must
+ * NOT add the whole expression to the query targetlist, because it
+ * may contain RPRNavExpr nodes (PREV/NEXT/FIRST/LAST) that can only
+ * be evaluated inside the owning WindowAgg. Coercing here, before
+ * pull_var_clause, keeps pull_var_clause operating on the final
+ * expression form and surfaces a type mismatch before the targetlist
+ * is touched.
*/
expr = transformExpr(pstate, restarget->val,
EXPR_KIND_RPR_DEFINE);
+ expr = coerce_to_boolean(pstate, expr, "DEFINE");
+
+ /* Build the defineClause entry directly from the transformed expr */
+ teDefine = makeTargetEntry((Expr *) expr,
+ list_length(defineClause) + 1,
+ pstrdup(restarget->name),
+ true);
+
+ /* build transformed DEFINE clause (list of TargetEntry) */
+ defineClause = lappend(defineClause, teDefine);
/*
* Pull out Var nodes from the transformed expression and ensure each
@@ -412,26 +412,9 @@ transformDefineClause(ParseState *pstate, WindowDef *windef,
}
}
list_free(vars);
-
- /* Build the defineClause entry directly from the transformed expr */
- teDefine = makeTargetEntry((Expr *) expr,
- list_length(defineClause) + 1,
- pstrdup(name),
- true);
-
- /* build transformed DEFINE clause (list of TargetEntry) */
- defineClause = lappend(defineClause, teDefine);
}
- list_free(restargets);
pstate->p_rpr_pattern_vars = NIL;
- /*
- * Make sure that the row pattern definition search condition is a boolean
- * expression.
- */
- foreach_ptr(TargetEntry, te, defineClause)
- te->expr = (Expr *) coerce_to_boolean(pstate, (Node *) te->expr, "DEFINE");
-
/*
* Validate DEFINE expressions: nested PREV/NEXT, column references,
* compound flatten, volatile callees -- all in a single walk per
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index cf158e1c043..80fabde514c 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -236,7 +236,7 @@ WINDOW w AS (
);
ERROR: DEFINE variable "a" appears more than once
LINE 7: DEFINE A AS id > 0, A AS id < 10
- ^
+ ^
DROP TABLE rpr_dup;
-- Boolean coercion
CREATE TABLE rpr_bool (id INT, flag BOOLEAN);
@@ -319,6 +319,82 @@ DROP CAST (truthyint AS boolean);
DROP FUNCTION truthyint_to_bool(truthyint);
DROP TYPE truthyint;
DROP TABLE rpr_bool;
+-- Coercion over a boolean domain is not a no-op; the wrapped Var must still
+-- propagate when referenced only in DEFINE (flag is not in the select list)
+CREATE DOMAIN boolish AS boolean;
+CREATE TABLE rpr_domain (id int, flag boolish);
+INSERT INTO rpr_domain VALUES (1, true), (2, false), (3, true);
+SELECT id, COUNT(*) OVER w AS cnt
+FROM rpr_domain
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS flag
+)
+ORDER BY id;
+ id | cnt
+----+-----
+ 1 | 1
+ 2 | 0
+ 3 | 1
+(3 rows)
+
+DROP TABLE rpr_domain;
+DROP DOMAIN boolish;
+-- A Var referenced only inside a navigation operation must still propagate
+-- (val appears only inside PREV(), not as a bare operand or in the select list)
+CREATE TABLE rpr_nav (id int, val int);
+INSERT INTO rpr_nav VALUES (1, 0), (2, 1), (3, 0), (4, 2);
+SELECT id, COUNT(*) OVER w AS cnt
+FROM rpr_nav
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (UP+)
+ DEFINE UP AS id > PREV(val)
+)
+ORDER BY id;
+ id | cnt
+----+-----
+ 1 | 0
+ 2 | 3
+ 3 | 0
+ 4 | 0
+(4 rows)
+
+DROP TABLE rpr_nav;
+-- A non-boolean DEFINE expression is rejected
+CREATE TABLE rpr_noncoerce (id int, n int);
+INSERT INTO rpr_noncoerce VALUES (1, 1);
+SELECT id, COUNT(*) OVER w AS cnt
+FROM rpr_noncoerce
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS n
+);
+ERROR: argument of DEFINE must be type boolean, not type integer
+LINE 7: DEFINE A AS n
+ ^
+DROP TABLE rpr_noncoerce;
+-- A non-boolean later DEFINE is rejected at its own definition even when an
+-- earlier DEFINE variable is valid
+CREATE TABLE rpr_noncoerce2 (id int, n int);
+INSERT INTO rpr_noncoerce2 VALUES (1, 1);
+SELECT id, COUNT(*) OVER w AS cnt
+FROM rpr_noncoerce2
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE A AS id > 0, B AS n
+);
+ERROR: argument of DEFINE must be type boolean, not type integer
+LINE 7: DEFINE A AS id > 0, B AS n
+ ^
+DROP TABLE rpr_noncoerce2;
-- Complex expressions
CREATE TABLE rpr_complex (id INT, val1 INT, val2 INT);
INSERT INTO rpr_complex VALUES (1, 10, 20), (2, 15, 25), (3, 20, 30);
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index e71f0dd3680..21840aa77be 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -261,6 +261,65 @@ DROP TYPE truthyint;
DROP TABLE rpr_bool;
+-- Coercion over a boolean domain is not a no-op; the wrapped Var must still
+-- propagate when referenced only in DEFINE (flag is not in the select list)
+CREATE DOMAIN boolish AS boolean;
+CREATE TABLE rpr_domain (id int, flag boolish);
+INSERT INTO rpr_domain VALUES (1, true), (2, false), (3, true);
+SELECT id, COUNT(*) OVER w AS cnt
+FROM rpr_domain
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS flag
+)
+ORDER BY id;
+DROP TABLE rpr_domain;
+DROP DOMAIN boolish;
+
+-- A Var referenced only inside a navigation operation must still propagate
+-- (val appears only inside PREV(), not as a bare operand or in the select list)
+CREATE TABLE rpr_nav (id int, val int);
+INSERT INTO rpr_nav VALUES (1, 0), (2, 1), (3, 0), (4, 2);
+SELECT id, COUNT(*) OVER w AS cnt
+FROM rpr_nav
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (UP+)
+ DEFINE UP AS id > PREV(val)
+)
+ORDER BY id;
+DROP TABLE rpr_nav;
+
+-- A non-boolean DEFINE expression is rejected
+CREATE TABLE rpr_noncoerce (id int, n int);
+INSERT INTO rpr_noncoerce VALUES (1, 1);
+SELECT id, COUNT(*) OVER w AS cnt
+FROM rpr_noncoerce
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS n
+);
+DROP TABLE rpr_noncoerce;
+
+-- A non-boolean later DEFINE is rejected at its own definition even when an
+-- earlier DEFINE variable is valid
+CREATE TABLE rpr_noncoerce2 (id int, n int);
+INSERT INTO rpr_noncoerce2 VALUES (1, 1);
+SELECT id, COUNT(*) OVER w AS cnt
+FROM rpr_noncoerce2
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE A AS id > 0, B AS n
+);
+DROP TABLE rpr_noncoerce2;
+
-- Complex expressions
CREATE TABLE rpr_complex (id INT, val1 INT, val2 INT);
INSERT INTO rpr_complex VALUES (1, 10, 20), (2, 15, 25), (3, 20, 30);
--
2.50.1 (Apple Git-155)