Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wS79Z-002xlQ-1q for pgsql-hackers@arkaria.postgresql.org; Wed, 27 May 2026 05:50:05 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1wS79X-007BNl-1D for pgsql-hackers@arkaria.postgresql.org; Wed, 27 May 2026 05:50:04 +0000 Received: from makus.postgresql.org ([2001:4800:3e1:1::229]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wS79W-007BNd-27 for pgsql-hackers@lists.postgresql.org; Wed, 27 May 2026 05:50:04 +0000 Received: from mail-ed1-x530.google.com ([2a00:1450:4864:20::530]) by makus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (Exim 4.98.2) (envelope-from ) id 1wS79T-00000000yCd-3Wsu for pgsql-hackers@postgresql.org; Wed, 27 May 2026 05:50:02 +0000 Received: by mail-ed1-x530.google.com with SMTP id 4fb4d7f45d1cf-67b32c695efso21121667a12.1 for ; Tue, 26 May 2026 22:49:59 -0700 (PDT) ARC-Seal: i=1; a=rsa-sha256; t=1779860997; cv=none; d=google.com; s=arc-20240605; b=MbvI5MiCx8DHk2sGUMenxh0DmFjqK+7Mos5zGsj/+3YFrYHVxTcKWSScG1pXNi34t+ kk9apDkfbvf6VLppCbQdlYXuZe4UgJY+0ifPOvqZezmJ2cspCTm226moEmtiF5lbsihv BK/Mc9HkRacGjz6H+EpCc48Ew6Mi9mXqshHKlzi+AXItZp+HSsHMIiFXPv86KHFMNpyA Lbbt0zEMIKDzrN69GMVdRZ06TKz89JAmND7wxdWiiv98jAmlXAfTFz8pzEa/xeJkLD27 cg9J6hVJ4VMRwtQRUW+OhE+sD6CWQKX1DB3M6YFYFFG8piot0yTyUmO+y/1xqRI/qDtb WjIA== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20240605; h=cc:to:subject:message-id:date:from:reply-to:in-reply-to:references :mime-version:dkim-signature; bh=VM0xk+XRtLh2RQUhDgpwb5PJ37nB0FGt2kz5t79O0Lg=; fh=IuQwkAq6WtUcqHy97wufQXsOjxeuHcBBMHzwgMfMIR4=; b=ftR/Na47IESYW/UzvrL+5fmAuIWR8Wy3fWpmy6pfTjXyv1yXDUcC8PB9RHf93lBhyG Wx64av4mp+WlNwjvQ+gKfFrRYAXu5XZZg48BTIbe7UmwZqCjaz8xL0ZwvQSGsM9SIiZu WvapcQG3XrPTicNSBrpwDwAV2BbKfOjz4RQxlZD5xjDEyMrSUhQgzIGSVh1S2pcqrh7z YDaW8hWi10BIzoMQu7oDECRKjzrm/Ua5UdLaIzJqedzXXbcC7v+eJtIaQAQQ9ldaQP/0 1rUhHTR9Jn/r+s2Na9R87MGlH4MRrL2KFkKOvBhrUj9DnTRNxCxpGx0Sh/Q5K8Eelr5n 8+Ig==; darn=postgresql.org ARC-Authentication-Results: i=1; mx.google.com; arc=none DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1779860997; x=1780465797; darn=postgresql.org; h=cc:to:subject:message-id:date:from:reply-to:in-reply-to:references :mime-version:from:to:cc:subject:date:message-id:reply-to; bh=VM0xk+XRtLh2RQUhDgpwb5PJ37nB0FGt2kz5t79O0Lg=; b=aAuk17bSHUg+dT75pyyKUo6ez7K3FLWP9EHLo1MT0FMqUFNkDf9szpstaANR1hgbv3 AdjFYVCtMHxUIh7XgpiD0WOIYe+qO68z4AMMyf8c+s0pyu40IsblxLUHbS+D6jp+ikfQ K7Sc4IeOFlvM8ISXpOygdohyaTlfT7fNL/Fwz6KvOMUusyXvPhZHvUISvc20o+txDiXo L87XNQJcStXiwHKGZHpYWi4B4F2rUnu0b1cuJlaJuIhD/jANnNOyUhv8ZrjTTVTHZrvS DA1UCFUW2izesy2EicZRkkxUetfERH/AvnOMbeBrpKeK6I5twcfUUBRGPtFHV4ZNufui dDsA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1779860997; x=1780465797; h=cc:to:subject:message-id:date:from:reply-to:in-reply-to:references :mime-version:x-gm-gg:x-gm-message-state:from:to:cc:subject:date :message-id:reply-to; bh=VM0xk+XRtLh2RQUhDgpwb5PJ37nB0FGt2kz5t79O0Lg=; b=slokkEjY5VGMxLc6S4VNjNQOzDRa7THNCHIsfSqDByg0URhLXmodKOPFHggYH2985F ebvqiims92GSwI5HUAhSmlx3OPzvT/Lh5yoF6gUaE3bMFLVGCd0HpU+YXQt8SY9s0QkQ h4SggqHoAInkTRa9MbmGUR+WXfFFhkLJ/MBAqyP5hIvXbMjxIMd4kguJnGxjJkky0su1 rE09zUZ/ijoN4PpvhElGwGvDpX4E7+0XMdj9pnkR9pzx9ar1vRXi8VANkRh5o4jEz1nQ kZbv14W4tZOV8QxOVOS1RK/89UwWdACYwROto7OXzl38NJNTVprTgcjH58VP9/2DDXGd eLfg== X-Forwarded-Encrypted: i=1; AFNElJ83MW6etDR0xzqJXaSXXn9bWbiWHPukPLpYS7jtXHhhRzYlFkul4+vJRaZBbFFdWxopzeRSyS49N8awuAFO@postgresql.org X-Gm-Message-State: AOJu0YwERJnGE4yT3zFM4m6FguRTM5ku1oYv1dTAS931XK7WB1h8Jp3d p2aG2y+Kt0AyE6cQclJPmp8S7x7V8snALT0YSEqyfbY7Ct3eiYiCAv4ATNek/eu8SYm3M2yTqW9 aXW3c8dgV8Zj/BKa9A+2UB/bXrAW0lvQ= X-Gm-Gg: Acq92OHbkZji0u8iuBsJzSyD+gUv7eGECnd+myMZZWaraqku/ZBOwDjqR1u3aDZzM9G PleG88rz+CRxuZVE3iYGap6Py5pm57xiAQWG9NA/kd4mTxV09ROpGqrwrF3TfVPs68ChrXaHHQ6 bpJjvZijf01hUSmRRN9Z3x3k7N0SMi/RQff8GZSZBag4ANQZCUkgg9+b+CfRNRb7X0mVxs87M1x o/11UXB5hRLQV1uQFziZas3T+SxTtE9HPiaaFbclm4RNjfZlT/e5ARZ2O6hCHFOJaHaVQ9Z/Jn9 dnLnjI3I0U4uJbLr2yAcbC5Y69zZrFIweFUTQVnXf12tNo7B7pk= X-Received: by 2002:a17:907:2849:b0:bda:24df:21c with SMTP id a640c23a62f3a-bdd4a3b7225mr813008966b.17.1779860996719; Tue, 26 May 2026 22:49:56 -0700 (PDT) MIME-Version: 1.0 References: <20260502.140304.670813149418899420.ishii@postgresql.org> <20260505.090124.365339750969814137.ishii@postgresql.org> <20260517.190023.159085681032648582.ishii@postgresql.org> In-Reply-To: Reply-To: assam258@gmail.com From: Henson Choi Date: Wed, 27 May 2026 14:49:44 +0900 X-Gm-Features: AVHnY4KCXF4q6k681-Fuw7UP1G4BgOF_blwizCbSIYgMMDw_KiDW5yyBqLS7HQc Message-ID: Subject: Re: Row pattern recognition To: jian he , Tatsuo Ishii Cc: zsolt.parragi@percona.com, sjjang112233@gmail.com, vik@postgresfriends.org, er@xs4all.nl, jacob.champion@enterprisedb.com, david.g.johnston@gmail.com, peter@eisentraut.org, li.evan.chao@gmail.com, pgsql-hackers@postgresql.org Content-Type: multipart/alternative; boundary="000000000000758c4c0652c62e2a" List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk --000000000000758c4c0652c62e2a Content-Type: text/plain; charset="UTF-8" Hi jian, Tatsuo, Thanks for the thorough first read of execRPR.c and the NFA. The depth of understanding you reached on a fairly intricate body of code in a short window is impressive -- and several of the open questions in your review pointed straight at documentation and comment debt on our side that needed exposing. First-reviewer feedback of that kind is especially valuable, because the gaps it surfaces are exactly the ones the next reader would otherwise have to re-discover; closing them now puts everyone reading the code afterwards on firmer ground. Would be glad to keep working together on this area. > Disclaimer: I had some private off-list discussions with Henson Choi > regarding execRPR.c, and the NFA. > The following are more formal comments regarding execRPR.c and the NFA > algorithm, based on v47-0001 to v47-0009, I haven't applied the > incremental diff yet. > (Since the incremental diffs are scattered across several threads, a > unified v48 would be better). > (I'm still wrapping my head around the NFA in execRPR.c, so take the > comments below with a grain of salt.) Agreed on the unified v48 -- both the scattered incrementals and the responses to your review will be part of that series. The responses below will land alongside their corresponding change (README, comment, test, or commit) in that series rather than as a free-standing reply that defers the actual fixes. > PATTERN (A B C D) > We can short-circuit and exit early if any of the evaluations (A, B, > or C) fail in nfa_match. > This is necessary since the chance of a pattern element evaluation > returning false is not rare, I think. Agreed on the optimization intent. A slightly different shape worth considering: rather than eager short-circuit in nfa_match, defer DEFINE predicate evaluation to first use -- varMatched[i] computed on demand and cached per row, so pruned paths never pay for predicates they didn't reach. The code change is small; the part needing care is whether "not evaluating" is safe for every predicate (navigation state, slot setup, side effects). The current code or the standard may already enforce constraints that make lazy evaluation naturally safe, but I'd rather not act on that assumption without concrete grounding in what the code actually enforces or in test coverage that pins it down. Would you be interested in driving the discussion in this thread? You've been deep in execRPR.c recently, so the trade-offs sit closest to you, and I think Tatsuo will land a good conclusion once they're on the table. Happy to support the discussion in whatever way is useful as it converges. > For src/backend/executor/README.rpr: > We should explicitly explain 'absorbable' and 'absorption' somewhere in > README.rpr, as the text currently just assumes the reader knows what they mean. > Using some example illustrate "absorption" meaning, put it on > README.rpr would be great. > We can also mention that 'DFS' refers to Depth-First Search". Acknowledged, and the request surfaced an underlying problem in the README's terminology. "Absorption" is currently used for two distinct things: an AST-level rewrite in Phase 1 that pulls identical sequences around a group inside it, and the runtime context-equivalence collapse that drives the O(n^2) -> O(n) optimization. Sharing the word leaves a reader encountering "absorbable" early on without an anchor. Rather than disambiguate by qualifier ("prefix/suffix absorption" vs "context absorption"), I'd lean toward renaming the AST-level case so "absorption" stays reserved for the runtime concept. The README then only needs to explain absorption in one place, in detail, without the disambig preamble. For the rename, "prefix/suffix merging" feels like the natural fit -- the other AST-level optimizations in the same Phase 1 are already named "consecutive variable / group / ALT merging", so it slots in cleanly. "Prefix/suffix factoring" is another candidate if a more descriptive verb is preferred. Tatsuo, curious what you think of this direction and naming. Happy to take any name you prefer for the AST-level operation, or to keep the original "absorption" wording with stronger forward-references if you'd rather not rename. For the absorption explanation itself in README.rpr, the diagnosis I'd offer is that Chapter VIII already carries the necessary content -- the issue is narrative order. VIII-1 leads with the O(n^2) problem framing, so a reader meets the cost shape before meeting the intuition for why absorption is possible, and has to carry the problem until VIII-2's monotonicity argument finally lands. Beyond that, VIII-2 stays abstract; there is no row-by-row trace showing two states being judged equivalent. Two small additions seem to close most of the gap: (A) A 4-5 line intuition summary at the top of Chapter VIII, before VIII-1, naming what absorption is (collapsing contexts that have converged on identical future behavior) and the monotonicity principle that makes it safe. This gives the reader an anchor before the problem framing. (B) A short worked example at the end of VIII-2: a PATTERN (A+) trace over a few rows showing each new context being absorbed by Context_1 once its (elemIdx, depth-0 count) is dominated. Concrete state/count comparisons make the abstract solution land. Curious if this read of the gap matches what tripped you up, and whether (A) + (B) feel sufficient. Happy to draft both as part of the v48 README changes. For DFS, will expand it to "Depth-First Search (DFS)" at the first occurrence. > `````` > (4) Call nfa_advance(initialAdvance=true) > `````` > In V47, the variable `initialAdvance` does not exist. Leftover from an earlier patch version -- the boolean parameter was refactored away and the README notation wasn't updated. I'll bring it in line with the current signature. > In nfa_advance_var, after the first Assert, we can add: > Assert(elem->next < pattern->numElements); Agreed. Will add it right after Assert(canLoop || canExit) in nfa_advance_var, with a >= 0 lower bound tacked on while there (RPRElemIdx is signed int16, INVALID = -1): Assert(elem->next >= 0 && elem->next < pattern->numElements); > ExecRPRFinalizeAllContexts seems unnecessary; I commented it out, > rerun the regress tests > (TESTS='test_setup rpr_base rpr_nfa rpr_explain rpr_integration rpr' > meson test -C $BUILD3 --num-processes 20 --suite regress --verbose) > Only two SQL tests in rpr_explain.sql failed. Reproduced this. You're right on correctness and memory: data rows are identical with the call removed, and release_partition's MemoryContextReset reclaims memory anyway. Finalize isn't really about handling matches. By the time the partition ends, all genuine FIN reaches have already been recorded in-flight. Its job is to kill any VAR states still pursuing when rows run out, so cleanup sees a uniform ctx->states == NULL across every context. Three shapes survive there: - Pure pursuit (no matchedState, e.g., A+ B mid-pattern). - Empty-match candidate + pursuit (matchedState set with matchEndRow < matchStartRow -- e.g., greedy A* with no successful matches yet, while VAR is still chasing a longer non-empty one). - Real match + pursuit (matchedState set with matchEndRow >= matchStartRow -- e.g., greedy A* with some matches recorded and still looping for a longer one). The first two get reclassified as failures by cleanup; without Finalize they linger without contributing to stats. The third is stat-neutral -- cleanup skips it either way -- but goes through the same uniform path so partition-end classification stays centralized. The classification surfaces today only via rpr_explain stats, but becomes user-visible once we extend the R020 surface or move into R010 -- MEASURES and eventual R010 hooks count matches based on this classification. Worth locking in now, and an explicit partition-end stage is structurally cleaner than scattering the logic. Plan: keep the call and reframe it as the partition-end classification policy holder. Strategically, that gives the future partition-end hooks a single anchor to extend, instead of growing scattered end-of-partition paths. > SELECT first_value(id) OVER w AS match_start FROM stock_ticks > WINDOW w AS ( ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED > FOLLOWING AFTER MATCH SKIP PAST LAST ROW PATTERN ((A B) {2}) DEFINE A > AS price < 100, B AS price < 100); > > The query above invokes the following code. Since the PATTERN above is > not greedy, is the comment below incorrect? > `````` > else > { > /* Greedy: enter first, skip second */ > ... > } > `````` The comment is misleading; the code is correct. Two pieces: First, greedy vs reluctant: '?' plays two distinct roles in our grammar. As a quantifier on its own (A?, (A B)?) it means "optional" -- equivalent to {0,1}. As a suffix on another quantifier (A+?, (A B)*?, {n}?, etc.) it makes that quantifier reluctant. So {n} without a trailing '?' is greedy; the {n}? form is reluctant. PATTERN ((A B){2}) is in fact greedy. Second, why the branch is entered: nfa_advance_begin's else arm handles two cases at once: (a) Greedy with optional group: skipState != NULL, not reluctant. "Enter the group; also create the skip path." (b) Non-nullable group (min > 0, regardless of greedy/reluctant): skipState stays NULL, the outer guard "if (skipState != NULL && RPRElemIsReluctant(elem))" falls through, and the inner "if (skipState != NULL)" prevents the skip-path action from running. (A B){2} has min = max = 2, so it lands in (b) -- the action that actually runs is "enter the group", no skip path. The current label only describes (a), which is why it reads wrong for your test query. Plan is to rewrite the comment along the lines of "Greedy-or-non-nullable: route to the first child; for optional groups (skipState != NULL), additionally create the skip path." > nfa_advance_var > ``` > else if (canExit) > { > ... > } > ``` > The above ELSE IF overrides all RPRNFAState field values except > RPRNFAState->next. > Should we set RPRNFAState->next to NULL? > (If I add ``state->next = NULL;`` in the above ELSE IF branch, all the > regress tests still pass) Good catch on the asymmetry. Tracing it through, state->next is actually already NULL at every branch you flagged: nfa_advance resets it just before crossing into nfa_advance_state, and the intermediate branches don't disturb it. Your experiment passing with an added "state->next = NULL" is consistent with that -- the assignment is redundant rather than load-bearing. The contract that keeps state->next sane lives at two concentrated points (nfa_advance entry, nfa_add_state_unique linking), and the branches in between are pass-through. Sprinkling the same reset at every branch would be defensive noise rather than a real safety net, so I'd leave the branches alone. Happy to add a short comment near nfa_advance's reset marking it as the boundary contract, so the next reader doesn't trip on the same question. > For function nfa_advance_var, I don't understand the meaning of the > variable "count", after the first Assert I have added below: > ... > Rerunning the regress tests shows that count >= 3 occurs very infrequently. > ... > Can we add more complex queries (more count >= 3) to check if the > "count" variable is working correctly? The "count" semantics will read more cleanly once the absorption README work above lands (counts[d] = iteration count at nesting depth d). For coverage I'll add a nested reluctant quantifier to rpr_nfa (e.g., PATTERN ((A B){3,5}? C)) to drive count through the 3..5 band repeatedly. (rpr_nfa is the suite that already targets Quantifier Runtime Behavior and Absorption Optimization.) > In function nfa_add_state_unique: > /* Mark VAR in visited before duplicate check to prevent DFS loops */ > ... > I honestly don't understand the purpose of the code block above. But it doesn't > seem to influence the subsequent FOR LOOP; > ... > Could we add some comments explaining which external functions rely on > this code and why it belongs in nfa_add_state_unique? The code is correct, but the contract is split across two functions and currently only one side points to the other. The visited marking scheme is asymmetric on purpose: - Non-VAR elements (END/ALT/BEGIN/FIN) are marked on entry to nfa_advance_state because epsilon cycles must be prevented immediately. - VAR elements are marked later, in nfa_add_state_unique, only when added to the state list. That delay is intentional: it keeps legitimate quantifier loop-back to the same VAR across iterations possible. The paired cycle check sits in nfa_advance_state, and the asymmetric-marking rationale is documented there. What's missing is the back-reference from nfa_add_state_unique. I'll add a single line at the marking site pointing back to nfa_advance_state. > nfa_states_equal > compareDepth = elem->depth + 1; /* depth 0 needs 1 count, etc. */ > The comment above isn't helpful, IMHO, and I don't understand it. > We should focus on why compareDepth should be ```elem->depth + 1```. Agree the trailing comment is too terse. Two pieces are missing: (a) The +1 arithmetic: to compare counts up to depth N, we need slots counts[0..N], which is N+1 entries. (b) Why deeper slots are excluded: counts[d > elem->depth] are scratch state from deeper groups and get reset on re-entry, so they must not participate in equivalence judgment. Two states sharing elemIdx are equivalent iff all enclosing-or-current depth counts match. I'll replace the trailing comment with a small block covering both pieces. > function nfa_add_state_unique return value is not being used? > Do we need to do something with the return value, or is this expected? > (I don't have an opinion on it, I guess it would be better to raise this issue) Leftover from an earlier design -- the duplicate case is fully handled inside the function (the state is freed and nfaStatesMerged is incremented), so callers have nothing to branch on, and indeed none of them do. Will change the signature from bool to void and drop the return statements. > In nfa_advance_alt, during the main WHILE loop, I think altElem->depth > must be larger than elem->depth. > Therefore we can do > `````` > if (altElem->depth == elem->depth) > elog(ERROR, "nfa_advance_alt altElem->depth should not be > the same as elem->depth reached"); > if (altElem->depth < elem->depth) > break; > `````` I had to push back on this one. Tracing the depth bookkeeping: - For an ALT at depth D, branches sit at depth D+1, and each branch's first element has .jump pointing to the next branch's first (set in fillRPRPatternAlt). So the walk normally terminates when the last branch's .jump = INVALID -- the depth check doesn't fire at all. - But when the last branch is a quantified group, its first element is a BEGIN whose .jump = past-END (set by fillRPRPatternGroup and not overridden for the last branch). The walk then steps to a post-ALT element, and the depth check is what stops it from creating a stray state out there. That post-ALT element has depth <= D: * D-1 if the ALT is inside an enclosing group with a non-trivial quantifier, e.g., PATTERN ((A | (B C)+){2}) -- post-ALT lands on the outer END at depth 0, ALT at depth 1. (A {1,1} outer wrap gets removed by single-child unwrap, so it has to be a real quantifier.) * D if the ALT has a sibling at the same level, e.g., PATTERN (A | (B C)+) at top level -- post-ALT is FIN at depth 0, matching the ALT's depth 0. So "altElem->depth == elem->depth" is a legitimate end-of-walk signal for the quantified-group-last-branch case, not an invariant violation. Treating it as an error would misfire on patterns like A | (B C)+. The current "if (altElem->depth <= elem->depth) break;" in nfa_advance_alt is intentionally <= and not <, and the looser comparison is correct. Happy to add a brief comment there noting the trigger condition, if it would help future readers. Summary of decisions, in the order above: Short-circuit optimization Separate series -- invite you to drive Absorption README narrative Accept -- Chapter VIII summary + example AST-level "absorption" rename Pending Tatsuo's call -- prefix/suffix merging? DFS expansion Accept initialAdvance README mismatch Accept -- align with current signature Defensive Assert in advance_var Accept -- also add lower bound Finalize unnecessary? Keep -- partition-end policy holder Greedy comment label Accept -- rewrite to cover both cases state->next reset Decline -- boundary contract covers it count >= 3 test coverage Accept -- add to rpr_nfa visited marking purpose Accept -- add back-reference comment compareDepth comment Accept -- rewrite with intent Unused bool return Accept -- change to void ALT depth invariant Assert Decline -- end-of-walk signal, not invariant That's the full pass. The actual patches (nocfbot-0016 onward) will follow shortly as a separate submission, for another review round. Thanks, Henson --000000000000758c4c0652c62e2a Content-Type: text/html; charset="UTF-8" Content-Transfer-Encoding: quoted-printable
Hi jian, Tatsuo,

