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)