nocfbot-0059-Allow-a-row-pattern-quantifier-with-no-space-befo.txt

text/plain

Filename: nocfbot-0059-Allow-a-row-pattern-quantifier-with-no-space-befo.txt
Type: text/plain
Part: 57
Message: Re: Row pattern recognition
From 657e8c72bca567387d073a629cf5813a7871ae2d Mon Sep 17 00:00:00 2001
From: Henson Choi <assam258@gmail.com>
Date: Mon, 8 Jun 2026 11:23:03 +0900
Subject: [PATCH 59/68] Allow a row pattern quantifier with no space before the
 alternation operator

The scanner lexes an operator such as "*|" as a single token, so a PATTERN
like (A*|B) previously failed with "unsupported quantifier".  Recognize these
glued forms in the quantifier rules and, once the surrounding sequence is
built, re-split it at the affected term so "|" keeps its lowest precedence;
(A*|B) now parses identically to the spaced (A* | B).  This covers the op-char
quantifiers (*| +| ?| *?| +?| ??|), the reluctant range forms ({n}?| and the
like), and mixed spacing such as "A* ?|B".  A dangling "|" with no right-hand
pattern is still rejected.
---
 src/backend/parser/gram.y              | 195 +++++++++++++++++++--
 src/backend/parser/parse_rpr.c         |   6 +
 src/include/nodes/parsenodes.h         |   8 +
 src/test/regress/expected/rpr_base.out | 228 +++++++++++++++++++++++++
 src/test/regress/sql/rpr_base.sql      | 112 ++++++++++++
 5 files changed, 535 insertions(+), 14 deletions(-)

diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index a2fafb717cd..147b5f37293 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -210,6 +210,8 @@ static void preprocess_pub_all_objtype_list(List *all_objects_list,
 static void preprocess_pubobj_list(List *pubobjspec_list,
 								   core_yyscan_t yyscanner);
 static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
+static RPRPatternNode *makeRPRSeqOrSingle(List *children, int location);
+static RPRPatternNode *splitRPRTrailingAlt(RPRPatternNode *node, core_yyscan_t yyscanner);
 static RPRPatternNode *makeRPRQuantifier(int min, int max, ParseLoc reluctant, int location,
 									   core_yyscan_t yyscanner);
 
@@ -17624,23 +17626,30 @@ row_pattern:
 		;
 
 row_pattern_alt:
-			row_pattern_seq						{ $$ = $1; }
+			row_pattern_seq
+				{
+					$$ = (Node *) splitRPRTrailingAlt((RPRPatternNode *) $1,
+													 yyscanner);
+				}
 			| row_pattern_alt '|' row_pattern_seq
 				{
 					RPRPatternNode *n;
+					RPRPatternNode *rhs = splitRPRTrailingAlt((RPRPatternNode *) $3,
+															 yyscanner);
+
 					/* If left side is already ALT, append to it */
 					if (IsA($1, RPRPatternNode) &&
 						((RPRPatternNode *) $1)->nodeType == RPR_PATTERN_ALT)
 					{
 						n = (RPRPatternNode *) $1;
-						n->children = lappend(n->children, $3);
+						n->children = lappend(n->children, rhs);
 						$$ = (Node *) n;
 					}
 					else
 					{
 						n = makeNode(RPRPatternNode);
 						n->nodeType = RPR_PATTERN_ALT;
-						n->children = list_make2($1, $3);
+						n->children = list_make2($1, rhs);
 						n->min = 1;
 						n->max = 1;
 						n->reluctant = false;
@@ -17656,7 +17665,12 @@ row_pattern_seq:
 			| row_pattern_seq row_pattern_term
 				{
 					RPRPatternNode *n;
-					/* If left side is already SEQ, append to it */
+
+					/*
+					 * If left side is already SEQ, append to it.  A glued
+					 * quantifier's trailing_alt stays on the child term;
+					 * row_pattern_alt splits on it once the seq is complete.
+					 */
 					if (IsA($1, RPRPatternNode) &&
 						((RPRPatternNode *) $1)->nodeType == RPR_PATTERN_SEQ)
 					{
@@ -17689,6 +17703,7 @@ row_pattern_term:
 					n->max = q->max;
 					n->reluctant = q->reluctant;
 					n->reluctant_location = q->reluctant_location;
+					n->trailing_alt = q->trailing_alt;
 					$$ = (Node *) n;
 				}
 		;
@@ -17739,6 +17754,36 @@ row_pattern_quantifier_opt:
 						$$ = (Node *) makeRPRQuantifier(1, INT_MAX, @1 + 1, @1, yyscanner);
 					else if (strcmp($1, "??") == 0)
 						$$ = (Node *) makeRPRQuantifier(0, 1, @1 + 1, @1, yyscanner);
+					else if (strcmp($1, "*|") == 0)
+					{
+						$$ = (Node *) makeRPRQuantifier(0, INT_MAX, -1, @1, yyscanner);
+						((RPRPatternNode *) $$)->trailing_alt = true;
+					}
+					else if (strcmp($1, "+|") == 0)
+					{
+						$$ = (Node *) makeRPRQuantifier(1, INT_MAX, -1, @1, yyscanner);
+						((RPRPatternNode *) $$)->trailing_alt = true;
+					}
+					else if (strcmp($1, "?|") == 0)
+					{
+						$$ = (Node *) makeRPRQuantifier(0, 1, -1, @1, yyscanner);
+						((RPRPatternNode *) $$)->trailing_alt = true;
+					}
+					else if (strcmp($1, "*?|") == 0)
+					{
+						$$ = (Node *) makeRPRQuantifier(0, INT_MAX, @1 + 1, @1, yyscanner);
+						((RPRPatternNode *) $$)->trailing_alt = true;
+					}
+					else if (strcmp($1, "+?|") == 0)
+					{
+						$$ = (Node *) makeRPRQuantifier(1, INT_MAX, @1 + 1, @1, yyscanner);
+						((RPRPatternNode *) $$)->trailing_alt = true;
+					}
+					else if (strcmp($1, "??|") == 0)
+					{
+						$$ = (Node *) makeRPRQuantifier(0, 1, @1 + 1, @1, yyscanner);
+						((RPRPatternNode *) $$)->trailing_alt = true;
+					}
 					else
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
@@ -17749,33 +17794,60 @@ row_pattern_quantifier_opt:
 			/* RELUCTANT quantifiers (when lexer separates tokens) */
 			| '*' Op
 				{
-					if (strcmp($2, "?") != 0)
+					if (strcmp($2, "?") == 0)
+						$$ = (Node *) makeRPRQuantifier(0, INT_MAX, @2, @1, yyscanner);
+					else if (strcmp($2, "?|") == 0)
+					{
+						/* "A* ?|B" = reluctant "A*?" plus alternation */
+						$$ = (Node *) makeRPRQuantifier(0, INT_MAX, @2, @1, yyscanner);
+						((RPRPatternNode *) $$)->trailing_alt = true;
+					}
+					else
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
 								errmsg("invalid token after \"*\" quantifier"),
 								errhint("Did you mean \"*?\" for reluctant quantifier?"),
 								parser_errposition(@2));
-					$$ = (Node *) makeRPRQuantifier(0, INT_MAX, @2, @1, yyscanner);
 				}
 			| '+' Op
 				{
-					if (strcmp($2, "?") != 0)
+					if (strcmp($2, "?") == 0)
+						$$ = (Node *) makeRPRQuantifier(1, INT_MAX, @2, @1, yyscanner);
+					else if (strcmp($2, "?|") == 0)
+					{
+						/* "A+ ?|B" = reluctant "A+?" plus alternation */
+						$$ = (Node *) makeRPRQuantifier(1, INT_MAX, @2, @1, yyscanner);
+						((RPRPatternNode *) $$)->trailing_alt = true;
+					}
+					else
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
 								errmsg("invalid token after \"+\" quantifier"),
 								errhint("Did you mean \"+?\" for reluctant quantifier?"),
 								parser_errposition(@2));
-					$$ = (Node *) makeRPRQuantifier(1, INT_MAX, @2, @1, yyscanner);
 				}
 			| Op Op
 				{
-					if (strcmp($1, "?") != 0 || strcmp($2, "?") != 0)
+					if (strcmp($1, "?") != 0)
+						ereport(ERROR,
+								errcode(ERRCODE_SYNTAX_ERROR),
+								errmsg("invalid quantifier combination"),
+								errhint("Did you mean \"??\" for reluctant quantifier?"),
+								parser_errposition(@1));
+					if (strcmp($2, "?") == 0)
+						$$ = (Node *) makeRPRQuantifier(0, 1, @2, @1, yyscanner);
+					else if (strcmp($2, "?|") == 0)
+					{
+						/* "A? ?|B" = reluctant "A??" plus alternation */
+						$$ = (Node *) makeRPRQuantifier(0, 1, @2, @1, yyscanner);
+						((RPRPatternNode *) $$)->trailing_alt = true;
+					}
+					else
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
 								errmsg("invalid quantifier combination"),
 								errhint("Did you mean \"??\" for reluctant quantifier?"),
 								parser_errposition(@1));
-					$$ = (Node *) makeRPRQuantifier(0, 1, @2, @1, yyscanner);
 				}
 			/* {n}, {n,}, {,m}, {n,m} quantifiers */
 			| '{' Iconst '}'
@@ -17822,7 +17894,7 @@ row_pattern_quantifier_opt:
 			/* Reluctant versions: {n}?, {n,}?, {,m}?, {n,m}? */
 			| '{' Iconst '}' Op
 				{
-					if (strcmp($4, "?") != 0)
+					if (strcmp($4, "?") != 0 && strcmp($4, "?|") != 0)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
 								errmsg("invalid token after range quantifier"),
@@ -17834,10 +17906,12 @@ row_pattern_quantifier_opt:
 								errmsg("quantifier bound must be between 1 and %d", INT_MAX - 1),
 								parser_errposition(@2));
 					$$ = (Node *) makeRPRQuantifier($2, $2, @4, @1, yyscanner);
+					if (strcmp($4, "?|") == 0)
+						((RPRPatternNode *) $$)->trailing_alt = true;
 				}
 			| '{' Iconst ',' '}' Op
 				{
-					if (strcmp($5, "?") != 0)
+					if (strcmp($5, "?") != 0 && strcmp($5, "?|") != 0)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
 								errmsg("invalid token after range quantifier"),
@@ -17849,10 +17923,12 @@ row_pattern_quantifier_opt:
 								errmsg("quantifier bound must be between 0 and %d", INT_MAX - 1),
 								parser_errposition(@2));
 					$$ = (Node *) makeRPRQuantifier($2, INT_MAX, @5, @1, yyscanner);