Thanks for the thorough first read= of execRPR.c and the NFA. The
depth of understanding you reached on a f= airly intricate body of
code in a short window is impressive -- and seve= ral of the open
questions in your review pointed straight at documentati= on and
comment debt on our side that needed exposing. First-reviewer
= feedback of that kind is especially valuable, because the gaps it
surfac= es are exactly the ones the next reader would otherwise have
to re-disco= ver; closing them now puts everyone reading the code
afterwards on firme= r ground. Would be glad to keep working together
on this area.

> Disclaimer: I had some private off-list discussions with Henson Choi=
> regarding execRPR.c, and the NFA.
> The following are more f= ormal comments regarding execRPR.c and the NFA
> algorithm, based on = v47-0001 to v47-0009, I haven't applied the
> incremental diff ye= t.
> (Since the incremental diffs are scattered across several thread= s, a
> unified v48 would be better).
> (I'm still wrapping = my head around the NFA in execRPR.c, so take the
> comments below wit= h a grain of salt.)

Agreed on the unified v48 -- both the scattered = incrementals and
the responses to your review will be part of that serie= s. The
responses below will land alongside their corresponding change(README, comment, test, or commit) in that series rather than as
a free= -standing reply that defers the actual fixes.


> PATTERN (A B = C D)
> We can short-circuit and exit early if any of the evaluations = (A, B,
> or C) fail in nfa_match.
> This is necessary since the= chance of a pattern element evaluation
> returning false is not rare= , I think.

