From a9c7017524a14820f07955366cca5e6a2a154319 Mon Sep 17 00:00:00 2001 From: Henson Choi 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)