nocfbot-0001-Fix-mergeGroupPrefixSuffix-max-increment.txt

text/plain

Filename: nocfbot-0001-Fix-mergeGroupPrefixSuffix-max-increment.txt
Type: text/plain
Part: 0
Message: Re: Row pattern recognition
From a9c7017524a14820f07955366cca5e6a2a154319 Mon Sep 17 00:00:00 2001
From: Henson Choi <assam258@gmail.com>
Date: Thu, 19 Mar 2026 11:37:34 +0900
Subject: [PATCH] Fix mergeGroupPrefixSuffix() to also increment max when
 absorbing prefix/suffix

When mergeGroupPrefixSuffix() found a prefix or suffix that matched a GROUP
node's children, it incremented GROUP's min quantifier but left max unchanged.
This produced invalid quantifier state (min > max) when the GROUP had a finite
max bound.

Fix by also incrementing max when it is finite. Guard both the prefix and
suffix cases with an overflow check (min < RPR_QUANTITY_INF - 1) to prevent
wraparound into the sentinel value.

Add Assert() calls in fillRPRPatternVar(), fillRPRPatternGroup(), and
finalizeRPRPattern() to catch invalid min/max combinations at pattern
compilation time.
---
 src/backend/optimizer/plan/rpr.c  | 33 ++++++++++++++++++++++++-----
 src/test/regress/expected/rpr.out | 35 +++++++++++++++++++++++++++++++
 src/test/regress/sql/rpr.sql      | 23 ++++++++++++++++++++
 3 files changed, 86 insertions(+), 5 deletions(-)

diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index b958280e94c..c50cbdc18f1 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -541,13 +541,17 @@ mergeGroupPrefixSuffix(List *children)
 
 				/* Compare with GROUP's (possibly unwrapped) children */
 				if (rprPatternChildrenEqual(prefixElements, groupContent) &&
-					child->min < RPR_QUANTITY_INF)
+					child->min < RPR_QUANTITY_INF - 1 &&
+					(child->max == RPR_QUANTITY_INF ||
+					 child->max < RPR_QUANTITY_INF - 1))
 				{
 					/*
-					 * Match! Merge by incrementing GROUP's min. Remove the
-					 * prefix elements from output.
+					 * Match! Merge by incrementing GROUP's quantifier. Remove
+					 * the prefix elements from output.
 					 */
 					child->min += 1;
+					if (child->max != RPR_QUANTITY_INF)
+						child->max += 1;
 
 					/* Rebuild result without matched prefix */
 					trimmed = NIL;
@@ -595,12 +599,17 @@ mergeGroupPrefixSuffix(List *children)
 				/* Compare with GROUP's children */
 				if (list_length(suffixElements) == groupChildCount &&
 					rprPatternChildrenEqual(suffixElements, groupContent) &&
-					child->min < RPR_QUANTITY_INF)
+					child->min < RPR_QUANTITY_INF - 1 &&
+					(child->max == RPR_QUANTITY_INF ||
+					 child->max < RPR_QUANTITY_INF - 1))
 				{
 					/*
-					 * Match! Absorb suffix by incrementing min and skipping.
+					 * Match! Absorb suffix by incrementing quantifier and
+					 * skipping.
 					 */
 					child->min += 1;
+					if (child->max != RPR_QUANTITY_INF)
+						child->max += 1;
 					skipUntil = suffixStart + groupChildCount;
 
 					/*
@@ -1152,6 +1161,9 @@ fillRPRPatternVar(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth dept
 	elem->depth = depth;
 	elem->min = node->min;
 	elem->max = (node->max == INT_MAX) ? RPR_QUANTITY_INF : node->max;
+	Assert(elem->min >= 0 && elem->min < RPR_QUANTITY_INF &&
+		   elem->max >= 1 &&
+		   (elem->max == RPR_QUANTITY_INF || elem->min <= elem->max));
 	elem->next = RPR_ELEMIDX_INVALID;
 	elem->jump = RPR_ELEMIDX_INVALID;
 	if (node->reluctant)
@@ -1191,6 +1203,9 @@ fillRPRPatternGroup(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth de
 		elem->depth = depth;
 		elem->min = node->min;
 		elem->max = (node->max == INT_MAX) ? RPR_QUANTITY_INF : node->max;
+		Assert(elem->min >= 0 && elem->min < RPR_QUANTITY_INF &&
+			   elem->max >= 1 &&
+			   (elem->max == RPR_QUANTITY_INF || elem->min <= elem->max));
 		elem->next = RPR_ELEMIDX_INVALID;	/* set by finalize */
 		elem->jump = RPR_ELEMIDX_INVALID;	/* set after END */
 		if (node->reluctant)
@@ -1216,6 +1231,9 @@ fillRPRPatternGroup(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth de
 		endElem->depth = depth;
 		endElem->min = node->min;
 		endElem->max = (node->max == INT_MAX) ? RPR_QUANTITY_INF : node->max;
+		Assert(endElem->min >= 0 && endElem->min < RPR_QUANTITY_INF &&
+			   endElem->max >= 1 &&
+			   (endElem->max == RPR_QUANTITY_INF || endElem->min <= endElem->max));
 		endElem->next = RPR_ELEMIDX_INVALID;
 		endElem->jump = groupStartIdx;	/* loop to first child */
 		if (node->reluctant)
@@ -1398,6 +1416,11 @@ finalizeRPRPattern(RPRPattern *result)
 
 		if (elem->next == RPR_ELEMIDX_INVALID)
 			elem->next = (i < finIdx - 1) ? i + 1 : finIdx;
+
+		/* Verify quantifier range is valid */
+		Assert(elem->min >= 0 && elem->min < RPR_QUANTITY_INF &&
+			   elem->max >= 1 &&
+			   (elem->max == RPR_QUANTITY_INF || elem->min <= elem->max));
 	}
 
 	/* Add FIN marker at the end */
diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
index ecbe835cbb4..e72171050c7 100644
--- a/src/test/regress/expected/rpr.out
+++ b/src/test/regress/expected/rpr.out
@@ -588,6 +588,41 @@ SELECT company, tdate, price, count(*) OVER w
  company2 | 07-10-2023 |  1300 |     0
 (20 rows)
 
+-- test prefix/suffix merge optimization with bounded quantifier
+-- Pattern A B (A B){1,2} A B should be optimized to (A B){3,4}
+CREATE TEMP TABLE rpr_t (id int, val text);
+INSERT INTO rpr_t VALUES
+  (1,'A'),(2,'B'),
+  (3,'A'),(4,'B'),
+  (5,'A'),(6,'B'),
+  (7,'A'),(8,'B'),
+  (9,'X');
+SELECT id, val, count(*) OVER w AS match_count
+FROM rpr_t
+WINDOW w AS (
+  ORDER BY id
+  ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+  AFTER MATCH SKIP TO NEXT ROW
+  INITIAL
+  PATTERN (A B (A B){1,2} A B)
+  DEFINE
+    A AS val = 'A',
+    B AS val = 'B'
+);
+ id | val | match_count 
+----+-----+-------------
+  1 | A   |           8
+  2 | B   |           0
+  3 | A   |           6
+  4 | B   |           0
+  5 | A   |           0
+  6 | B   |           0
+  7 | A   |           0
+  8 | B   |           0
+  9 | X   |           0
+(9 rows)
+
+DROP TABLE rpr_t;
 -- last_value() should remain consistent
 SELECT company, tdate, price, last_value(price) OVER w
  FROM stock
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index b308f8d7cb4..95794d409e1 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -249,6 +249,29 @@ SELECT company, tdate, price, count(*) OVER w
   A AS price > 100
 );
 
+-- test prefix/suffix merge optimization with bounded quantifier
+-- Pattern A B (A B){1,2} A B should be optimized to (A B){3,4}
+CREATE TEMP TABLE rpr_t (id int, val text);
+INSERT INTO rpr_t VALUES
+  (1,'A'),(2,'B'),
+  (3,'A'),(4,'B'),
+  (5,'A'),(6,'B'),
+  (7,'A'),(8,'B'),
+  (9,'X');
+SELECT id, val, count(*) OVER w AS match_count
+FROM rpr_t
+WINDOW w AS (
+  ORDER BY id
+  ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+  AFTER MATCH SKIP TO NEXT ROW
+  INITIAL
+  PATTERN (A B (A B){1,2} A B)
+  DEFINE
+    A AS val = 'A',
+    B AS val = 'B'
+);
+DROP TABLE rpr_t;
+
 -- last_value() should remain consistent
 SELECT company, tdate, price, last_value(price) OVER w
  FROM stock
-- 
2.50.1 (Apple Git-155)