Agreed on the optimization intent. A slightly different s= hape worth
considering: rather than eager short-circuit in nfa_match, de= fer
DEFINE predicate evaluation to first use -- varMatched[i] computedon demand and cached per row, so pruned paths never pay for
predicates= they didn't reach. The code change is small; the part
needing care = is whether "not evaluating" is safe for every predicate
(navig= ation state, slot setup, side effects). The current code or
the standard= may already enforce constraints that make lazy
evaluation naturally saf= e, but I'd rather not act on that assumption
without concrete ground= ing in what the code actually enforces or in
test coverage that pins it = down.

Would you be interested in driving the discussion in this thre= ad?
You've been deep in execRPR.c recently, so the trade-offs sitclosest to you, and I think Tatsuo will land a good conclusion once
the= y're on the table. Happy to support the discussion in whatever
way i= s useful as it converges.


> For src/backend/executor/README.r= pr:
> We should explicitly explain 'absorbable' and 'abso= rption' somewhere in
> README.rpr, as the text currently just ass= umes the reader knows what they mean.
> Using some example illustrate= "absorption" meaning, put it on
> README.rpr would be grea= t.
> We can also mention that 'DFS' refers to Depth-First Sea= rch".

Acknowledged, and the request surfaced an underlying prob= lem in the
README's terminology. "Absorption" is currently= used for two
distinct things: an AST-level rewrite in Phase 1 that pull= s
identical sequences around a group inside it, and the runtime
conte= xt-equivalence collapse that drives the O(n^2) -> O(n)
optimization. = Sharing the word leaves a reader encountering
"absorbable" ear= ly on without an anchor.

