nocfbot-0005-cycle-detection-bitmap.patch
application/octet-stream
Filename: nocfbot-0005-cycle-detection-bitmap.patch
Type: application/octet-stream
Part: 2
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 0005
Subject: Detect zero-consumption NFA cycles using elemIdx visited bitmap
| File | + | − |
|---|---|---|
| src/backend/executor/nodeWindowAgg.c | 26 | 19 |
| src/include/nodes/execnodes.h | 4 | 0 |
| src/test/regress/expected/rpr_nfa.out | 4 | 4 |
| src/test/regress/sql/rpr_nfa.sql | 4 | 4 |
From 39fc31eca0cee7fe874aa4e0abfc7ebaaa69bc1e Mon Sep 17 00:00:00 2001
From: Henson Choi <assam258@gmail.com>
Date: Mon, 23 Feb 2026 13:02:06 +0900
Subject: [PATCH 5/6] Detect zero-consumption NFA cycles using elemIdx visited
bitmap
---
src/backend/executor/nodeWindowAgg.c | 45 ++++++++++++++++-----------
src/include/nodes/execnodes.h | 4 +++
src/test/regress/expected/rpr_nfa.out | 8 ++---
src/test/regress/sql/rpr_nfa.sql | 8 ++---
4 files changed, 38 insertions(+), 27 deletions(-)
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 371439aad91..98546fb17f7 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -343,6 +343,10 @@ static void nfa_advance(WindowAggState *winstate, RPRNFAContext *ctx,
/* calculate shift bits */
#define NN_SHIFT(pos) ((pos) % NN_ITEM_PER_VAR) * NN_BITS_PER_MEMBER
+/* Bitmap macros for NFA cycle detection (cf. bitmapset.c, tidbitmap.c) */
+#define WORDNUM(x) ((x) / BITS_PER_BITMAPWORD)
+#define BITNUM(x) ((x) % BITS_PER_BITMAPWORD)
+
/*
* initialize_windowaggregate
* parallel to initialize_aggregates in nodeAgg.c
@@ -3016,11 +3020,15 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
/* Set up row pattern recognition PATTERN clause (compiled NFA) */
winstate->rpPattern = node->rpPattern;
- /* Calculate NFA state size for allocation */
+ /* Calculate NFA state size and allocate cycle detection bitmap */
if (node->rpPattern != NULL)
{
winstate->nfaStateSize = offsetof(RPRNFAState, counts) +
sizeof(int32) * node->rpPattern->maxDepth;
+ winstate->nfaVisitedNWords =
+ (node->rpPattern->numElements - 1) / BITS_PER_BITMAPWORD + 1;
+ winstate->nfaVisitedElems = palloc0(sizeof(bitmapword) *
+ winstate->nfaVisitedNWords);
}
/* Set up row pattern recognition DEFINE clause */
@@ -6214,25 +6222,9 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
nfa_route_to_elem(winstate, ctx, state, jumpElem, currentPos, initialAdvance);
}
- else if ((elem->max != RPR_QUANTITY_INF && count >= elem->max) ||
- (count == 0 && elem->min == 0))
+ else if (elem->max != RPR_QUANTITY_INF && count >= elem->max)
{
- /*----------
- * Must exit: either reached max iterations, or group matched empty.
- *
- * FIXME: The (count == 0 && min == 0) condition is insufficient for
- * cycle prevention. Cycles can occur at any count value when loop back
- * happens without consuming rows. For example:
- * Pattern: (A*)*
- * After matching 3 A's (count=3), loop back at a B row
- * Inner A* matches 0 times (skip path) → same (elemIdx, count=3)
- * Infinite cycle at count=3, not count=0
- *
- * Currently, cycles are silently prevented by nfa_add_state_unique
- * detecting duplicate states, but this is implicit and not guaranteed
- * for all code paths. Explicit cycle detection is needed.
- *----------
- */
+ /* Must exit: reached max iterations. */
RPRPatternElement *nextElem;
state->counts[depth] = 0;
@@ -6452,6 +6444,17 @@ nfa_advance_state(WindowAggState *winstate, RPRNFAContext *ctx,
RPRPatternElement *elem;
Assert(state->elemIdx >= 0 && state->elemIdx < pattern->numElements);
+
+ /* Cycle detection: if this elemIdx was already visited in this DFS, bail */
+ if (winstate->nfaVisitedElems[WORDNUM(state->elemIdx)] &
+ ((bitmapword) 1 << BITNUM(state->elemIdx)))
+ {
+ nfa_state_free(winstate, state);
+ return;
+ }
+ winstate->nfaVisitedElems[WORDNUM(state->elemIdx)] |=
+ ((bitmapword) 1 << BITNUM(state->elemIdx));
+
elem = &pattern->elements[state->elemIdx];
switch (elem->varId)
@@ -6506,6 +6509,10 @@ nfa_advance(WindowAggState *winstate, RPRNFAContext *ctx, int64 currentPos,
{
RPRNFAState *savedMatchedState = ctx->matchedState;
+ /* Clear visited bitmap before each state's DFS expansion */
+ memset(winstate->nfaVisitedElems, 0,
+ sizeof(bitmapword) * winstate->nfaVisitedNWords);
+
state = states;
states = states->next;
state->next = NULL;
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 9a1561fce7e..3681d905bde 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -2651,6 +2651,10 @@ typedef struct WindowAggState
Size nfaStateSize; /* pre-calculated RPRNFAState size */
bool *nfaVarMatched; /* per-row cache: varMatched[varId] for varId
* < numDefines */
+ bitmapword *nfaVisitedElems; /* elemIdx visited bitmap for cycle
+ * detection */
+ int nfaVisitedNWords; /* number of bitmapwords in
+ * nfaVisitedElems */
int64 nfaLastProcessedRow; /* last row processed by NFA (-1 =
* none) */
diff --git a/src/test/regress/expected/rpr_nfa.out b/src/test/regress/expected/rpr_nfa.out
index a2a257329b7..8ac375cfe86 100644
--- a/src/test/regress/expected/rpr_nfa.out
+++ b/src/test/regress/expected/rpr_nfa.out
@@ -30,7 +30,7 @@
-- Special Partition Cases
-- DEFINE Special Cases
-- Absorption Dynamic Flags
--- FIXME Issues (Known Limitations)
+-- Zero-Consumption Cycle Detection
--
-- Responsibility:
-- - NFA runtime execution paths
@@ -2945,9 +2945,9 @@ WINDOW w AS (
(5 rows)
-- ============================================================
--- FIXME Issues - Known Limitations
+-- Zero-Consumption Cycle Detection
-- ============================================================
--- FIXME 2 - Cycle prevention at count > 0
+-- Cycle prevention at count > 0: (A*)* inner skip cycles at count=3
WITH test_cycle_nonzero AS (
SELECT * FROM (VALUES
(1, ARRAY['A']),
@@ -2976,7 +2976,7 @@ WINDOW w AS (
4 | {B} | |
(4 rows)
--- FIXME 2 - Cycle with mixed nullables
+-- Cycle with mixed nullables: (A* B*)* multiple nullable paths
WITH test_cycle_mixed AS (
SELECT * FROM (VALUES
(1, ARRAY['A']),
diff --git a/src/test/regress/sql/rpr_nfa.sql b/src/test/regress/sql/rpr_nfa.sql
index 0e853ca753b..be78ab760a9 100644
--- a/src/test/regress/sql/rpr_nfa.sql
+++ b/src/test/regress/sql/rpr_nfa.sql
@@ -30,7 +30,7 @@
-- Special Partition Cases
-- DEFINE Special Cases
-- Absorption Dynamic Flags
--- FIXME Issues (Known Limitations)
+-- Zero-Consumption Cycle Detection
--
-- Responsibility:
-- - NFA runtime execution paths
@@ -2190,10 +2190,10 @@ WINDOW w AS (
);
-- ============================================================
--- FIXME Issues - Known Limitations
+-- Zero-Consumption Cycle Detection
-- ============================================================
--- FIXME 2 - Cycle prevention at count > 0
+-- Cycle prevention at count > 0: (A*)* inner skip cycles at count=3
WITH test_cycle_nonzero AS (
SELECT * FROM (VALUES
(1, ARRAY['A']),
@@ -2215,7 +2215,7 @@ WINDOW w AS (
A AS 'A' = ANY(flags)
);
--- FIXME 2 - Cycle with mixed nullables
+-- Cycle with mixed nullables: (A* B*)* multiple nullable paths
WITH test_cycle_mixed AS (
SELECT * FROM (VALUES
(1, ARRAY['A']),
--
2.50.1 (Apple Git-155)