+					if (strcmp($5, "?|") == 0)
+						((RPRPatternNode *) $$)->trailing_alt = true;
 				}
 			| '{' ',' Iconst '}' Op
 				{
-					if (strcmp($5, "?") != 0)
+					if (strcmp($5, "?") != 0 && strcmp($5, "?|") != 0)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
 								errmsg("invalid token after range quantifier"),
@@ -17864,10 +17940,12 @@ row_pattern_quantifier_opt:
 								errmsg("quantifier bound must be between 1 and %d", INT_MAX - 1),
 								parser_errposition(@3));
 					$$ = (Node *) makeRPRQuantifier(0, $3, @5, @1, yyscanner);
+					if (strcmp($5, "?|") == 0)
+						((RPRPatternNode *) $$)->trailing_alt = true;
 				}
 			| '{' Iconst ',' Iconst '}' Op
 				{
-					if (strcmp($6, "?") != 0)
+					if (strcmp($6, "?") != 0 && strcmp($6, "?|") != 0)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
 								errmsg("invalid token after range quantifier"),
@@ -17884,6 +17962,8 @@ row_pattern_quantifier_opt:
 								errmsg("quantifier minimum bound must not exceed maximum"),
 								parser_errposition(@2));
 					$$ = (Node *) makeRPRQuantifier($2, $4, @6, @1, yyscanner);
+					if (strcmp($6, "?|") == 0)
+						((RPRPatternNode *) $$)->trailing_alt = true;
 				}
 		;
 
@@ -21325,6 +21405,93 @@ makeRPRQuantifier(int min, int max, ParseLoc reluctant_location, int location,
 	return n;
 }
 
+/*
+ * Build a SEQ node from children, or return the lone child unchanged.
+ */
+static RPRPatternNode *
+makeRPRSeqOrSingle(List *children, int location)
+{
+	RPRPatternNode *n;
+
+	if (list_length(children) == 1)
+		return (RPRPatternNode *) linitial(children);
+
+	n = makeNode(RPRPatternNode);
+	n->nodeType = RPR_PATTERN_SEQ;
+	n->children = children;
+	n->min = 1;
+	n->max = 1;
+	n->reluctant = false;
+	n->reluctant_location = -1;
+	n->location = location;
+	return n;
+}
+
+/*
+ * A glued quantifier such as "A*|" leaves trailing_alt set on its term while
+ * the enclosing sequence is built.  Once the sequence is complete, split it at
+ * the flagged term into alt(left, right), where the right operand is the whole
+ * remaining sequence -- this keeps "|" as the lowest-precedence operator, so
+ * "A*|B C" parses as "A* | (B C)", identical to the spaced form.  A flag with
+ * nothing to its right is a dangling "|" and is rejected.
+ */
+static RPRPatternNode *
+splitRPRTrailingAlt(RPRPatternNode *node, core_yyscan_t yyscanner)
+{
+	ListCell   *lc;
+	int			i = 0;
+
+	if (node->nodeType != RPR_PATTERN_SEQ)
+	{
+		if (node->trailing_alt)
+		{
+			node->trailing_alt = false;
+			ereport(ERROR,
+					errcode(ERRCODE_SYNTAX_ERROR),
+					errmsg("alternation operator \"|\" requires a pattern on both sides"),
+					parser_errposition(node->location));
+		}
+		return node;
+	}
+
+	foreach(lc, node->children)
+	{
+		RPRPatternNode *child = (RPRPatternNode *) lfirst(lc);
+
+		if (child->trailing_alt)
+		{
+			List	   *lefthalf = list_copy_head(node->children, i + 1);
+			List	   *righthalf = list_copy_tail(node->children, i + 1);
+			RPRPatternNode *altn;
+			RPRPatternNode *rightnode;
+
+			child->trailing_alt = false;
+			if (righthalf == NIL)
+				ereport(ERROR,
+						errcode(ERRCODE_SYNTAX_ERROR),
+						errmsg("alternation operator \"|\" requires a pattern on both sides"),
+						parser_errposition(node->location));
+
+			/* the right branch starts at its own first element, not the seq start */
+			rightnode = splitRPRTrailingAlt(makeRPRSeqOrSingle(righthalf,
+															   ((RPRPatternNode *) linitial(righthalf))->location),
+											yyscanner);
+			altn = makeNode(RPRPatternNode);
+			altn->nodeType = RPR_PATTERN_ALT;
+			altn->children = list_make2(makeRPRSeqOrSingle(lefthalf, node->location),
+										rightnode);
+			altn->min = 1;
+			altn->max = 1;
+			altn->reluctant = false;
+			altn->reluctant_location = -1;
+			altn->location = node->location;
+			return altn;
+		}
+		i++;
+	}
+	return node;
+}
+
 /* parser_init()
  * Initialize to parse one query string
  */
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index c9469b56b7b..4e1d2650cf6 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -213,6 +213,12 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
 	/* Pattern node must exist - parser always provides non-NULL root */
 	Assert(node != NULL);
 