Rather than disambiguate by qualifier (&quo= t;prefix/suffix absorption"
vs "context absorption"), I&#= 39;d lean toward renaming the AST-level
case so "absorption" s= tays reserved for the runtime concept. The
README then only needs to exp= lain absorption in one place, in
detail, without the disambig preamble.<= br>
For the rename, "prefix/suffix merging" feels like the nat= ural fit
-- the other AST-level optimizations in the same Phase 1 are al= ready
named "consecutive variable / group / ALT merging", so i= t slots in
cleanly. "Prefix/suffix factoring" is another candi= date if a more
descriptive verb is preferred.

Tatsuo, curious wha= t you think of this direction and naming. Happy
to take any name you pre= fer for the AST-level operation, or to keep
the original "absorptio= n" wording with stronger forward-references
if you'd rather not= rename.

For the absorption explanation itself in README.rpr, the di= agnosis
I'd offer is that Chapter VIII already carries the necessary= content
-- the issue is narrative order. VIII-1 leads with the O(n^2) p= roblem
framing, so a reader meets the cost shape before meeting the
i= ntuition for why absorption is possible, and has to carry the
problem un= til VIII-2's monotonicity argument finally lands. Beyond
that, VIII-= 2 stays abstract; there is no row-by-row trace showing
two states being = judged equivalent.

Two small additions seem to close most of the gap= :

=C2=A0 (A) A 4-5 line intuition summary at the top of Chapter VIII= ,
=C2=A0 =C2=A0 =C2=A0 before VIII-1, naming what absorption is (collaps= ing contexts
=C2=A0 =C2=A0 =C2=A0 that have converged on identical futur= e behavior) and the
=C2=A0 =C2=A0 =C2=A0 monotonicity principle that mak= es it safe. This gives the
=C2=A0 =C2=A0 =C2=A0 reader an anchor before = the problem framing.

=C2=A0 (B) A short worked example at the end of= VIII-2: a PATTERN (A+)
=C2=A0 =C2=A0 =C2=A0 trace over a few rows showi= ng each new context being absorbed
=C2=A0 =C2=A0 =C2=A0 by Context_1 onc= e its (elemIdx, depth-0 count) is dominated.
=C2=A0 =C2=A0 =C2=A0 Concre= te state/count comparisons make the abstract solution
=C2=A0 =C2=A0 =C2= =A0 land.

Curious if this read of the gap matches what tripped you u= p, and
whether (A) + (B) feel sufficient. Happy to draft both as part of=
the v48 README changes.

For DFS, will expand it to "Depth-F= irst Search (DFS)" at the first
occurrence.


> ``````<= br>> =C2=A0 (4) Call nfa_advance(initialAdvance=3Dtrue)
> ``````> In V47, the variable `initialAdvance` does not exist.