+	/*
+	 * trailing_alt is a transient grammar flag; splitRPRTrailingAlt must have
+	 * cleared it on every node before the pattern reaches parse analysis.
+	 */
+	Assert(!node->trailing_alt);
+
 	check_stack_depth();
 
 	switch (node->nodeType)
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 5200182aa46..e371f04a403 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -632,6 +632,14 @@ typedef struct RPRPatternNode
 	ParseLoc	location;		/* token location, or -1 */
 	char	   *varName;		/* VAR: variable name */
 	List	   *children;		/* SEQ, ALT, GROUP: child nodes */
+
+	/*
+	 * Transient parse flag, cleared by splitRPRTrailingAlt before the tree is
+	 * finalized: a glued quantifier such as "*|" is immediately followed by
+	 * the alternation operator '|'.  It is always false in a finalized tree,
+	 * so query_jumble_ignore keeps it off the pg_stat_statements queryid.
+	 */
+	bool		trailing_alt pg_node_attr(query_jumble_ignore);
 } RPRPatternNode;
 
 /*
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index c50c7a5f6a8..1fcb2ce22f0 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -13,6 +13,7 @@
 --   Navigation Functions Tests
 --   SKIP TO / INITIAL Tests
 --   Serialization/Deserialization Tests (objects kept for pg_upgrade/pg_dump)
+--   Glued Quantifier / Alternation Tests
 --   Error Cases Tests
 --   Window Deduplication Tests
 --
@@ -3083,6 +3084,233 @@ SELECT pg_get_viewdef('rpr_quant_n_plus_v'::regclass);
    a AS (val > 0) );
 (1 row)
 
+-- ============================================================
+-- Glued Quantifier / Alternation Tests
+-- ============================================================
+CREATE TABLE rpr_glue (id INT, val INT);
+INSERT INTO rpr_glue VALUES (1, 5), (2, 8), (3, 9), (4, -1), (5, 6), (6, -2);
+-- Quantifier glued to the alternation operator '|' without a space (0059).
+-- The lexer glues the trailing '|' into one Op token; the grammar reattaches it
+-- as the lowest-precedence alternation once the surrounding sequence is built.
+-- Deparse is canonical, so the glued, spaced, and mixed-spacing forms all
+-- reduce to the same PATTERN -- one deparse per shape proves the parse tree.
+-- Op-char quantifiers (*, +, ?, *?, +?, ??) glued to '|'.
+CREATE VIEW rpr_dp_op AS SELECT
+    count(*) OVER w1 AS w1, count(*) OVER w2 AS w2, count(*) OVER w3 AS w3,
+    count(*) OVER w4 AS w4, count(*) OVER w5 AS w5, count(*) OVER w6 AS w6
+FROM rpr_glue
+WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B) DEFINE A AS val > 0, B AS val <= 0),
+       w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+|B) DEFINE A AS val > 0, B AS val <= 0),
+       w3 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w4 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w5 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w6 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A??|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_op'), E'\n')) AS line WHERE line ~ 'PATTERN';
+         line         
+----------------------
+   PATTERN (a* | b) 
+   PATTERN (a+ | b) 
+   PATTERN (a? | b) 
+   PATTERN (a*? | b) 
+   PATTERN (a+? | b) 
+   PATTERN (a?? | b) 
+(6 rows)
+
+DROP VIEW rpr_dp_op;
+-- Spaced reference: the fully-spaced canonical forms.  Identical deparse to the
+-- glued rpr_dp_op w1/w4 above completes the glued = spaced = mixed equivalence.
+CREATE VIEW rpr_dp_spc AS SELECT count(*) OVER w1 AS w1, count(*) OVER w2 AS w2
+FROM rpr_glue
+WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A* | B) DEFINE A AS val > 0, B AS val <= 0),
+       w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*? | B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_spc'), E'\n')) AS line WHERE line ~ 'PATTERN';
+         line         
+----------------------
+   PATTERN (a* | b) 
+   PATTERN (a*? | b) 
+(2 rows)
+
+DROP VIEW rpr_dp_spc;
+-- Range quantifiers glued to '|': non-reluctant {n}| (} + char '|') and
+-- reluctant {n}?| (} + Op "?|").
+CREATE VIEW rpr_dp_rng AS SELECT
+    count(*) OVER w1 AS w1, count(*) OVER w2 AS w2, count(*) OVER w3 AS w3, count(*) OVER w4 AS w4,
+    count(*) OVER w5 AS w5, count(*) OVER w6 AS w6, count(*) OVER w7 AS w7, count(*) OVER w8 AS w8
+FROM rpr_glue
+WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2}|B) DEFINE A AS val > 0, B AS val <= 0),
+       w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2,}|B) DEFINE A AS val > 0, B AS val <= 0),
+       w3 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{,3}|B) DEFINE A AS val > 0, B AS val <= 0),
+       w4 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2,3}|B) DEFINE A AS val > 0, B AS val <= 0),
+       w5 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2}?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w6 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2,}?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w7 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{,3}?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w8 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2,3}?|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_rng'), E'\n')) AS line WHERE line ~ 'PATTERN';
+           line           
+--------------------------
+   PATTERN (a{2} | b) 
+   PATTERN (a{2,} | b) 
+   PATTERN (a{0,3} | b) 
+   PATTERN (a{2,3} | b) 
+   PATTERN (a{2}? | b) 
+   PATTERN (a{2,}? | b) 
+   PATTERN (a{0,3}? | b) 
+   PATTERN (a{2,3}? | b) 
+(8 rows)
+
+DROP VIEW rpr_dp_rng;
+-- Mixed spacing: a space inside the quantifier with '|' still glued.
+-- "A* ?|B" = '*' + Op"?|" = reluctant "A*?" plus alternation.
+CREATE VIEW rpr_dp_mix AS SELECT count(*) OVER w1 AS w1, count(*) OVER w2 AS w2, count(*) OVER w3 AS w3
+FROM rpr_glue
+WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A* ?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+ ?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w3 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A? ?|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_mix'), E'\n')) AS line WHERE line ~ 'PATTERN';
+         line         
+----------------------
+   PATTERN (a*? | b) 
+   PATTERN (a+? | b) 
+   PATTERN (a?? | b) 
+(3 rows)
+
+DROP VIEW rpr_dp_mix;
+-- Structure: precedence (| is lowest, so its right operand is the whole
+-- following sequence), chaining, concatenation, and grouping.
+CREATE VIEW rpr_dp_struct AS SELECT
+    count(*) OVER w1 AS w1, count(*) OVER w2 AS w2, count(*) OVER w3 AS w3,
+    count(*) OVER w4 AS w4, count(*) OVER w5 AS w5, count(*) OVER w6 AS w6
+FROM rpr_glue
+WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B C) DEFINE A AS val > 0, B AS val <= 0, C AS val < 100),
+       w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B*|C) DEFINE A AS val > 0, B AS val <= 0, C AS val < 100),
+       w3 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A B*|C D) DEFINE A AS val > 0, B AS val <= 0, C AS val < 100, D AS val > 5),
+       w4 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A*|B)) DEFINE A AS val > 0, B AS val <= 0),
+       w5 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|(B|C)) DEFINE A AS val > 0, B AS val <= 0, C AS val < 100),
+       w6 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A*|B)+) DEFINE A AS val > 0, B AS val <= 0);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_struct'), E'\n')) AS line WHERE line ~ 'PATTERN';
+           line            
+---------------------------
+   PATTERN (a* | b c) 
+   PATTERN (a* | b* | c) 
+   PATTERN (a b* | c d) 
+   PATTERN ((a* | b)) 
+   PATTERN (a* | (b | c)) 
+   PATTERN ((a* | b)+) 
+(6 rows)
+
+DROP VIEW rpr_dp_struct;
+-- Execution semantics (deparse cannot show reluctant shortest-match).  The
+-- rpr_glue rows -- an A-run followed by B rows -- make the '|B' alternative
+-- reachable: with "*" the greedy form matches the whole run while the
+-- reluctant form matches empty; with "+" the greedy form matches the run and
+-- the reluctant form matches one row, and on a B row (where "A+" fails) the B
+-- alternative fires.
+SELECT id, val,
+       count(*) OVER gs AS gstar, count(*) OVER rs AS rstar,
+       count(*) OVER gp AS gplus, count(*) OVER rp AS rplus
+FROM rpr_glue
+WINDOW gs AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B) DEFINE A AS val > 0, B AS val <= 0),
+       rs AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*?|B) DEFINE A AS val > 0, B AS val <= 0),
+       gp AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+|B) DEFINE A AS val > 0, B AS val <= 0),
+       rp AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+?|B) DEFINE A AS val > 0, B AS val <= 0)
+ORDER BY id;
+ id | val | gstar | rstar | gplus | rplus 
+----+-----+-------+-------+-------+-------
+  1 |   5 |     3 |     0 |     3 |     1
+  2 |   8 |     0 |     0 |     0 |     1
+  3 |   9 |     0 |     0 |     0 |     1
+  4 |  -1 |     1 |     1 |     1 |     1
+  5 |   6 |     1 |     0 |     1 |     1
+  6 |  -2 |     1 |     1 |     1 |     1
+(6 rows)
+
+-- Patterns that must stay rejected.  "&" is an invalid op; a '|' with an empty
+-- side (leading, trailing, doubled, or alone in a group) has no operand; "||"
+-- and "*||" are doubled pipes; "A* *|B"/"A* *?|B"/"A{2}*?|B" are doubled
+-- quantifiers.
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A&B) DEFINE A AS val > 0);
+ERROR:  unsupported quantifier "&"
+LINE 1: ...EEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A&B) DEFINE...
+                                                             ^
+HINT:  Valid quantifiers are: *, +, ?, *?, +?, ??, {n}, {n,}, {,m}, {n,m} and their reluctant versions.
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|) DEFINE A AS val > 0);
+ERROR:  alternation operator "|" requires a pattern on both sides
+LINE 1: ...WEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|) DEFIN...
+                                                             ^
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*| |B) DEFINE A AS val > 0, B AS val <= 0);
+ERROR:  alternation operator "|" requires a pattern on both sides
+LINE 1: ...WEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*| |B) DE...
+                                                             ^
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*||B) DEFINE A AS val > 0, B AS val <= 0);
+ERROR:  unsupported quantifier "*||"
+LINE 1: ...EEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*||B) DEFI...
+                                                             ^
+HINT:  Valid quantifiers are: *, +, ?, *?, +?, ??, {n}, {n,}, {,m}, {n,m} and their reluctant versions.
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A||B) DEFINE A AS val > 0, B AS val <= 0);
+ERROR:  unsupported quantifier "||"
+LINE 1: ...EEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A||B) DEFIN...
+                                                             ^
+HINT:  Valid quantifiers are: *, +, ?, *?, +?, ??, {n}, {n,}, {,m}, {n,m} and their reluctant versions.
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B|) DEFINE A AS val > 0, B AS val <= 0);
+ERROR:  syntax error at or near ")"
+LINE 1: ...CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B|) DEFINE A...
+                                                             ^
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (|A) DEFINE A AS val > 0);
+ERROR:  syntax error at or near "|"
+LINE 1: ...WEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (|A) DEFINE...
+                                                             ^
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A*|)) DEFINE A AS val > 0);
+ERROR:  alternation operator "|" requires a pattern on both sides
+LINE 1: ...EEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A*|)) DEFI...
+                                                             ^
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A* *|B) DEFINE A AS val > 0, B AS val <= 0);
+ERROR:  invalid token after "*" quantifier
+LINE 1: ...N CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A* *|B) DEFIN...
+                                                             ^
+HINT:  Did you mean "*?" for reluctant quantifier?
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A* *?|B) DEFINE A AS val > 0, B AS val <= 0);
+ERROR:  invalid token after "*" quantifier
+LINE 1: ...N CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A* *?|B) DEFI...
+                                                             ^
+HINT:  Did you mean "*?" for reluctant quantifier?
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A? *?|B) DEFINE A AS val > 0, B AS val <= 0);
+ERROR:  invalid quantifier combination
+LINE 1: ...EEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A? *?|B) DE...
+                                                             ^
+HINT:  Did you mean "??" for reluctant quantifier?
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2}*?|B) DEFINE A AS val > 0, B AS val <= 0);
+ERROR:  invalid token after range quantifier
+LINE 1: ... CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2}*?|B) DEFI...
+                                                             ^
+HINT:  Only "?" is allowed after {n} to make it reluctant.
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2} *?|B) DEFINE A AS val > 0, B AS val <= 0);
+ERROR:  invalid token after range quantifier
+LINE 1: ...CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2} *?|B) DEFI...
+                                                             ^
+HINT:  Only "?" is allowed after {n} to make it reluctant.
+-- Doubled op-char quantifiers lex as one Op token and are unsupported, whether
+-- glued to '|' ("**|", "*+|", "???|") or on their own ("**").
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A**|B) DEFINE A AS val > 0, B AS val <= 0);
+ERROR:  unsupported quantifier "**|"
+LINE 1: ...EEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A**|B) DEFI...
+                                                             ^
+HINT:  Valid quantifiers are: *, +, ?, *?, +?, ??, {n}, {n,}, {,m}, {n,m} and their reluctant versions.
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*+|B) DEFINE A AS val > 0, B AS val <= 0);
+ERROR:  unsupported quantifier "*+|"
+LINE 1: ...EEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*+|B) DEFI...
+                                                             ^
+HINT:  Valid quantifiers are: *, +, ?, *?, +?, ??, {n}, {n,}, {,m}, {n,m} and their reluctant versions.
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A???|B) DEFINE A AS val > 0, B AS val <= 0);
+ERROR:  unsupported quantifier "???|"
+LINE 1: ...EEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A???|B) DEF...
+                                                             ^
+HINT:  Valid quantifiers are: *, +, ?, *?, +?, ??, {n}, {n,}, {,m}, {n,m} and their reluctant versions.
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A**B) DEFINE A AS val > 0);
+ERROR:  unsupported quantifier "**"
+LINE 1: ...EEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A**B) DEFIN...
+                                                             ^
+HINT:  Valid quantifiers are: *, +, ?, *?, +?, ??, {n}, {n,}, {,m}, {n,m} and their reluctant versions.
+DROP TABLE rpr_glue;
 -- ============================================================
 -- Error Cases Tests
 -- ============================================================
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index 7dfb72f6bfd..cc79843aeb7 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -13,6 +13,7 @@
 --   Navigation Functions Tests
 --   SKIP TO / INITIAL Tests
 --   Serialization/Deserialization Tests (objects kept for pg_upgrade/pg_dump)
+--   Glued Quantifier / Alternation Tests
 --   Error Cases Tests
 --   Window Deduplication Tests
 --
@@ -2074,6 +2075,117 @@ WINDOW w AS (ORDER BY id
              DEFINE A AS val > 0);
 SELECT pg_get_viewdef('rpr_quant_n_plus_v'::regclass);
 
+-- ============================================================
+-- Glued Quantifier / Alternation Tests
+-- ============================================================
+CREATE TABLE rpr_glue (id INT, val INT);
+INSERT INTO rpr_glue VALUES (1, 5), (2, 8), (3, 9), (4, -1), (5, 6), (6, -2);
+-- Quantifier glued to the alternation operator '|' without a space (0059).
+-- The lexer glues the trailing '|' into one Op token; the grammar reattaches it
+-- as the lowest-precedence alternation once the surrounding sequence is built.
+-- Deparse is canonical, so the glued, spaced, and mixed-spacing forms all
+-- reduce to the same PATTERN -- one deparse per shape proves the parse tree.
+
+-- Op-char quantifiers (*, +, ?, *?, +?, ??) glued to '|'.
+CREATE VIEW rpr_dp_op AS SELECT
+    count(*) OVER w1 AS w1, count(*) OVER w2 AS w2, count(*) OVER w3 AS w3,
+    count(*) OVER w4 AS w4, count(*) OVER w5 AS w5, count(*) OVER w6 AS w6
+FROM rpr_glue
+WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B) DEFINE A AS val > 0, B AS val <= 0),
+       w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+|B) DEFINE A AS val > 0, B AS val <= 0),
+       w3 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w4 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w5 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w6 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A??|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_op'), E'\n')) AS line WHERE line ~ 'PATTERN';
+DROP VIEW rpr_dp_op;
+-- Spaced reference: the fully-spaced canonical forms.  Identical deparse to the
+-- glued rpr_dp_op w1/w4 above completes the glued = spaced = mixed equivalence.
+CREATE VIEW rpr_dp_spc AS SELECT count(*) OVER w1 AS w1, count(*) OVER w2 AS w2
+FROM rpr_glue
+WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A* | B) DEFINE A AS val > 0, B AS val <= 0),
+       w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*? | B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_spc'), E'\n')) AS line WHERE line ~ 'PATTERN';
+DROP VIEW rpr_dp_spc;
+-- Range quantifiers glued to '|': non-reluctant {n}| (} + char '|') and
+-- reluctant {n}?| (} + Op "?|").
+CREATE VIEW rpr_dp_rng AS SELECT
+    count(*) OVER w1 AS w1, count(*) OVER w2 AS w2, count(*) OVER w3 AS w3, count(*) OVER w4 AS w4,
+    count(*) OVER w5 AS w5, count(*) OVER w6 AS w6, count(*) OVER w7 AS w7, count(*) OVER w8 AS w8
+FROM rpr_glue
+WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2}|B) DEFINE A AS val > 0, B AS val <= 0),
+       w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2,}|B) DEFINE A AS val > 0, B AS val <= 0),
+       w3 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{,3}|B) DEFINE A AS val > 0, B AS val <= 0),
+       w4 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2,3}|B) DEFINE A AS val > 0, B AS val <= 0),
+       w5 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2}?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w6 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2,}?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w7 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{,3}?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w8 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2,3}?|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_rng'), E'\n')) AS line WHERE line ~ 'PATTERN';
+DROP VIEW rpr_dp_rng;
+-- Mixed spacing: a space inside the quantifier with '|' still glued.
+-- "A* ?|B" = '*' + Op"?|" = reluctant "A*?" plus alternation.
+CREATE VIEW rpr_dp_mix AS SELECT count(*) OVER w1 AS w1, count(*) OVER w2 AS w2, count(*) OVER w3 AS w3
+FROM rpr_glue
+WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A* ?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+ ?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w3 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A? ?|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_mix'), E'\n')) AS line WHERE line ~ 'PATTERN';
+DROP VIEW rpr_dp_mix;
+-- Structure: precedence (| is lowest, so its right operand is the whole
+-- following sequence), chaining, concatenation, and grouping.
+CREATE VIEW rpr_dp_struct AS SELECT
+    count(*) OVER w1 AS w1, count(*) OVER w2 AS w2, count(*) OVER w3 AS w3,
+    count(*) OVER w4 AS w4, count(*) OVER w5 AS w5, count(*) OVER w6 AS w6
+FROM rpr_glue
+WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B C) DEFINE A AS val > 0, B AS val <= 0, C AS val < 100),
+       w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B*|C) DEFINE A AS val > 0, B AS val <= 0, C AS val < 100),
+       w3 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A B*|C D) DEFINE A AS val > 0, B AS val <= 0, C AS val < 100, D AS val > 5),
+       w4 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A*|B)) DEFINE A AS val > 0, B AS val <= 0),
+       w5 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|(B|C)) DEFINE A AS val > 0, B AS val <= 0, C AS val < 100),
+       w6 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A*|B)+) DEFINE A AS val > 0, B AS val <= 0);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_struct'), E'\n')) AS line WHERE line ~ 'PATTERN';
+DROP VIEW rpr_dp_struct;
+-- Execution semantics (deparse cannot show reluctant shortest-match).  The
+-- rpr_glue rows -- an A-run followed by B rows -- make the '|B' alternative
+-- reachable: with "*" the greedy form matches the whole run while the
+-- reluctant form matches empty; with "+" the greedy form matches the run and
+-- the reluctant form matches one row, and on a B row (where "A+" fails) the B
+-- alternative fires.
+SELECT id, val,
+       count(*) OVER gs AS gstar, count(*) OVER rs AS rstar,
+       count(*) OVER gp AS gplus, count(*) OVER rp AS rplus
+FROM rpr_glue
+WINDOW gs AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B) DEFINE A AS val > 0, B AS val <= 0),
+       rs AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*?|B) DEFINE A AS val > 0, B AS val <= 0),
+       gp AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+|B) DEFINE A AS val > 0, B AS val <= 0),
+       rp AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+?|B) DEFINE A AS val > 0, B AS val <= 0)
+ORDER BY id;
+-- Patterns that must stay rejected.  "&" is an invalid op; a '|' with an empty
+-- side (leading, trailing, doubled, or alone in a group) has no operand; "||"
+-- and "*||" are doubled pipes; "A* *|B"/"A* *?|B"/"A{2}*?|B" are doubled
+-- quantifiers.
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A&B) DEFINE A AS val > 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|) DEFINE A AS val > 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*| |B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*||B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A||B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B|) DEFINE A AS val > 0, B AS val <= 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (|A) DEFINE A AS val > 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A*|)) DEFINE A AS val > 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A* *|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A* *?|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A? *?|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2}*?|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2} *?|B) DEFINE A AS val > 0, B AS val <= 0);
+-- Doubled op-char quantifiers lex as one Op token and are unsupported, whether
+-- glued to '|' ("**|", "*+|", "???|") or on their own ("**").
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A**|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*+|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A???|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A**B) DEFINE A AS val > 0);
+DROP TABLE rpr_glue;
+
 -- ============================================================
 -- Error Cases Tests
 -- ============================================================
-- 
2.50.1 (Apple Git-155)