Leftove= r from an earlier patch version -- the boolean parameter was
refactored = away and the README notation wasn't updated. I'll bring
it in li= ne with the current signature.


> In nfa_advance_var, after th= e first Assert, we can add:
> Assert(elem->next < =C2=A0pattern= ->numElements);

Agreed. Will add it right after Assert(canLoop ||= canExit) in
nfa_advance_var, with a >=3D 0 lower bound tacked on whi= le there
(RPRElemIdx is signed int16, INVALID =3D -1):

=C2=A0 Ass= ert(elem->next >=3D 0 && elem->next < pattern->numEl= ements);


> ExecRPRFinalizeAllContexts seems unnecessary; I co= mmented it out,
> rerun the regress tests
> (TESTS=3D'test_= setup rpr_base rpr_nfa rpr_explain rpr_integration rpr'
> meson t= est -C $BUILD3 --num-processes 20 --suite regress --verbose)
> Only t= wo SQL tests in rpr_explain.sql failed.

Reproduced this. You're = right on correctness and memory: data rows
are identical with the call r= emoved, and release_partition's
MemoryContextReset reclaims memory a= nyway.

Finalize isn't really about handling matches. By the time= the
partition ends, all genuine FIN reaches have already been recorded<= br>in-flight. Its job is to kill any VAR states still pursuing when
rows= run out, so cleanup sees a uniform ctx->states =3D=3D NULL across
ev= ery context. Three shapes survive there:

=C2=A0 - Pure pursuit (no m= atchedState, e.g., A+ B mid-pattern).
=C2=A0 - Empty-match candidate + p= ursuit (matchedState set with
=C2=A0 =C2=A0 matchEndRow < matchStartR= ow -- e.g., greedy A* with no
=C2=A0 =C2=A0 successful matches yet, whil= e VAR is still chasing a longer
=C2=A0 =C2=A0 non-empty one).
=C2=A0 = - Real match + pursuit (matchedState set with matchEndRow >=3D
=C2=A0= =C2=A0 matchStartRow -- e.g., greedy A* with some matches recorded
=C2= =A0 =C2=A0 and still looping for a longer one).

The first two get re= classified as failures by cleanup; without
Finalize they linger without = contributing to stats. The third is
stat-neutral -- cleanup skips it eit= her way -- but goes through
the same uniform path so partition-end class= ification stays
centralized.

The classification surfaces today on= ly via rpr_explain stats, but
becomes user-visible once we extend the R0= 20 surface or move into
R010 -- MEASURES and eventual R010 hooks count m= atches based on
this classification. Worth locking in now, and an explic= it
partition-end stage is structurally cleaner than scattering the
lo= gic.

Plan: keep the call and reframe it as the partition-end
clas= sification policy holder. Strategically, that gives the future
partition= -end hooks a single anchor to extend, instead of growing
scattered end-o= f-partition paths.


> SELECT first_value(id) OVER w AS match_s= tart FROM stock_ticks
> WINDOW w AS ( ORDER BY id ROWS BETWEEN CURREN= T ROW AND UNBOUNDED
> FOLLOWING AFTER MATCH SKIP PAST LAST ROW PATTER= N ((A B) {2}) DEFINE A
> AS price < 100, B AS price < 100);
= >
> The query above invokes the following code. Since the PATTERN = above is
> not greedy, is the comment below incorrect?
> ``````=
> =C2=A0 =C2=A0 else
> =C2=A0 =C2=A0 {
> =C2=A0 =C2=A0 = =C2=A0 =C2=A0 /* Greedy: enter first, skip second */
> =C2=A0 =C2=A0 = =C2=A0 =C2=A0 ...
> =C2=A0 =C2=A0 }
> ``````

The comment= is misleading; the code is correct. Two pieces:

First, greedy vs re= luctant: '?' plays two distinct roles in our
grammar. As a quant= ifier on its own (A?, (A B)?) it means "optional"
-- equivalen= t to {0,1}. As a suffix on another quantifier (A+?,
(A B)*?, {n}?, etc.)= it makes that quantifier reluctant. So {n}
without a trailing '?= 9; is greedy; the {n}? form is reluctant.
PATTERN ((A B){2}) is in fact = greedy.

Second, why the branch is entered: nfa_advance_begin's e= lse arm
handles two cases at once:

=C2=A0 (a) Greedy with optiona= l group: skipState !=3D NULL, not reluctant.
=C2=A0 =C2=A0 =C2=A0 "= Enter the group; also create the skip path."
=C2=A0 (b) Non-nullabl= e group (min > 0, regardless of greedy/reluctant):
=C2=A0 =C2=A0 =C2= =A0 skipState stays NULL, the outer guard
=C2=A0 =C2=A0 =C2=A0 "if = (skipState !=3D NULL && RPRElemIsReluctant(elem))" falls
= =C2=A0 =C2=A0 =C2=A0 through, and the inner "if (skipState !=3D NULL)&= quot; prevents the
=C2=A0 =C2=A0 =C2=A0 skip-path action from running.
(A B){2} has min =3D max =3D 2, so it lands in (b) -- the action that=
actually runs is "enter the group", no skip path. The current= label
only describes (a), which is why it reads wrong for your test que= ry.
Plan is to rewrite the comment along the lines of
"Greedy-or= -non-nullable: route to the first child; for optional
groups (skipState = !=3D NULL), additionally create the skip path."


> nfa_ad= vance_var
> ```
> =C2=A0 =C2=A0 else if (canExit)
> =C2= =A0 =C2=A0 {
> =C2=A0 =C2=A0 =C2=A0 =C2=A0 ...
> =C2=A0 =C2=A0 = }
> ```
> The above ELSE IF overrides all RPRNFAState field val= ues except
> RPRNFAState->next.
> Should we set RPRNFAState-= >next to NULL?
> (If I add ``state->next =3D NULL;`` in the abo= ve ELSE IF branch, all the
> regress tests still pass)

Good ca= tch on the asymmetry. Tracing it through, state->next is
actually alr= eady NULL at every branch you flagged: nfa_advance
resets it just before= crossing into nfa_advance_state, and the
intermediate branches don'= t disturb it. Your experiment passing
with an added "state->next= =3D NULL" is consistent with that -- the
assignment is redundant r= ather than load-bearing.

The contract that keeps state->next sane= lives at two concentrated
points (nfa_advance entry, nfa_add_state_uniq= ue linking), and the
branches in between are pass-through. Sprinkling th= e same reset at
every branch would be defensive noise rather than a real= safety
net, so I'd leave the branches alone.

Happy to add a = short comment near nfa_advance's reset marking it
as the boundary co= ntract, so the next reader doesn't trip on the
same question.

> For function nfa_advance_var, I don't understand the meaning = of the
> variable "count", after the first Assert I have ad= ded below:
> ...
> Rerunning the regress tests shows that count= >=3D 3 occurs very infrequently.
> ...
> Can we add more co= mplex queries (more count >=3D 3) to check if the
> "count&qu= ot; variable is working correctly?

The "count" semantics w= ill read more cleanly once the absorption
README work above lands (count= s[d] =3D iteration count at nesting
depth d). For coverage I'll add = a nested reluctant quantifier to
rpr_nfa (e.g., PATTERN ((A B){3,5}? C))= to drive count through the
3..5 band repeatedly. (rpr_nfa is the suite = that already targets
Quantifier Runtime Behavior and Absorption Optimiza= tion.)


> In function nfa_add_state_unique:
> =C2=A0 =C2= =A0 /* Mark VAR in visited before duplicate check to prevent DFS loops */> =C2=A0 =C2=A0 ...
> I honestly don't understand the purpos= e of the code block above. But it doesn't
> seem to influence the= subsequent FOR LOOP;
> ...
> Could we add some comments explai= ning which external functions rely on
> this code and why it belongs = in nfa_add_state_unique?

The code is correct, but the contract is sp= lit across two functions
and currently only one side points to the other= . The visited marking
scheme is asymmetric on purpose:

=C2=A0 - N= on-VAR elements (END/ALT/BEGIN/FIN) are marked on entry to
=C2=A0 =C2=A0= nfa_advance_state because epsilon cycles must be prevented
=C2=A0 =C2= =A0 immediately.
=C2=A0 - VAR elements are marked later, in nfa_add_stat= e_unique, only
=C2=A0 =C2=A0 when added to the state list. That delay is= intentional: it
=C2=A0 =C2=A0 keeps legitimate quantifier loop-back to = the same VAR across
=C2=A0 =C2=A0 iterations possible.

The paired= cycle check sits in nfa_advance_state, and the
asymmetric-marking ratio= nale is documented there. What's missing is
the back-reference from = nfa_add_state_unique. I'll add a single line
at the marking site poi= nting back to nfa_advance_state.


> nfa_states_equal
> c= ompareDepth =3D elem->depth + 1; /* depth 0 needs 1 count, etc. */
&g= t; The comment above isn't helpful, IMHO, and I don't understand it= .
> We should focus on why compareDepth should be ```elem->depth += 1```.

Agree the trailing comment is too terse. Two pieces are missi= ng:

=C2=A0 (a) The +1 arithmetic: to compare counts up to depth N, w= e need
=C2=A0 =C2=A0 =C2=A0 slots counts[0..N], which is N+1 entries.=C2=A0 (b) Why deeper slots are excluded: counts[d > elem->depth] ar= e
=C2=A0 =C2=A0 =C2=A0 scratch state from deeper groups and get reset on= re-entry,
=C2=A0 =C2=A0 =C2=A0 so they must not participate in equivale= nce judgment.

Two states sharing elemIdx are equivalent iff all
e= nclosing-or-current depth counts match. I'll replace the trailing
co= mment with a small block covering both pieces.


> function nfa= _add_state_unique return value is not being used?
> Do we need to do = something with the return value, or is this expected?
> (I don't = have an opinion on it, I guess it would be better to raise this issue)
<= br>Leftover from an earlier design -- the duplicate case is fully
handle= d inside the function (the state is freed and nfaStatesMerged
is increme= nted), so callers have nothing to branch on, and indeed
none of them do.= Will change the signature from bool to void and
drop the return stateme= nts.


> In nfa_advance_alt, during the main WHILE loop, I thin= k altElem->depth
> must be larger than elem->depth.
> The= refore we can do
> ``````
> =C2=A0 =C2=A0 =C2=A0 =C2=A0 if (alt= Elem->depth =3D=3D elem->depth)
> =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0 elog(ERROR, "nfa_advance_alt altElem->depth should no= t be
> the same as elem->depth reached");
> =C2=A0 =C2= =A0 =C2=A0 =C2=A0 if (altElem->depth < elem->depth)
> =C2=A0= =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 break;
> ``````

I had to p= ush back on this one. Tracing the depth bookkeeping:

=C2=A0 - For an= ALT at depth D, branches sit at depth D+1, and each
=C2=A0 =C2=A0 branc= h's first element has .jump pointing to the next branch's
=C2=A0= =C2=A0 first (set in fillRPRPatternAlt). So the walk normally
=C2=A0 = =C2=A0 terminates when the last branch's .jump =3D INVALID -- the depth=
=C2=A0 =C2=A0 check doesn't fire at all.
=C2=A0 - But when the l= ast branch is a quantified group, its first
=C2=A0 =C2=A0 element is a B= EGIN whose .jump =3D past-END (set by
=C2=A0 =C2=A0 fillRPRPatternGroup = and not overridden for the last branch).
=C2=A0 =C2=A0 The walk then ste= ps to a post-ALT element, and the depth check
=C2=A0 =C2=A0 is what stop= s it from creating a stray state out there.

That post-ALT element ha= s depth <=3D D:

=C2=A0 * D-1 if the ALT is inside an enclosing gr= oup with a non-trivial
=C2=A0 =C2=A0 quantifier, e.g., PATTERN ((A | (B = C)+){2}) -- post-ALT lands
=C2=A0 =C2=A0 on the outer END at depth 0, AL= T at depth 1. (A {1,1} outer
=C2=A0 =C2=A0 wrap gets removed by single-c= hild unwrap, so it has to be a
=C2=A0 =C2=A0 real quantifier.)
=C2=A0= * D if the ALT has a sibling at the same level, e.g.,
=C2=A0 =C2=A0 PAT= TERN (A | (B C)+) at top level -- post-ALT is FIN at depth 0,
=C2=A0 =C2= =A0 matching the ALT's depth 0.

So "altElem->depth =3D= =3D elem->depth" is a legitimate end-of-walk
signal for the quan= tified-group-last-branch case, not an invariant
violation. Treating it a= s an error would misfire on patterns like
A | (B C)+. The current "= if (altElem->depth <=3D elem->depth) break;"
in nfa_advanc= e_alt is intentionally <=3D and not <, and the looser
comparison i= s correct. Happy to add a brief comment there noting
the trigger conditi= on, if it would help future readers.


Summary of decisions, in th= e order above:

=C2=A0 Short-circuit optimization =C2=A0 =C2=A0 =C2= =A0 Separate series -- invite you to drive
=C2=A0 Absorption README narr= ative =C2=A0 =C2=A0 =C2=A0Accept -- Chapter VIII summary + example
=C2= =A0 AST-level "absorption" rename =C2=A0 =C2=A0Pending Tatsuo'= ;s call -- prefix/suffix merging?
=C2=A0 DFS expansion =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0Accept
=C2=A0 initia= lAdvance README mismatch =C2=A0 Accept -- align with current signature
= =C2=A0 Defensive Assert in advance_var =C2=A0Accept -- also add lower bound=
=C2=A0 Finalize unnecessary? =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0K= eep -- partition-end policy holder
=C2=A0 Greedy comment label =C2=A0 = =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 Accept -- rewrite to cover both cases=C2=A0 state->next reset =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0Decline -- boundary contract covers it
=C2=A0 count >=3D 3 = test coverage =C2=A0 =C2=A0 =C2=A0 =C2=A0 Accept -- add to rpr_nfa
=C2= =A0 visited marking purpose =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0Accept -- add= back-reference comment
=C2=A0 compareDepth comment =C2=A0 =C2=A0 =C2=A0= =C2=A0 =C2=A0 =C2=A0 Accept -- rewrite with intent
=C2=A0 Unused bool r= eturn =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 Accept -- change to = void
=C2=A0 ALT depth invariant Assert =C2=A0 =C2=A0 =C2=A0 Decline -- e= nd-of-walk signal, not invariant

That's the full pass. The actua= l patches (nocfbot-0016 onward) will
follow shortly as a separate submis= sion, for another review round.


Thanks,
Henson
--000000000000758c4c0652c62e2a--