From 97e13595ad516ca7ab918142eba246f4110e07d3 Mon Sep 17 00:00:00 2001 From: Tatsuo Ishii Date: Sun, 30 Aug 2026 07:20:31 +0900 Subject: [PATCH v51 5/9] Row pattern recognition patch (executor and commands). --- .../pg_stat_statements/expected/select.out | 52 + contrib/pg_stat_statements/sql/select.sql | 27 + src/backend/commands/explain.c | 458 ++++ src/backend/executor/Makefile | 1 + src/backend/executor/execExpr.c | 80 + src/backend/executor/execExprInterp.c | 257 +++ src/backend/executor/execRPR.c | 2044 +++++++++++++++++ src/backend/executor/meson.build | 1 + src/backend/executor/nodeWindowAgg.c | 1820 +++++++++++++-- src/backend/jit/llvm/llvmjit_expr.c | 78 +- src/backend/jit/llvm/llvmjit_types.c | 2 + src/include/executor/execExpr.h | 14 + src/include/executor/execRPR.h | 39 + src/include/executor/nodeWindowAgg.h | 3 + src/include/nodes/execnodes.h | 214 ++ 15 files changed, 4833 insertions(+), 257 deletions(-) create mode 100644 src/backend/executor/execRPR.c create mode 100644 src/include/executor/execRPR.h diff --git a/contrib/pg_stat_statements/expected/select.out b/contrib/pg_stat_statements/expected/select.out index a069119c790..51d0069bd5c 100644 --- a/contrib/pg_stat_statements/expected/select.out +++ b/contrib/pg_stat_statements/expected/select.out @@ -386,6 +386,58 @@ SELECT calls, query FROM pg_stat_statements ORDER BY query COLLATE "C"; DROP TABLE pgss_a, pgss_b CASCADE; -- +-- queries with a row pattern recognition window +-- +CREATE TABLE pgss_rpr (id integer); +SELECT pg_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +-- DEFINE lists that differ only in which variable name gets which condition +-- must not collide on one query id +SELECT count(*) OVER w FROM pgss_rpr +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B) + DEFINE A AS id > 50, B AS id < 50); + count +------- +(0 rows) + +SELECT count(*) OVER w FROM pgss_rpr +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B) + DEFINE B AS id > 50, A AS id < 50); + count +------- +(0 rows) + +SELECT calls, query FROM pg_stat_statements ORDER BY query COLLATE "C"; + calls | query +-------+------------------------------------------------------------------------ + 0 | SELECT calls, query FROM pg_stat_statements ORDER BY query COLLATE "C" + 1 | SELECT count(*) OVER w FROM pgss_rpr + + | WINDOW w AS ( + + | ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + + | AFTER MATCH SKIP PAST LAST ROW + + | PATTERN (A B) + + | DEFINE A AS id > $1, B AS id < $2) + 1 | SELECT count(*) OVER w FROM pgss_rpr + + | WINDOW w AS ( + + | ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + + | AFTER MATCH SKIP PAST LAST ROW + + | PATTERN (A B) + + | DEFINE B AS id > $1, A AS id < $2) + 1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t +(4 rows) + +DROP TABLE pgss_rpr; +-- -- access to pg_stat_statements_info view -- SELECT pg_stat_statements_reset() IS NOT NULL AS t; diff --git a/contrib/pg_stat_statements/sql/select.sql b/contrib/pg_stat_statements/sql/select.sql index a10d618c034..d981ef4fbeb 100644 --- a/contrib/pg_stat_statements/sql/select.sql +++ b/contrib/pg_stat_statements/sql/select.sql @@ -126,6 +126,33 @@ SELECT calls, query FROM pg_stat_statements ORDER BY query COLLATE "C"; DROP TABLE pgss_a, pgss_b CASCADE; +-- +-- queries with a row pattern recognition window +-- +CREATE TABLE pgss_rpr (id integer); + +SELECT pg_stat_statements_reset() IS NOT NULL AS t; + +-- DEFINE lists that differ only in which variable name gets which condition +-- must not collide on one query id +SELECT count(*) OVER w FROM pgss_rpr +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B) + DEFINE A AS id > 50, B AS id < 50); + +SELECT count(*) OVER w FROM pgss_rpr +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B) + DEFINE B AS id > 50, A AS id < 50); + +SELECT calls, query FROM pg_stat_statements ORDER BY query COLLATE "C"; + +DROP TABLE pgss_rpr; + -- -- access to pg_stat_statements_info view -- diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c index e35e0a649b3..53b4e818019 100644 --- a/src/backend/commands/explain.c +++ b/src/backend/commands/explain.c @@ -30,6 +30,7 @@ #include "nodes/extensible.h" #include "nodes/makefuncs.h" #include "nodes/nodeFuncs.h" +#include "optimizer/rpr.h" #include "parser/analyze.h" #include "parser/parsetree.h" #include "rewrite/rewriteHandler.h" @@ -119,6 +120,14 @@ static void show_window_def(WindowAggState *planstate, static void show_window_keys(StringInfo buf, PlanState *planstate, int nkeys, AttrNumber *keycols, List *ancestors, ExplainState *es); +static void append_rpr_quantifier(StringInfo buf, RPRPatternElement *elem); +static char *deparse_rpr_pattern(RPRPattern *pattern); +static void deparse_rpr_seq(RPRPattern *pattern, int start, int limit, + StringInfo buf); +static int deparse_rpr_node(RPRPattern *pattern, int idx, int limit, + StringInfo buf); +static int rpr_match_end(RPRPattern *pattern, int beginIdx); +static int rpr_alt_scope_end(RPRPattern *pattern, int idx); static void show_storage_info(char *maxStorageType, int64 maxSpaceUsed, ExplainState *es); static void show_tablesample(TableSampleClause *tsc, PlanState *planstate, @@ -129,6 +138,7 @@ static void show_incremental_sort_info(IncrementalSortState *incrsortstate, static void show_hash_info(HashState *hashstate, ExplainState *es); static void show_material_info(MaterialState *mstate, ExplainState *es); static void show_windowagg_info(WindowAggState *winstate, ExplainState *es); +static void show_rpr_nfa_stats(WindowAggState *winstate, ExplainState *es); static void show_ctescan_info(CteScanState *ctescanstate, ExplainState *es); static void show_table_func_scan_info(TableFuncScanState *tscanstate, ExplainState *es); @@ -2898,6 +2908,243 @@ show_sortorder_options(StringInfo buf, Node *sortexpr, } } +/* + * Append quantifier suffix for a pattern element. + */ +static void +append_rpr_quantifier(StringInfo buf, RPRPatternElement *elem) +{ + /* Append quantifier if not {1,1} */ + if (elem->min == 0 && elem->max == RPR_QUANTITY_INF) + appendStringInfoChar(buf, '*'); + else if (elem->min == 1 && elem->max == RPR_QUANTITY_INF) + appendStringInfoChar(buf, '+'); + else if (elem->min == 0 && elem->max == 1) + appendStringInfoChar(buf, '?'); + else if (elem->max == RPR_QUANTITY_INF) + appendStringInfo(buf, "{%d,}", elem->min); + else if (elem->min == elem->max && elem->min != 1) + appendStringInfo(buf, "{%d}", elem->min); + else if (elem->min != 1 || elem->max != 1) + appendStringInfo(buf, "{%d,%d}", elem->min, elem->max); + + /* A fixed count is normalized to greedy, so '?' cannot be read as {0,1} */ + if (RPRElemIsReluctant(elem)) + { + Assert(elem->min != elem->max); + appendStringInfoChar(buf, '?'); + } + + /* + * Append absorption markers: # for the comparison point, ~ for the + * absorbable region. Neither character can occur in a bare pattern + * variable name, and a name that does contain one is always double-quoted + * by quote_identifier(), so a marker is never read as part of the name. + */ + if (RPRElemIsAbsorbable(elem)) + { + Assert(elem->max == RPR_QUANTITY_INF); + appendStringInfoChar(buf, '#'); + } + else if (RPRElemIsAbsorbableBranch(elem)) + appendStringInfoChar(buf, '~'); +} + +/* + * Deparse a compiled RPRPattern (bytecode) back to a pattern string. + * + * The flat RPRPatternElement[] array is walked by recursive descent. Each + * construct is deparsed within an inherited [start, limit) window: the parent + * passes the boundary down, so each construct's extent is fixed by its caller. + * Three signals drive the walk: + * + * - a GROUP body's end comes from depth, via rpr_match_end(); an ALT's + * scope end comes from its SEP chain, via rpr_alt_scope_end(). + * - branch boundaries (where a "|" goes) come from the ALT's SEP chain: each + * branch is terminated by a SEP whose jump links to the next branch's SEP + * (-1 on the last), so a branch runs from its content start up to its SEP. + * - parentheses come from structure (a BEGIN group, an ALT) plus a one-step + * lookahead for a group that wraps a lone ALT. + * + * depth and the SEP chain are stable across the next/jump values the compiler + * assigns to branch tails and nested alternations, which is what makes them + * suitable to anchor scope and branch boundaries. + * + * EXPLAIN parenthesizes every ALT on its own, so a top-level "A | B" deparses + * as "(a | b)". This self-consistent EXPLAIN form is the correctness oracle + * here; pg_get_viewdef differs, as its parens come only from an enclosing + * GROUP. Absorption markers (# ~) are orthogonal and handled by + * append_rpr_quantifier(). + * + * Two compiler invariants hold throughout: {1,1} groups are unwrapped before + * bytecode generation (so every BEGIN/END group carries a non-trivial + * quantifier, and a lone ALT inside a group always spans to the group's END), + * and a group's quantifier is read from its END element (the BEGIN copy is + * ignored). + */ +static char * +deparse_rpr_pattern(RPRPattern *pattern) +{ + StringInfoData buf; + + Assert(pattern != NULL && pattern->numElements >= 2); + + initStringInfo(&buf); + deparse_rpr_seq(pattern, 0, pattern->numElements, &buf); + return buf.data; +} + +/* + * Deparse a run of sibling elements in [start, limit), separated by spaces. + * + * Stops at limit or at the FIN terminator (top-level call passes limit = + * numElements, where the last element is FIN). + */ +static void +deparse_rpr_seq(RPRPattern *pattern, int start, int limit, StringInfo buf) +{ + int i = start; + bool first = true; + + while (i < limit && !RPRElemIsFin(&pattern->elements[i])) + { + if (!first) + appendStringInfoChar(buf, ' '); + first = false; + i = deparse_rpr_node(pattern, i, limit, buf); + } +} + +/* + * Deparse the single construct starting at index idx, bounded by the + * inherited limit. Returns the index just past the construct. + * + * A VAR is its name plus quantifier. A BEGIN opens a group spanning to its + * matching END (rpr_match_end); when the group's sole child is an ALT that + * runs to the END, the ALT supplies the parentheses and the group only adds + * the quantifier, otherwise the group body is wrapped in its own "( )". An + * ALT runs to its SEP-chain scope end (capped by the inherited limit) and + * emits "( b1 | b2 | ... )", each branch deparsed within the boundary handed + * down by its SEP chain. + */ +static int +deparse_rpr_node(RPRPattern *pattern, int idx, int limit, StringInfo buf) +{ + RPRPatternElement *elem = &pattern->elements[idx]; + + if (RPRElemIsVar(elem)) + { + Assert(elem->varId < pattern->numVars); + appendStringInfoString(buf, + quote_pattern_variable(pattern->varNames[elem->varId])); + append_rpr_quantifier(buf, elem); + return idx + 1; + } + + if (RPRElemIsBegin(elem)) + { + int end = rpr_match_end(pattern, idx); + bool loneAlt; + + loneAlt = (idx + 1 < end && + RPRElemIsAlt(&pattern->elements[idx + 1]) && + rpr_alt_scope_end(pattern, idx + 1) == end); + + if (loneAlt) + { + /* The ALT child already parenthesizes the whole group body. */ + (void) deparse_rpr_node(pattern, idx + 1, end, buf); + } + else + { + appendStringInfoChar(buf, '('); + deparse_rpr_seq(pattern, idx + 1, end, buf); + appendStringInfoChar(buf, ')'); + } + append_rpr_quantifier(buf, &pattern->elements[end]); + return end + 1; + } + + if (RPRElemIsAlt(elem)) + { + int altEnd = rpr_alt_scope_end(pattern, idx); + int branchStart; + int sepIdx; + bool first = true; + + /* an alternation's SEP-chain scope end never exceeds the limit */ + Assert(altEnd <= limit); + + appendStringInfoChar(buf, '('); + branchStart = elem->next; + sepIdx = elem->jump; + while (sepIdx != RPR_ELEMIDX_INVALID) + { + RPRPatternElement *sepElem = &pattern->elements[sepIdx]; + + /* The branch runs up to its terminating SEP */ + Assert(RPRElemIsSep(sepElem)); + if (!first) + appendStringInfoString(buf, " | "); + first = false; + deparse_rpr_seq(pattern, branchStart, sepIdx, buf); + + /* The last branch's SEP has no link, ending the walk */ + branchStart = sepElem->next; + sepIdx = sepElem->jump; + } + appendStringInfoChar(buf, ')'); + return altEnd; + } + + pg_unreachable(); /* only VAR, BEGIN and ALT start a node */ +} + +/* + * Find the END that closes the group opened by the BEGIN at beginIdx: the + * first END at the same depth scanning forward. + */ +static int +rpr_match_end(RPRPattern *pattern, int beginIdx) +{ + RPRDepth d = pattern->elements[beginIdx].depth; + int i; + + for (i = beginIdx + 1; i < pattern->numElements; i++) + { + RPRPatternElement *e = &pattern->elements[i]; + + if (RPRElemIsEnd(e) && e->depth == d) + return i; + } + pg_unreachable(); /* a BEGIN always has a matching END */ +} + +/* + * Scope end of the alternation marker at idx: the element just past its last + * branch. Walk the SEP chain from ALT.jump to the last SEP (jump invalid); + * the element right after that SEP is the post-ALT element. Only ever called + * on an ALT. + * + * Use "last SEP index + 1", not the SEP's next: for a nested ALT the last + * SEP's next is redirected past the *enclosing* alternation by the branch-exit + * fixup in fillRPRPatternAlt, whereas the last SEP is emitted as the final + * element of the alternation, so the index after it is always this ALT's own + * post-ALT element. + */ +static int +rpr_alt_scope_end(RPRPattern *pattern, int idx) +{ + int sepIdx; + + Assert(RPRElemIsAlt(&pattern->elements[idx])); + + sepIdx = pattern->elements[idx].jump; + while (pattern->elements[sepIdx].jump != RPR_ELEMIDX_INVALID) + sepIdx = pattern->elements[sepIdx].jump; + return sepIdx + 1; +} + /* * Show the window definition for a WindowAgg node. */ @@ -2956,6 +3203,62 @@ show_window_def(WindowAggState *planstate, List *ancestors, ExplainState *es) appendStringInfoChar(&wbuf, ')'); ExplainPropertyText("Window", wbuf.data, es); pfree(wbuf.data); + + /* Show Row Pattern Recognition pattern if present */ + if (wagg->rpPattern != NULL) + { + char *patternStr = deparse_rpr_pattern(wagg->rpPattern); + + ExplainPropertyText("Pattern", patternStr, es); + + pfree(patternStr); + + /* + * Navigation offsets for tuplestore trim are resolved at executor + * init, which runs even for plain EXPLAIN, so read the resolved value + * and its kind from the planstate. + */ + if (planstate->hasMaxNav) + { + switch (planstate->navMaxOffsetKind) + { + case RPR_NAV_OFFSET_NEEDS_EVAL: + ExplainPropertyText("Nav Mark Lookback", "runtime", es); + break; + case RPR_NAV_OFFSET_RETAIN_ALL: + ExplainPropertyText("Nav Mark Lookback", "retain all", es); + break; + case RPR_NAV_OFFSET_FIXED: + ExplainPropertyInteger("Nav Mark Lookback", NULL, + planstate->navMaxOffset, es); + break; + } + } + + if (planstate->hasFirstNav) + { + switch (planstate->navFirstOffsetKind) + { + case RPR_NAV_OFFSET_NEEDS_EVAL: + ExplainPropertyText("Nav Mark Lookahead", "runtime", es); + break; + case RPR_NAV_OFFSET_FIXED: + if (planstate->navFirstOffset == PG_INT64_MAX) + ExplainPropertyText("Nav Mark Lookahead", "infinite", es); + else + ExplainPropertyInteger("Nav Mark Lookahead", NULL, + planstate->navFirstOffset, es); + break; + default: + /* a forward reach is unbounded, never retain all */ + Assert(planstate->navFirstOffsetKind == + RPR_NAV_OFFSET_NEEDS_EVAL || + planstate->navFirstOffsetKind == + RPR_NAV_OFFSET_FIXED); + break; + } + } + } } /* @@ -3513,6 +3816,7 @@ show_windowagg_info(WindowAggState *winstate, ExplainState *es) { char *maxStorageType; int64 maxSpaceUsed; + WindowAgg *wagg = (WindowAgg *) winstate->ss.ps.plan; Tuplestorestate *tupstore = winstate->buffer; @@ -3525,6 +3829,160 @@ show_windowagg_info(WindowAggState *winstate, ExplainState *es) tuplestore_get_stats(tupstore, &maxStorageType, &maxSpaceUsed); show_storage_info(maxStorageType, maxSpaceUsed, es); + + /* Show NFA statistics for Row Pattern Recognition */ + if (wagg->rpPattern != NULL) + show_rpr_nfa_stats(winstate, es); +} + +/* + * Show NFA statistics for Row Pattern Recognition on WindowAgg node. + */ +static void +show_rpr_nfa_stats(WindowAggState *winstate, ExplainState *es) +{ + if (es->format != EXPLAIN_FORMAT_TEXT) + { + /* State and context counters */ + ExplainPropertyInteger("NFA States Peak", NULL, winstate->nfaStatesMax, es); + ExplainPropertyInteger("NFA States Total", NULL, winstate->nfaStatesTotalCreated, es); + ExplainPropertyInteger("NFA States Merged", NULL, winstate->nfaStatesMerged, es); + ExplainPropertyInteger("NFA Contexts Peak", NULL, winstate->nfaContextsMax, es); + ExplainPropertyInteger("NFA Contexts Total", NULL, winstate->nfaContextsTotalCreated, es); + ExplainPropertyInteger("NFA Contexts Absorbed", NULL, winstate->nfaContextsAbsorbed, es); + ExplainPropertyInteger("NFA Contexts Skipped", NULL, winstate->nfaContextsSkipped, es); + ExplainPropertyInteger("NFA Contexts Pruned", NULL, winstate->nfaContextsPruned, es); + + /* Match/mismatch counts and length statistics */ + ExplainPropertyInteger("NFA Matched", NULL, winstate->nfaMatchesSucceeded, es); + ExplainPropertyInteger("NFA Mismatched", NULL, winstate->nfaMatchesFailed, es); + if (winstate->nfaMatchesSucceeded > 0) + { + ExplainPropertyInteger("NFA Match Length Min", NULL, winstate->nfaMatchLen.min, es); + ExplainPropertyInteger("NFA Match Length Max", NULL, winstate->nfaMatchLen.max, es); + ExplainPropertyFloat("NFA Match Length Avg", NULL, + (double) winstate->nfaMatchLen.total / winstate->nfaMatchesSucceeded, 1, + es); + } + if (winstate->nfaMatchesFailed > 0) + { + ExplainPropertyInteger("NFA Mismatch Length Min", NULL, winstate->nfaFailLen.min, es); + ExplainPropertyInteger("NFA Mismatch Length Max", NULL, winstate->nfaFailLen.max, es); + ExplainPropertyFloat("NFA Mismatch Length Avg", NULL, + (double) winstate->nfaFailLen.total / winstate->nfaMatchesFailed, 1, + es); + } + + /* Absorbed/skipped context length statistics */ + if (winstate->nfaContextsAbsorbed > 0) + { + ExplainPropertyInteger("NFA Absorbed Length Min", NULL, winstate->nfaAbsorbedLen.min, es); + ExplainPropertyInteger("NFA Absorbed Length Max", NULL, winstate->nfaAbsorbedLen.max, es); + ExplainPropertyFloat("NFA Absorbed Length Avg", NULL, + (double) winstate->nfaAbsorbedLen.total / winstate->nfaContextsAbsorbed, 1, + es); + } + if (winstate->nfaContextsSkipped > 0) + { + ExplainPropertyInteger("NFA Skipped Length Min", NULL, winstate->nfaSkippedLen.min, es); + ExplainPropertyInteger("NFA Skipped Length Max", NULL, winstate->nfaSkippedLen.max, es); + ExplainPropertyFloat("NFA Skipped Length Avg", NULL, + (double) winstate->nfaSkippedLen.total / winstate->nfaContextsSkipped, 1, + es); + } + } + else + { + /* State and context counters */ + ExplainIndentText(es); + appendStringInfo(es->str, + "NFA States: " INT64_FORMAT " peak, " INT64_FORMAT " total, " INT64_FORMAT " merged\n", + winstate->nfaStatesMax, + winstate->nfaStatesTotalCreated, + winstate->nfaStatesMerged); + ExplainIndentText(es); + appendStringInfo(es->str, + "NFA Contexts: " INT64_FORMAT " peak, " INT64_FORMAT " total, " INT64_FORMAT " pruned\n", + winstate->nfaContextsMax, + winstate->nfaContextsTotalCreated, + winstate->nfaContextsPruned); + + /* Match/mismatch counts with length min/max/avg */ + ExplainIndentText(es); + appendStringInfoString(es->str, "NFA: "); + if (winstate->nfaMatchesSucceeded > 0) + { + double avgLen = (double) winstate->nfaMatchLen.total / winstate->nfaMatchesSucceeded; + + appendStringInfo(es->str, + INT64_FORMAT " matched (len " INT64_FORMAT "/" INT64_FORMAT "/%.1f)", + winstate->nfaMatchesSucceeded, + winstate->nfaMatchLen.min, + winstate->nfaMatchLen.max, + avgLen); + } + else + { + appendStringInfoString(es->str, "0 matched"); + } + if (winstate->nfaMatchesFailed > 0) + { + double avgFail = (double) winstate->nfaFailLen.total / winstate->nfaMatchesFailed; + + appendStringInfo(es->str, + ", " INT64_FORMAT " mismatched (len " INT64_FORMAT "/" INT64_FORMAT "/%.1f)", + winstate->nfaMatchesFailed, + winstate->nfaFailLen.min, + winstate->nfaFailLen.max, + avgFail); + } + else + { + appendStringInfoString(es->str, ", 0 mismatched"); + } + appendStringInfoChar(es->str, '\n'); + + /* Absorbed/skipped context length statistics */ + if (winstate->nfaContextsAbsorbed > 0 || winstate->nfaContextsSkipped > 0) + { + ExplainIndentText(es); + appendStringInfoString(es->str, "NFA: "); + + if (winstate->nfaContextsAbsorbed > 0) + { + double avgAbsorbed = (double) winstate->nfaAbsorbedLen.total / winstate->nfaContextsAbsorbed; + + appendStringInfo(es->str, + INT64_FORMAT " absorbed (len " INT64_FORMAT "/" INT64_FORMAT "/%.1f)", + winstate->nfaContextsAbsorbed, + winstate->nfaAbsorbedLen.min, + winstate->nfaAbsorbedLen.max, + avgAbsorbed); + } + else + { + appendStringInfoString(es->str, "0 absorbed"); + } + + if (winstate->nfaContextsSkipped > 0) + { + double avgSkipped = (double) winstate->nfaSkippedLen.total / winstate->nfaContextsSkipped; + + appendStringInfo(es->str, + ", " INT64_FORMAT " skipped (len " INT64_FORMAT "/" INT64_FORMAT "/%.1f)", + winstate->nfaContextsSkipped, + winstate->nfaSkippedLen.min, + winstate->nfaSkippedLen.max, + avgSkipped); + } + else + { + appendStringInfoString(es->str, ", 0 skipped"); + } + + appendStringInfoChar(es->str, '\n'); + } + } } /* diff --git a/src/backend/executor/Makefile b/src/backend/executor/Makefile index 11118d0ce02..2b257427795 100644 --- a/src/backend/executor/Makefile +++ b/src/backend/executor/Makefile @@ -25,6 +25,7 @@ OBJS = \ execParallel.o \ execPartition.o \ execProcnode.o \ + execRPR.o \ execReplication.o \ execSRF.o \ execScan.o \ diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index 82e846a1f4f..fa58de60c91 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -1168,6 +1168,86 @@ ExecInitExprRec(Expr *node, ExprState *state, break; } + case T_RPRNavExpr: + { + /* + * RPR navigation functions (PREV/NEXT/FIRST/LAST) are + * compiled into EEOP_RPR_NAV_SET / EEOP_RPR_NAV_RESTORE + * opcodes instead of a normal function call. The SET opcode + * swaps ecxt_outertuple to the target row, the argument + * expression is compiled normally (reads from the swapped + * slot), and the RESTORE opcode restores the original slot. + */ + RPRNavState *rprnavstate; + RPRNavOffsets *entry; + RPRNavExpr *nav = (RPRNavExpr *) node; + WindowAggState *winstate; + int skip_arg_step; + + Assert(state->parent && IsA(state->parent, WindowAggState)); + winstate = (WindowAggState *) state->parent; + + /* + * The offsets live in executor state, not on the RPRNavExpr, + * because the plan tree is read-only. navno indexes the list + * build_define_offsets() filled at startup; the values in it + * are settled per scan by resolve_nav_offsets(). + */ + if (nav->navno < 0 || + nav->navno >= list_length(winstate->rprNavOffsets)) + elog(ERROR, "RPRNavExpr navno %d out of range for %d offsets entries", + nav->navno, list_length(winstate->rprNavOffsets)); + + entry = list_nth(winstate->rprNavOffsets, nav->navno); + if (entry->nav != nav) + elog(ERROR, "offsets entry %d belongs to a different RPRNavExpr", + nav->navno); + rprnavstate = entry->rprnavstate; + + /* Emit SET opcode: swap slot to target row */ + scratch.opcode = EEOP_RPR_NAV_SET; + scratch.d.rpr_nav.rprnavstate = rprnavstate; + + ExprEvalPushStep(state, &scratch); + + /* + * If the target row does not exist, skip evaluation of the + * argument expression and go straight to RESTORE. The + * EEOP_RPR_NAV_SET step writes a definitive resnull (false + * when the target row exists), so the jump condition is + * always up to date. + */ + skip_arg_step = state->steps_len; + scratch.opcode = EEOP_JUMP_IF_NULL; + scratch.resvalue = resv; + scratch.resnull = resnull; + scratch.d.jump.jumpdone = -1; /* set below */ + ExprEvalPushStep(state, &scratch); + + /* Compile the argument expression normally */ + ExecInitExprRec(nav->arg, state, resv, resnull); + + /* out-of-range jump lands on the RESTORE step */ + state->steps[skip_arg_step].d.jump.jumpdone = state->steps_len; + + /* Emit RESTORE opcode: restore original slot */ + scratch.opcode = EEOP_RPR_NAV_RESTORE; + scratch.resvalue = resv; + scratch.resnull = resnull; + scratch.d.rpr_nav.rprnavstate = rprnavstate; + + /* + * The state is shared with the offsets entry, but resulttype + * belongs to the plan node, so every compilation of this + * navigation writes the same pair. + */ + get_typlenbyval(nav->resulttype, + &rprnavstate->resulttyplen, + &rprnavstate->resulttypbyval); + ExprEvalPushStep(state, &scratch); + break; + } + case T_MergeSupportFunc: { /* must be in a MERGE, else something messed up */ diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 397219f7a3a..d19e20a2eaa 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -60,8 +60,10 @@ #include "access/tupconvert.h" #include "catalog/pg_type.h" #include "commands/sequence.h" +#include "common/int.h" #include "executor/execExpr.h" #include "executor/nodeSubplan.h" +#include "executor/nodeWindowAgg.h" #include "funcapi.h" #include "miscadmin.h" #include "nodes/miscnodes.h" @@ -586,6 +588,8 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) &&CASE_EEOP_WINDOW_FUNC, &&CASE_EEOP_MERGE_SUPPORT_FUNC, &&CASE_EEOP_SUBPLAN, + &&CASE_EEOP_RPR_NAV_SET, + &&CASE_EEOP_RPR_NAV_RESTORE, &&CASE_EEOP_AGG_STRICT_DESERIALIZE, &&CASE_EEOP_AGG_DESERIALIZE, &&CASE_EEOP_AGG_STRICT_INPUT_CHECK_ARGS, @@ -2013,6 +2017,24 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) EEO_NEXT(); } + /* RPR navigation: swap slot to target row */ + EEO_CASE(EEOP_RPR_NAV_SET) + { + ExecEvalRPRNavSet(state, op, econtext); + outerslot = econtext->ecxt_outertuple; + + EEO_NEXT(); + } + + /* RPR navigation: restore slot to original row */ + EEO_CASE(EEOP_RPR_NAV_RESTORE) + { + ExecEvalRPRNavRestore(state, op, econtext); + outerslot = econtext->ecxt_outertuple; + + EEO_NEXT(); + } + /* evaluate a strict aggregate deserialization function */ EEO_CASE(EEOP_AGG_STRICT_DESERIALIZE) { @@ -5988,3 +6010,238 @@ ExecAggPlainTransByRef(AggState *aggstate, AggStatePerTrans pertrans, MemoryContextSwitchTo(oldContext); } + +/* + * Evaluate RPR navigation (PREV/NEXT/FIRST/LAST): swap slot to target row. + * + * Saves the current outertuple into winstate for later restore, computes + * the target row position, fetches the corresponding slot from the + * tuplestore, and replaces econtext->ecxt_outertuple with it. + * + * This is called both from the interpreter inline handler and from + * JIT-compiled expressions via build_EvalXFunc. + */ +void +ExecEvalRPRNavSet(ExprState *state, ExprEvalStep *op, ExprContext *econtext) +{ + WindowAggState *winstate; + int64 offset; + int64 compound_offset; + int64 target_pos; + TupleTableSlot *target_slot; + RPRNavState *rprnavstate = op->d.rpr_nav.rprnavstate; + + winstate = rprnavstate->winstate; + + /* Save current slot for later restore */ + winstate->nav_saved_outertuple = econtext->ecxt_outertuple; + + /* + * resolve_nav_offsets() settled both offsets for this scan: it writes + * them as non-null, and where either is negative it raises the error and + * never writes them at all. Assert the invariants rather than repeating + * those checks here. + */ + Assert(!rprnavstate->offset.isnull && !rprnavstate->compound_offset.isnull); + + offset = DatumGetInt64(rprnavstate->offset.value); + compound_offset = DatumGetInt64(rprnavstate->compound_offset.value); + + Assert(offset >= 0 && compound_offset >= 0); + + /* + * Calculate target position based on navigation direction. On overflow, + * use -1 so that ExecRPRNavGetSlot treats it as out of range. + */ + switch (rprnavstate->rprnavexpr->kind) + { + case RPR_NAV_PREV: + + /* + * currentpos and offset are both non-negative, so the subtraction + * cannot underflow; assert the invariant rather than guarding an + * unreachable overflow. + */ + Assert(!pg_sub_s64_overflow(winstate->currentpos, offset, &target_pos)); + target_pos = winstate->currentpos - offset; + break; + case RPR_NAV_NEXT: + if (pg_add_s64_overflow(winstate->currentpos, offset, &target_pos)) + target_pos = -1; + break; + case RPR_NAV_FIRST: + /* FIRST: offset from match_start, clamped to currentpos */ + if (pg_add_s64_overflow(winstate->nav_match_start, offset, &target_pos)) + target_pos = -1; + else if (target_pos > winstate->currentpos) + target_pos = -1; /* beyond current match range */ + break; + case RPR_NAV_LAST: + /* LAST: offset backward from currentpos, clamped to match_start */ + if (pg_sub_s64_overflow(winstate->currentpos, offset, &target_pos)) + target_pos = -1; + else if (target_pos < winstate->nav_match_start) + target_pos = -1; /* before match_start */ + break; + + case RPR_NAV_PREV_FIRST: + case RPR_NAV_NEXT_FIRST: + { + int64 inner_pos; + + /* Inner: match_start + offset */ + if (pg_add_s64_overflow(winstate->nav_match_start, offset, &inner_pos)) + { + target_pos = -1; + break; + } + if (inner_pos > winstate->currentpos || inner_pos < 0) + { + target_pos = -1; + break; + } + + /* Apply outer: PREV subtracts, NEXT adds */ + if (rprnavstate->rprnavexpr->kind == RPR_NAV_PREV_FIRST) + { + /* + * inner_pos is in [0, currentpos] and compound_offset is + * non-negative, so this cannot underflow. + */ + Assert(!pg_sub_s64_overflow(inner_pos, compound_offset, &target_pos)); + target_pos = inner_pos - compound_offset; + } + else + { + if (pg_add_s64_overflow(inner_pos, compound_offset, &target_pos)) + target_pos = -1; + } + } + break; + + case RPR_NAV_PREV_LAST: + case RPR_NAV_NEXT_LAST: + { + int64 inner_pos; + + /* Inner: currentpos - offset */ + if (pg_sub_s64_overflow(winstate->currentpos, offset, &inner_pos)) + { + target_pos = -1; + break; + } + if (inner_pos < winstate->nav_match_start) + { + target_pos = -1; + break; + } + + /* Apply outer: PREV subtracts, NEXT adds */ + if (rprnavstate->rprnavexpr->kind == RPR_NAV_PREV_LAST) + { + /* + * inner_pos is in [nav_match_start, currentpos] (>= 0) + * and compound_offset is non-negative, so this cannot + * underflow. + */ + Assert(!pg_sub_s64_overflow(inner_pos, compound_offset, &target_pos)); + target_pos = inner_pos - compound_offset; + } + else + { + if (pg_add_s64_overflow(inner_pos, compound_offset, &target_pos)) + target_pos = -1; + } + } + break; + default: + elog(ERROR, "unrecognized RPR navigation kind: %d", + (int) rprnavstate->rprnavexpr->kind); + break; + } + + /* + * Slot swap elision: if target_pos is the current row, skip the + * tuplestore fetch and slot swap entirely. This benefits LAST(expr), + * PREV(expr, 0), NEXT(expr, 0), and similar cases. + * + * We must still set nav_saved_outertuple (done above) so that + * EEOP_RPR_NAV_RESTORE is a harmless no-op. + */ + if (target_pos == winstate->currentpos) + { + /* target row trivially exists; see comment below */ + *op->resnull = false; + return; + } + + target_slot = ExecRPRNavGetSlot(winstate, target_pos); + + /* + * Report whether the target row exists through resnull, which the jump + * step tests before the argument expression gets to overwrite it: null + * when the row is out of range, so the jump skips the argument, and a + * definitive false otherwise, since resnull may still hold a stale value + * from a previous evaluation. + */ + if (target_slot == NULL) + { + *op->resvalue = (Datum) 0; + *op->resnull = true; + return; + } + *op->resnull = false; + + /* + * Update econtext to point to the target slot. Also decompress the new + * slot's attributes since FETCHSOME already ran for the original slot. + * The caller (interpreter or JIT) is responsible for updating any local + * slot cache (e.g. outerslot) from econtext after we return. + */ + slot_getallattrs(target_slot); + econtext->ecxt_outertuple = target_slot; +} + +/* + * Evaluate RPR navigation: restore slot to original row. + * + * Restores econtext->ecxt_outertuple from the saved slot in winstate. + * The caller is responsible for updating any local slot cache. + * + * For pass-by-reference result types, the result datum points into + * nav_slot's tuple memory. If a subsequent navigation in the same + * expression re-fetches nav_slot for a different position, the old + * tuple is freed, leaving a dangling pointer. We prevent this by + * copying pass-by-ref results into per-tuple memory, which survives + * until the next ResetExprContext. + */ +void +ExecEvalRPRNavRestore(ExprState *state, ExprEvalStep *op, + ExprContext *econtext) +{ + WindowAggState *winstate = op->d.rpr_nav.rprnavstate->winstate; + + /* + * When the slot swap was elided (target == currentpos), restoring is a + * no-op, and the argument read the current row's slot rather than + * nav_slot, so no re-fetch of nav_slot can invalidate a pass-by-ref + * result. + */ + if (econtext->ecxt_outertuple == winstate->nav_saved_outertuple) + return; + + econtext->ecxt_outertuple = winstate->nav_saved_outertuple; + + /* Stabilize pass-by-ref result against nav_slot re-fetch */ + if (!op->d.rpr_nav.rprnavstate->resulttypbyval && + !*op->resnull) + { + MemoryContext oldContext; + + oldContext = MemoryContextSwitchTo(econtext->ecxt_per_tuple_memory); + *op->resvalue = datumCopy(*op->resvalue, + false, + op->d.rpr_nav.rprnavstate->resulttyplen); + MemoryContextSwitchTo(oldContext); + } +} diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c new file mode 100644 index 00000000000..68664568556 --- /dev/null +++ b/src/backend/executor/execRPR.c @@ -0,0 +1,2044 @@ +/*------------------------------------------------------------------------- + * + * execRPR.c + * NFA-based Row Pattern Recognition engine for window functions. + * + * This file implements the NFA execution engine for the ROWS BETWEEN + * PATTERN clause (SQL Standard Feature R020: Row Pattern Recognition in + * Window Functions). + * + * The engine executes the compiled RPRPattern structure directly, avoiding + * regex compilation overhead. It is called by nodeWindowAgg.c and exposes + * the interface declared in executor/execRPR.h. + * + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/executor/execRPR.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "common/int.h" +#include "executor/execRPR.h" +#include "executor/executor.h" +#include "miscadmin.h" +#include "optimizer/rpr.h" +#include "utils/memutils.h" + +/* + * For the design and execution model of the NFA engine implemented + * in this file, see src/backend/executor/README.rpr. + */ + +/* 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) + +/* + * Set the visited bit for elemIdx and update the high-water marks + * (nfaVisitedMin/MaxWord) so that the next reset only has to clear + * the touched range instead of the full nfaVisitedEnds bitmap. + */ +static inline void +nfa_mark_visited(WindowAggState *winstate, int16 elemIdx) +{ + int16 w = WORDNUM(elemIdx); + + winstate->nfaVisitedEnds[w] |= ((bitmapword) 1 << BITNUM(elemIdx)); + winstate->nfaVisitedMinWord = Min(winstate->nfaVisitedMinWord, w); + winstate->nfaVisitedMaxWord = Max(winstate->nfaVisitedMaxWord, w); +} + +/* Forward declarations */ +static RPRNFAState *nfa_state_make(WindowAggState *winstate); +static void nfa_state_free(WindowAggState *winstate, RPRNFAState *state); +static void nfa_state_free_list(WindowAggState *winstate, RPRNFAState *list); +static RPRNFAState *nfa_state_clone(WindowAggState *winstate, int16 elemIdx, + int32 *counts, bool sourceAbsorbable); +static bool nfa_states_equal(WindowAggState *winstate, RPRNFAState *s1, + RPRNFAState *s2); +static void nfa_add_state_unique(WindowAggState *winstate, RPRNFAContext *ctx, + RPRNFAState *state); +static void nfa_add_matched_state(WindowAggState *winstate, RPRNFAContext *ctx, + RPRNFAState *state, int64 matchEndRow); + +static RPRNFAContext *nfa_context_make(WindowAggState *winstate); +static void nfa_unlink_context(WindowAggState *winstate, RPRNFAContext *ctx); + +static void nfa_update_length_stats(int64 count, NFALengthStats *stats, int64 newLen); +static void nfa_record_context_skipped(WindowAggState *winstate, int64 skippedLen); +static void nfa_record_context_absorbed(WindowAggState *winstate, int64 absorbedLen); + +static void nfa_update_absorption_flags(RPRNFAContext *ctx); +static bool nfa_states_covered(RPRPattern *pattern, RPRNFAContext *older, + RPRNFAContext *newer); +static void nfa_try_absorb_context(WindowAggState *winstate, RPRNFAContext *ctx); +static void nfa_absorb_contexts(WindowAggState *winstate); + +static bool nfa_eval_var_match(WindowAggState *winstate, + RPRPatternElement *elem, RPRVarMatch *varMatched); +static void nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, + RPRVarMatch *varMatched, int64 currentPos); +static void nfa_route_to_elem(WindowAggState *winstate, RPRNFAContext *ctx, + RPRNFAState *state, RPRPatternElement *nextElem, + int64 currentPos); +static void nfa_advance_alt(WindowAggState *winstate, RPRNFAContext *ctx, + RPRNFAState *state, RPRPatternElement *elem, + int64 currentPos); +static void nfa_advance_begin(WindowAggState *winstate, RPRNFAContext *ctx, + RPRNFAState *state, RPRPatternElement *elem, + int64 currentPos); +static void nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx, + RPRNFAState *state, RPRPatternElement *elem, + int64 currentPos); +static void nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx, + RPRNFAState *state, RPRPatternElement *elem, + int64 currentPos); +static void nfa_advance_state(WindowAggState *winstate, RPRNFAContext *ctx, + RPRNFAState *state, int64 currentPos); +static void nfa_advance(WindowAggState *winstate, RPRNFAContext *ctx, + int64 currentPos); + +static void nfa_reevaluate_dependent_vars(WindowAggState *winstate, + RPRNFAContext *ctx, + int64 currentPos); + +/* + * The engine runs three phases per row: match (evaluate VARs, prune dead + * states), absorb (drop contexts an older context already covers), advance + * (expand epsilon transitions until states park on VARs). Per-element + * advance behaviour, the absorption argument and the dual-flag contract are + * documented in README.rpr chapters VIII and IX and in the RPRNFAContext + * comment in nodes/execnodes.h. + */ + +/* + * nfa_state_make + * + * Allocate an NFA state, reusing from freeList if available. + * freeList is stored in WindowAggState for reuse across match attempts. + */ +static RPRNFAState * +nfa_state_make(WindowAggState *winstate) +{ + RPRNFAState *state; + + /* Try to reuse from free list first */ + if (winstate->nfaStateFree != NULL) + { + state = winstate->nfaStateFree; + winstate->nfaStateFree = state->next; + } + else + { + /* Allocate in partition context for proper lifetime */ + state = MemoryContextAlloc(winstate->partcontext, winstate->nfaStateSize); + } + + /* Initialize entire state to zero */ + memset(state, 0, winstate->nfaStateSize); + + /* Update statistics */ + winstate->nfaStatesActive++; + winstate->nfaStatesTotalCreated++; + winstate->nfaStatesMax = Max(winstate->nfaStatesMax, + winstate->nfaStatesActive); + + return state; +} + +/* + * nfa_state_free + * + * Return a state to the free list for later reuse. + */ +static void +nfa_state_free(WindowAggState *winstate, RPRNFAState *state) +{ + winstate->nfaStatesActive--; +#ifdef USE_VALGRIND + /* real free so Valgrind catches use-after-free instead of recycling */ + pfree(state); +#else + state->next = winstate->nfaStateFree; + winstate->nfaStateFree = state; +#endif +} + +/* + * nfa_state_free_list + * + * Return all states in a list to the free list. + */ +static void +nfa_state_free_list(WindowAggState *winstate, RPRNFAState *list) +{ + RPRNFAState *next; + + for (; list != NULL; list = next) + { + next = list->next; + nfa_state_free(winstate, list); + } +} + +/* + * nfa_state_clone + * + * Clone a state from the given elemIdx and counts. + * isAbsorbable is computed immediately: inherited AND new element's flag. + * Monotonic property: once false, stays false through all transitions. + * + * Caller is responsible for linking the returned state. + */ +static RPRNFAState * +nfa_state_clone(WindowAggState *winstate, int16 elemIdx, + int32 *counts, bool sourceAbsorbable) +{ + RPRPattern *pattern = winstate->rpPattern; + int maxDepth = pattern->maxDepth; + RPRNFAState *state = nfa_state_make(winstate); + RPRPatternElement *elem = &pattern->elements[elemIdx]; + + state->elemIdx = elemIdx; + /* Every reachable caller passes a live state's counts; maxDepth >= 1. */ + Assert(counts != NULL && maxDepth > 0); + memcpy(state->counts, counts, sizeof(int32) * maxDepth); + + /* + * Compute isAbsorbable immediately at transition time. isAbsorbable = + * sourceAbsorbable && (elem->flags & ABSORBABLE_BRANCH) Monotonic: once + * false, stays false (can't re-enter absorbable region). + */ + state->isAbsorbable = sourceAbsorbable && RPRElemIsAbsorbableBranch(elem); + + return state; +} + +/* + * nfa_exit_to + * + * Move state out of the construct owning depth and onto targetIdx, then + * return the target element. Callers route from there. + * + * Centralizes three conventions whose violations are silent: + * + * - Count-clear: zero the exited depth slot so the next occupant enters at + * zero (asserted on entry by nfa_advance_begin/nfa_route_to_elem). + * - Arrival increment: landing on an END completes one iteration + * (saturating at RPR_COUNT_INF). + * - isAbsorbable is recomputed against the target and is monotonic. + * Reapplying it is idempotent, so clone and in-place callers share this path. + */ +static RPRPatternElement * +nfa_exit_to(WindowAggState *winstate, RPRNFAState *state, int depth, + int16 targetIdx) +{ + RPRPattern *pattern = winstate->rpPattern; + RPRPatternElement *nextElem; + + state->counts[depth] = 0; + state->elemIdx = targetIdx; + nextElem = &pattern->elements[targetIdx]; + + state->isAbsorbable = state->isAbsorbable && + RPRElemIsAbsorbableBranch(nextElem); + + if (RPRElemIsEnd(nextElem) && + state->counts[nextElem->depth] < RPR_COUNT_INF) + state->counts[nextElem->depth]++; + + return nextElem; +} + +/* + * nfa_states_equal + * + * Check if two states are equivalent (same elemIdx and counts). + */ +static bool +nfa_states_equal(WindowAggState *winstate, RPRNFAState *s1, RPRNFAState *s2) +{ + RPRPattern *pattern = winstate->rpPattern; + RPRPatternElement *elem; + int compareDepth; + + if (s1->elemIdx != s2->elemIdx) + return false; + + /* + * Compare counts up to current element's depth. Two states sharing + * elemIdx are equivalent iff every enclosing-or-current depth count + * matches. + * + * The +1 is the slot arithmetic: comparing through depth N requires + * counts[0..N], i.e., N+1 entries. Deeper slots (counts[d] with d > + * elem->depth) are excluded because they hold scratch state from inner + * groups. Per the count-clear policy such a slot is zeroed when its + * owning element exits (see nfa_advance_var and the inline fast path in + * nfa_match), so it must not participate in equivalence judgment. + */ + elem = &pattern->elements[s1->elemIdx]; + compareDepth = elem->depth + 1; + + if (memcmp(s1->counts, s2->counts, sizeof(int32) * compareDepth) != 0) + return false; + + return true; +} + +/* + * nfa_add_state_unique + * + * Add the state to the end of the ctx->states linked list, but only if a + * duplicate state is not already present. + * Earlier states have better lexical order (DFS traversal order), so existing + * wins; the new state is freed when a duplicate is found. + */ +static void +nfa_add_state_unique(WindowAggState *winstate, RPRNFAContext *ctx, RPRNFAState *state) +{ + RPRNFAState *s; + RPRNFAState *tail = NULL; + + /* + * Nothing is parked once this advance has recorded: a state kept here + * survives to the next row, where it could complete and replace the match + * that outranks it. + */ + Assert(!ctx->matchUpdated); + + /* Check for duplicate and find tail */ + for (s = ctx->states; s != NULL; s = s->next) + { + CHECK_FOR_INTERRUPTS(); + + if (nfa_states_equal(winstate, s, state)) + { + /* + * Duplicate found - existing has better lexical order, discard + * new + */ + nfa_state_free(winstate, state); + winstate->nfaStatesMerged++; + return; + } + tail = s; + } + + /* No duplicate, add at end */ + state->next = NULL; + if (tail == NULL) + ctx->states = state; + else + tail->next = state; +} + +/* + * nfa_add_matched_state + * + * Record a state that reached FIN, replacing any previous match. + * + * For SKIP PAST LAST ROW, also prune subsequent contexts whose start row + * falls within the match range, as they cannot produce output rows. + */ +static void +nfa_add_matched_state(WindowAggState *winstate, RPRNFAContext *ctx, + RPRNFAState *state, int64 matchEndRow) +{ + /* + * One advance records at most one match. The guards below stop every + * less-preferred path once matchUpdated is set, so arriving here twice + * would mean a path that can reach FIN is not reading it. + */ + Assert(!ctx->matchUpdated); + + if (ctx->matchedState != NULL) + nfa_state_free(winstate, ctx->matchedState); + + ctx->matchedState = state; + state->next = NULL; + ctx->matchEndRow = matchEndRow; + + /* + * Tell the frames that are unwinding. FIN is not marked visited, so an + * expansion can reach it more than once, and the later arrival is the + * less preferred one: the paths that cut less-preferred alternatives read + * this and stop rather than record again. + */ + ctx->matchUpdated = true; + + /* Prune contexts that started within this match's range */ + if (winstate->rpSkipTo == ST_PAST_LAST_ROW) + { + int64 skippedLen; + + while (ctx->next != NULL && + ctx->next->matchStartRow <= matchEndRow) + { + RPRNFAContext *nextCtx = ctx->next; + + /* Only later-starting contexts are freed; callers walk forward */ + Assert(nextCtx->matchStartRow > ctx->matchStartRow); + Assert(nextCtx->lastProcessedRow >= nextCtx->matchStartRow); + skippedLen = nextCtx->lastProcessedRow - nextCtx->matchStartRow + 1; + nfa_record_context_skipped(winstate, skippedLen); + + ExecRPRFreeContext(winstate, nextCtx); + } + } +} + +/* + * nfa_context_make + * + * Allocate an NFA context, reusing from free list if available. + */ +static RPRNFAContext * +nfa_context_make(WindowAggState *winstate) +{ + RPRNFAContext *ctx; + + if (winstate->nfaContextFree != NULL) + { + ctx = winstate->nfaContextFree; + winstate->nfaContextFree = ctx->next; + } + else + { + /* Allocate in partition context for proper lifetime */ + ctx = MemoryContextAlloc(winstate->partcontext, sizeof(RPRNFAContext)); + } + + ctx->next = NULL; + ctx->prev = NULL; + ctx->states = NULL; + ctx->matchStartRow = -1; + ctx->matchEndRow = -1; + ctx->lastProcessedRow = -1; + ctx->matchedState = NULL; + ctx->matchUpdated = false; + + /* Initialize two-flag absorption design based on pattern */ + ctx->hasAbsorbableState = winstate->rpPattern->isAbsorbable; + ctx->allStatesAbsorbable = winstate->rpPattern->isAbsorbable; + + /* Update statistics */ + winstate->nfaContextsActive++; + winstate->nfaContextsTotalCreated++; + winstate->nfaContextsMax = Max(winstate->nfaContextsMax, + winstate->nfaContextsActive); + + return ctx; +} + +/* + * nfa_unlink_context + * + * Remove a context from the doubly-linked active context list. + * Updates head (nfaContext) and tail (nfaContextTail) as needed. + */ +static void +nfa_unlink_context(WindowAggState *winstate, RPRNFAContext *ctx) +{ + if (ctx->prev != NULL) + ctx->prev->next = ctx->next; + else + winstate->nfaContext = ctx->next; /* was head */ + + if (ctx->next != NULL) + ctx->next->prev = ctx->prev; + else + winstate->nfaContextTail = ctx->prev; /* was tail */ + + ctx->next = NULL; + ctx->prev = NULL; +} + +/* + * nfa_update_length_stats + * + * Helper function to update min/max/total length statistics. + * Called when tracking match/mismatch/absorbed/skipped lengths. + */ +static void +nfa_update_length_stats(int64 count, NFALengthStats *stats, int64 newLen) +{ + if (count == 1) + { + stats->min = newLen; + stats->max = newLen; + } + else + { + stats->min = Min(stats->min, newLen); + stats->max = Max(stats->max, newLen); + } + stats->total += newLen; +} + +/* + * nfa_record_context_skipped + * + * Record a skipped context in statistics. + */ +static void +nfa_record_context_skipped(WindowAggState *winstate, int64 skippedLen) +{ + winstate->nfaContextsSkipped++; + nfa_update_length_stats(winstate->nfaContextsSkipped, + &winstate->nfaSkippedLen, + skippedLen); +} + +/* + * nfa_record_context_absorbed + * + * Record an absorbed context in statistics. + */ +static void +nfa_record_context_absorbed(WindowAggState *winstate, int64 absorbedLen) +{ + winstate->nfaContextsAbsorbed++; + nfa_update_length_stats(winstate->nfaContextsAbsorbed, + &winstate->nfaAbsorbedLen, + absorbedLen); +} + +/* + * nfa_update_absorption_flags + * + * Update context's absorption flags after state changes. + * + * Two flags control absorption behavior: + * hasAbsorbableState: true if context has at least one absorbable state. + * This flag is monotonic (true -> false only). Once all absorbable states + * die, no new absorbable states can be created through transitions. + * allStatesAbsorbable: true if ALL states in context are absorbable and no + * match is recorded. Dynamic (false -> true as non-absorbable states die + * off), except that a recorded match pins it false: absorbing would free + * a match no absorbing context can reproduce. + * + * Optimization: Once hasAbsorbableState becomes false, both flags remain false + * permanently, so we skip recalculation. + */ +static void +nfa_update_absorption_flags(RPRNFAContext *ctx) +{ + RPRNFAState *state; + bool hasAbsorbable = false; + bool allAbsorbable = true; + + /* + * Optimization: Once hasAbsorbableState becomes false, it stays false. No + * need to recalculate - both flags remain false permanently. + */ + if (!ctx->hasAbsorbableState) + { + ctx->allStatesAbsorbable = false; + return; + } + + /* No states means no absorbable states */ + if (ctx->states == NULL) + { + ctx->hasAbsorbableState = false; + ctx->allStatesAbsorbable = false; + return; + } + + /* + * Iterate through all states to check absorption status. Uses + * state->isAbsorbable which tracks if state is in absorbable region. This + * is different from RPRElemIsAbsorbable(elem) which checks comparison + * point. + */ + for (state = ctx->states; state != NULL; state = state->next) + { + CHECK_FOR_INTERRUPTS(); + + if (state->isAbsorbable) + hasAbsorbable = true; + else + allAbsorbable = false; + } + + /* + * A recorded match makes this context non-absorbable: absorption would + * free the match, which no absorbing context can reproduce. + */ + if (ctx->matchedState != NULL) + allAbsorbable = false; + + ctx->hasAbsorbableState = hasAbsorbable; + ctx->allStatesAbsorbable = allAbsorbable; +} + +/* + * nfa_states_covered + * + * Check if all states in newer context are "covered" by older context. + * + * A newer state is covered when older context has an absorbable state at the + * same pattern element (elemIdx) with count >= newer's count at that depth. + * The covering state must be absorbable because only absorbable states can + * guarantee to produce superset matches. + * + * If all newer states are covered, newer context's eventual matches will be + * a subset of older context's matches, making newer redundant. + */ +static bool +nfa_states_covered(RPRPattern *pattern, RPRNFAContext *older, RPRNFAContext *newer) +{ + RPRNFAState *newerState; + + for (newerState = newer->states; newerState != NULL; newerState = newerState->next) + { + RPRNFAState *olderState; + RPRPatternElement *elem; + int depth; + bool found = false; + + /* All states are absorbable (caller checks allStatesAbsorbable) */ + elem = &pattern->elements[newerState->elemIdx]; + depth = elem->depth; + + /* + * Only compare at absorption comparison points (RPR_ELEM_ABSORBABLE). + * Comparison points are where count-dominance guarantees the newer + * context's future matches are a subset of the older's. + */ + if (!RPRElemIsAbsorbable(elem)) + return false; + + for (olderState = older->states; olderState != NULL; olderState = olderState->next) + { + CHECK_FOR_INTERRUPTS(); + + /* Covering state must also be absorbable */ + if (olderState->isAbsorbable && + olderState->elemIdx == newerState->elemIdx && + olderState->counts[depth] >= newerState->counts[depth]) + { + found = true; + break; + } + } + + if (!found) + return false; + } + + return true; +} + +/* + * nfa_try_absorb_context + * + * Try to absorb ctx (newer) into an older in-progress context. If one is + * found, ctx is unlinked and freed here. + * + * Absorption requires three conditions: + * 1. ctx must have all states absorbable (allStatesAbsorbable). + * If ctx has any non-absorbable state, it may produce unique matches. + * 2. older must have at least one absorbable state (hasAbsorbableState). + * Without absorbable states, older cannot cover newer's states. + * 3. All ctx states must be covered by older's absorbable states. + * This ensures older will produce all matches that ctx would produce. + * + * Context list is ordered by creation time (oldest first via prev chain). + * Each row creates at most one context, so earlier contexts have smaller + * matchStartRow values. + */ +static void +nfa_try_absorb_context(WindowAggState *winstate, RPRNFAContext *ctx) +{ + RPRPattern *pattern = winstate->rpPattern; + RPRNFAContext *older; + + /* Early exit: ctx must have all states absorbable */ + if (!ctx->allStatesAbsorbable) + return; + + for (older = ctx->prev; older != NULL; older = older->prev) + { + CHECK_FOR_INTERRUPTS(); + + /* + * By invariant: ctx->prev chain is in creation order (oldest first), + * and each row creates at most one context. So all contexts in this + * chain have matchStartRow < ctx->matchStartRow. + */ + + /* Older must also be in-progress */ + if (older->states == NULL) + continue; + + /* Older must have at least one absorbable state */ + if (!older->hasAbsorbableState) + continue; + + /* Check if all newer states are covered by older */ + if (nfa_states_covered(pattern, older, ctx)) + { + int64 absorbedLen = ctx->lastProcessedRow - ctx->matchStartRow + 1; + + ExecRPRFreeContext(winstate, ctx); + nfa_record_context_absorbed(winstate, absorbedLen); + return; + } + } +} + +/* + * nfa_absorb_contexts + * + * Absorb redundant contexts to reduce memory usage and computation. + * + * For patterns like A+, newer contexts starting later will produce subset + * matches of older contexts with higher counts. By absorbing these redundant + * contexts early, we avoid duplicate work. + * + * Iterates from tail (newest) toward head (oldest) via prev chain. + * Only in-progress contexts (states != NULL) are candidates for absorption; + * completed contexts represent valid match results. + */ +static void +nfa_absorb_contexts(WindowAggState *winstate) +{ + RPRNFAContext *ctx; + RPRNFAContext *nextCtx; + + for (ctx = winstate->nfaContextTail; ctx != NULL; ctx = nextCtx) + { + nextCtx = ctx->prev; + + /* + * Only absorb in-progress contexts; completed contexts are valid + * results + */ + if (ctx->states != NULL) + nfa_try_absorb_context(winstate, ctx); + } +} + +/* + * nfa_eval_var_match + * + * Evaluate if a VAR element matches the current row. + * + * varMatched is a per-row tri-state cache indexed by varId. Evaluation is + * lazy: the variable's DEFINE predicate is evaluated here the first time the + * NFA consumes the variable (cache is RPR_VAR_UNEVALUATED), then cached, so a + * variable that no active state tests at this row is never evaluated. This + * matches ISO/IEC 19075-5, where a Boolean condition is evaluated only with + * the current row tentatively mapped to that variable. A NULL varMatched + * makes every VAR not match; nfa_match() is called that way to force a + * mismatch at a frame boundary and at partition-end finalization. + * + * The caller must have set up the current row (ecxt_outertuple, currentpos, + * nav_match_start, nav_slot cache) via rpr_prepare_row() / + * nfa_reevaluate_dependent_vars() before consumption. + * + * Per ISO/IEC 19075-5 Feature R020, pattern variables not listed in DEFINE + * are implicitly TRUE -- they match every row. This is checked via + * varId >= list_length. + */ +static bool +nfa_eval_var_match(WindowAggState *winstate, RPRPatternElement *elem, + RPRVarMatch *varMatched) +{ + int varId; + + /* This function should only be called for VAR elements */ + Assert(RPRElemIsVar(elem)); + + if (varMatched == NULL) + return false; + + varId = elem->varId; + if (varId >= list_length(winstate->defineClauseExprs)) + return true; + + /* Lazily evaluate this variable's DEFINE predicate on first consumption. */ + if (varMatched[varId] == RPR_VAR_UNEVALUATED) + { + ExprState *exprState = list_nth(winstate->defineClauseExprs, varId); + Datum result; + bool isnull; + + result = ExecEvalExpr(exprState, winstate->rprContext, &isnull); + varMatched[varId] = (!isnull && DatumGetBool(result)) ? + RPR_VAR_TRUE : RPR_VAR_FALSE; + } + + return (varMatched[varId] == RPR_VAR_TRUE); +} + +/* + * nfa_match + * + * Match phase (convergence): evaluate VAR elements against current row. + * Only updates counts and removes dead states. Minimal transitions. + * + * For VAR elements: + * - matched: count++ (saturating at RPR_COUNT_INF), keep state + * - not matched: remove state (exit alternatives already exist from + * previous advance when count >= min was satisfied) + * + * For VARs that reached max count followed by END: + * - Advance through the END-element chain to the absorption + * comparison point + * - Only deterministic exits are handled. A count saturates at + * RPR_COUNT_INF, so count >= max does not by itself exclude an unbounded + * VAR; what excludes it is the absorbable-region test, which no unbounded + * VAR inside a still-looping group passes. + * - Chains through END elements while count >= max (must-exit path) + * + * Non-VAR elements (only an END parked by the chain above) are kept as-is for + * advance phase. + * + * currentPos is unused by the matching logic itself; it is accepted so that + * every NFA helper carries the row index for debugging. + */ +static void +nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, RPRVarMatch *varMatched, + int64 currentPos) +{ + RPRPattern *pattern = winstate->rpPattern; + RPRPatternElement *elements = pattern->elements; + RPRNFAState **prevPtr = &ctx->states; + RPRNFAState *state; + RPRNFAState *nextState; + + /* Evaluate VAR elements against current row. */ + for (state = ctx->states; state != NULL; state = nextState) + { + RPRPatternElement *elem = &elements[state->elemIdx]; + + CHECK_FOR_INTERRUPTS(); + + nextState = state->next; + + if (RPRElemIsVar(elem)) + { + bool matched; + int depth = elem->depth; + int32 count = state->counts[depth]; + + matched = nfa_eval_var_match(winstate, elem, varMatched); + + if (matched) + { + /* + * Increment count, saturating at RPR_COUNT_INF to avoid int32 + * overflow; a saturated count then compares as "unbounded". + */ + if (count < RPR_COUNT_INF) + count++; + + /* Max constraint should not be exceeded */ + Assert(elem->max == RPR_QUANTITY_INF || count <= elem->max); + + state->counts[depth] = count; + + /* + * For VAR at max count with END next, advance through END + * chain to reach the absorption comparison point. Only + * deterministic exits (count >= max, max finite) are handled; + * unbounded VARs stay for advance phase. + * + * In nested patterns like ((A (B C){2}){2})+, a VAR reaching + * its max triggers an exit cascade: inner END increments + * inner group count, which may itself reach max, requiring an + * exit to the next outer END. The loop below walks this + * chain. + * + * ABSORBABLE_BRANCH marks elements inside the absorbable + * region; ABSORBABLE marks the outermost comparison point + * where count-dominance is evaluated. We chain through + * BRANCH elements until reaching the ABSORBABLE point or an + * element that can still loop (count < max). + */ + if (RPRElemIsAbsorbableBranch(elem) && + !RPRElemIsAbsorbable(elem) && + count >= elem->max && + RPRElemIsEnd(&elements[elem->next])) + { + RPRPatternElement *endElem = &elements[elem->next]; + int endDepth = endElem->depth; + int32 endCount = state->counts[endDepth]; + + /* Increment group count */ + if (endCount < RPR_COUNT_INF) + endCount++; + Assert(endElem->max == RPR_QUANTITY_INF || + endCount <= endElem->max); + + state->elemIdx = elem->next; + state->counts[endDepth] = endCount; + + /* + * Leaf VAR exited (reached max): clear its own count so + * the next occupant enters with zero, as nfa_advance_var + * does on exit (this inline path replaces that exit). + * depth > endDepth, so this leaves the group count just + * written intact. + */ + Assert(endDepth < depth); + state->counts[depth] = 0; + + /* + * Chain through END elements within the absorbable region + * (ABSORBABLE_BRANCH) until reaching the comparison point + * (ABSORBABLE). Continue only on must-exit path (count + * >= max) with END next. + */ + while (RPRElemIsAbsorbableBranch(endElem) && + !RPRElemIsAbsorbable(endElem) && + endCount >= endElem->max && + RPRElemIsEnd(&elements[endElem->next])) + { + RPRPatternElement *outerEnd = &elements[endElem->next]; + int outerDepth = outerEnd->depth; + int32 outerCount = state->counts[outerDepth]; + + /* + * Exit this intermediate group: clear its own count + * (count-clear policy). It sits below the absorbable + * comparison point, so it is excluded from the + * dominance comparison; the comparison point where + * the chain stops keeps its count. + */ + state->counts[endDepth] = 0; + + /* Increment outer group count */ + if (outerCount < RPR_COUNT_INF) + outerCount++; + Assert(outerEnd->max == RPR_QUANTITY_INF || + outerCount <= outerEnd->max); + + state->elemIdx = endElem->next; + state->counts[outerDepth] = outerCount; + + /* Advance to next END in chain */ + endElem = outerEnd; + endDepth = outerDepth; + endCount = outerCount; + } + } + /* else: stay at VAR for advance phase */ + } + else + { + /* + * Not matched - remove state. Exit alternatives were already + * created by advance phase when count >= min was satisfied. + */ + *prevPtr = nextState; + nfa_state_free(winstate, state); + continue; + } + } + /* Non-VAR elements: keep as-is for advance phase */ + + prevPtr = &state->next; + } +} + +/* + * nfa_route_to_elem + * + * Route state to next element. If VAR, add to ctx->states and process + * skip path if optional. Otherwise, continue epsilon expansion via recursion. + */ +static void +nfa_route_to_elem(WindowAggState *winstate, RPRNFAContext *ctx, + RPRNFAState *state, RPRPatternElement *nextElem, + int64 currentPos) +{ + if (RPRElemIsVar(nextElem)) + { + RPRNFAState *skipState = NULL; + + /* + * Entry-side check of the count-clear policy: a VAR is always routed + * to with a clean slot. Each element zeroes its own count on exit, + * so a nonzero count here would be a leak from an earlier element + * (see nfa_advance_var / nfa_advance_end exit handling and the inline + * fast path in nfa_match). + */ + Assert(state->counts[nextElem->depth] == 0); + + /* Create skip state before add_unique, which may free state */ + if (RPRElemCanSkip(nextElem)) + { + RPRPatternElement *landElem; + + skipState = nfa_state_clone(winstate, nextElem->next, + state->counts, state->isAbsorbable); + + /* + * When the skip lands directly on an outer END, increment its + * iteration count, just as the exit path in nfa_advance_var does: + * a skipped iteration still ran, and that count is what the + * group's min check and the cycle guard's below-min fall-through + * both read. + */ + landElem = &winstate->rpPattern->elements[skipState->elemIdx]; + if (RPRElemIsEnd(landElem) && + skipState->counts[landElem->depth] < RPR_COUNT_INF) + skipState->counts[landElem->depth]++; + } + + if (skipState != NULL && RPRElemIsReluctant(nextElem)) + { + /* + * Reluctant optional VAR: prefer skipping. Explore the skip path + * first so it outranks the enter (match) path; if it reaches FIN + * the shortest match is found and the enter state is dropped. + * This mirrors the reluctant branch of nfa_advance_begin used by + * the leading-position and optional-group paths. + */ + nfa_advance_state(winstate, ctx, skipState, currentPos); + + if (ctx->matchUpdated) + { + nfa_state_free(winstate, state); + return; + } + + nfa_add_state_unique(winstate, ctx, state); + } + else + { + /* Greedy (or non-skippable): enter first, then skip */ + nfa_add_state_unique(winstate, ctx, state); + + if (skipState != NULL) + nfa_advance_state(winstate, ctx, skipState, currentPos); + } + } + else + { + nfa_advance_state(winstate, ctx, state, currentPos); + } +} + +/* + * nfa_advance_alt + * + * Handle ALT element: expand all branches in lexical order via DFS. + * + * ALT.next is the first branch's content and ALT.jump is that branch's + * terminating SEP. Each SEP.jump links to the next branch's SEP (-1 on the + * last) and each SEP.next is the next branch's content. The walk reads SEP + * but never enters one -- states are always created at a branch's content. + */ +static void +nfa_advance_alt(WindowAggState *winstate, RPRNFAContext *ctx, + RPRNFAState *state, RPRPatternElement *elem, + int64 currentPos) +{ + RPRPattern *pattern = winstate->rpPattern; + RPRPatternElement *elements = pattern->elements; + RPRElemIdx branchStart = elem->next; + RPRElemIdx sepIdx = elem->jump; + + while (sepIdx != RPR_ELEMIDX_INVALID) + { + RPRPatternElement *sepElem; + RPRNFAState *newState; + + /* Create independent state at this branch's content */ + newState = nfa_state_clone(winstate, branchStart, + state->counts, state->isAbsorbable); + + /* Recursively process this branch before the next */ + nfa_advance_state(winstate, ctx, newState, currentPos); + + /* + * Branches are enumerated in preference order, so once one of them + * has recorded a match the later branches must not be explored: a + * later branch would either reach FIN in this same DFS and replace + * the preferred match, or park states that complete on a later row + * and replace it then. Same technique as the reluctant paths in + * nfa_route_to_elem and nfa_advance_begin. + * + * PATTERN (A* | B) on a row where A is false and B is true: the A* + * branch is explored first, its skip path runs straight to FIN, and + * the empty match is recorded here. Breaking leaves that match + * standing, which is what the preference order asks for. Without the + * break, B would be expanded too, would match the row, and its + * one-row match would replace the empty one, as if the pattern were + * written (B | A*). + */ + if (ctx->matchUpdated) + break; + + Assert(sepIdx >= 0 && sepIdx < pattern->numElements); + sepElem = &elements[sepIdx]; + Assert(RPRElemIsSep(sepElem)); + + /* The last branch's SEP has no link, ending the walk */ + branchStart = sepElem->next; + sepIdx = sepElem->jump; + } + + nfa_state_free(winstate, state); +} + +/* + * nfa_advance_begin + * + * Handle BEGIN element: group entry logic. + * BEGIN is only visited at initial group entry; loop-back from END goes + * directly to first child, bypassing BEGIN. Per the count-clear policy the + * group's own count slot is therefore already zero on entry (asserted below). + * If min=0, creates a skip path past the group. + */ +static void +nfa_advance_begin(WindowAggState *winstate, RPRNFAContext *ctx, + RPRNFAState *state, RPRPatternElement *elem, + int64 currentPos) +{ + RPRPattern *pattern = winstate->rpPattern; + RPRPatternElement *elements = pattern->elements; + RPRNFAState *skipState = NULL; + + /* + * Entry-side check of the count-clear policy: the group's own count slot + * is already zero here. BEGIN is only visited at initial group entry, + * and the previous occupant of this depth slot cleared it on exit. + */ + Assert(state->counts[elem->depth] == 0); + + /* Optional group: create skip path (but don't route yet) */ + if (elem->min == 0) + { + RPRPatternElement *landElem; + + skipState = nfa_state_clone(winstate, elem->jump, + state->counts, state->isAbsorbable); + + /* + * As in nfa_route_to_elem, a skip that lands directly on an outer END + * still counts as an iteration of that END's group. + */ + landElem = &elements[elem->jump]; + if (RPRElemIsEnd(landElem) && + skipState->counts[landElem->depth] < RPR_COUNT_INF) + skipState->counts[landElem->depth]++; + } + + if (skipState != NULL && RPRElemIsReluctant(elem)) + { + /* Reluctant: skip first (prefer fewer iterations), enter second */ + nfa_route_to_elem(winstate, ctx, skipState, + &elements[elem->jump], currentPos); + + /* The skip matched: do not enter the group over it */ + if (ctx->matchUpdated) + { + nfa_state_free(winstate, state); + return; + } + + state->elemIdx = elem->next; + nfa_route_to_elem(winstate, ctx, state, + &elements[state->elemIdx], currentPos); + } + else + { + /* + * Greedy-or-non-nullable: route to the first child. For optional + * groups (skipState != NULL, greedy min=0) additionally create the + * skip path; for non-nullable groups (skipState == NULL, min>0) the + * skip-path action is suppressed by the guard below. + */ + state->elemIdx = elem->next; + nfa_route_to_elem(winstate, ctx, state, + &elements[state->elemIdx], currentPos); + + /* Entering matched: do not take the skip over it */ + if (ctx->matchUpdated) + { + if (skipState != NULL) + nfa_state_free(winstate, skipState); + return; + } + + if (skipState != NULL) + { + nfa_route_to_elem(winstate, ctx, skipState, + &elements[elem->jump], currentPos); + } + } +} + +/* + * nfa_advance_end + * + * Handle END element: group repetition logic. + * Decides whether to loop back or exit based on count vs min/max. + */ +static void +nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx, + RPRNFAState *state, RPRPatternElement *elem, + int64 currentPos) +{ + RPRPattern *pattern = winstate->rpPattern; + RPRPatternElement *elements = pattern->elements; + int depth = elem->depth; + int32 count = state->counts[depth]; + + if (count < elem->min) + { + RPRPatternElement *jumpElem; + RPRNFAState *ffState = NULL; + RPRPatternElement *nextElem = NULL; + + /*---------- + * Two paths are explored when the group body is nullable + * (RPR_ELEM_EMPTY_LOOP): + * + * 1. Loop-back path: attempt real matches in the next iteration + * (state, modified below). + * + * 2. Fast-forward path: skip directly to after the group, treating + * all remaining required iterations as empty matches (ffState). + * Route to elem->next (not nfa_advance_end) to avoid creating + * competing greedy/reluctant loop states. + * + * The body decides the order, not the group's own greed: the + * fast-forward comes first exactly when the body prefers the empty + * match (RPR_ELEM_EMPTY_PREFERRED). If it then reaches FIN, the + * loop-back is dropped so a longer match cannot replace the preferred + * one -- mirroring the min<=countelemIdx, + state->counts, state->isAbsorbable); + + /* + * nfa_exit_to()'s isAbsorbable recompute is a no-op here: + * EMPTY_LOOP groups are never in an absorbable region. + */ + nextElem = nfa_exit_to(winstate, ffState, depth, elem->next); + } + + /* + * Prepare the loop-back state. Visited marks are deliberately left + * in place; see the cycle guard in nfa_advance_state. + */ + state->elemIdx = elem->jump; + jumpElem = &elements[state->elemIdx]; + + if (ffState != NULL && RPRElemIsEmptyPreferred(elem)) + { + /* Body prefers empty: take the fast-forward (exit) first */ + nfa_route_to_elem(winstate, ctx, ffState, nextElem, + currentPos); + + /* The fast-forward matched: do not loop back over it */ + if (ctx->matchUpdated) + { + nfa_state_free(winstate, state); + return; + } + + /* Loop-back second */ + nfa_route_to_elem(winstate, ctx, state, jumpElem, + currentPos); + } + else + { + /* Greedy (or non-nullable): loop-back first, fast-forward second */ + nfa_route_to_elem(winstate, ctx, state, jumpElem, + currentPos); + + /* The loop-back matched: do not fast-forward over it */ + if (ctx->matchUpdated) + { + if (ffState != NULL) + nfa_state_free(winstate, ffState); + return; + } + + if (ffState != NULL) + nfa_route_to_elem(winstate, ctx, ffState, nextElem, + currentPos); + } + } + else if (elem->max != RPR_QUANTITY_INF && count >= elem->max) + { + /* Must exit: reached max iterations. */ + RPRPatternElement *nextElem; + + nextElem = nfa_exit_to(winstate, state, depth, elem->next); + + nfa_route_to_elem(winstate, ctx, state, nextElem, currentPos); + } + else + { + /* + * Between min and max (with at least one iteration) - can exit or + * loop. Greedy: loop first (prefer more iterations). Reluctant: exit + * first (prefer fewer iterations). + */ + RPRNFAState *exitState; + RPRPatternElement *jumpElem; + RPRPatternElement *nextElem; + + /* + * Create exit state first (need original counts before modifying + * state) + */ + exitState = nfa_state_clone(winstate, elem->next, + state->counts, state->isAbsorbable); + nextElem = nfa_exit_to(winstate, exitState, depth, elem->next); + + /* Prepare loop state */ + state->elemIdx = elem->jump; + jumpElem = &elements[state->elemIdx]; + + if (RPRElemIsReluctant(elem)) + { + /* Exit first (preferred for reluctant) */ + nfa_route_to_elem(winstate, ctx, exitState, nextElem, + currentPos); + + /* The exit matched: do not loop over it */ + if (ctx->matchUpdated) + { + nfa_state_free(winstate, state); + return; + } + + /* Loop second */ + nfa_route_to_elem(winstate, ctx, state, jumpElem, + currentPos); + } + else + { + /* Loop first (preferred for greedy) */ + nfa_route_to_elem(winstate, ctx, state, jumpElem, + currentPos); + + /* The loop matched: do not exit over it */ + if (ctx->matchUpdated) + { + nfa_state_free(winstate, exitState); + return; + } + + /* Exit second */ + nfa_route_to_elem(winstate, ctx, exitState, nextElem, + currentPos); + } + } +} + +/* + * nfa_advance_var + * + * Handle VAR element: loop/exit transitions. + * After match phase, all VAR states have matched - decide next action. + */ +static void +nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx, + RPRNFAState *state, RPRPatternElement *elem, + int64 currentPos) +{ + RPRPattern *pattern = winstate->rpPattern; + int depth = elem->depth; + int32 count = state->counts[depth]; + bool canLoop = (elem->max == RPR_QUANTITY_INF || count < elem->max); + bool canExit = (count >= elem->min); + + /* min <= max, so !canExit (count < min) implies canLoop (count < max) */ + Assert(canLoop || canExit); + + /* elem->next must be a valid index for any reachable VAR */ + Assert(elem->next >= 0 && elem->next < pattern->numElements); + + if (canLoop && canExit) + { + /* + * Both loop and exit possible. Greedy: loop first (prefer longer + * match). Reluctant: exit first (prefer shorter match). + */ + RPRNFAState *cloneState; + RPRPatternElement *nextElem; + bool reluctant = RPRElemIsReluctant(elem); + + /* + * Clone state for the first-priority path. For greedy, clone is the + * loop state; for reluctant, clone is the exit state. + */ + if (reluctant) + { + /* Clone for exit, original stays for loop */ + cloneState = nfa_state_clone(winstate, elem->next, + state->counts, state->isAbsorbable); + nextElem = nfa_exit_to(winstate, cloneState, depth, elem->next); + + /* Exit first (preferred for reluctant) */ + nfa_route_to_elem(winstate, ctx, cloneState, nextElem, + currentPos); + + /* The exit matched: do not loop over it */ + if (ctx->matchUpdated) + { + nfa_state_free(winstate, state); + return; + } + + /* Loop second */ + nfa_add_state_unique(winstate, ctx, state); + } + else + { + /* Clone for loop, original used for exit */ + cloneState = nfa_state_clone(winstate, state->elemIdx, + state->counts, state->isAbsorbable); + + /* Loop first (preferred for greedy) */ + nfa_add_state_unique(winstate, ctx, cloneState); + + /* Exit second: nfa_match handles only deterministic exits */ + nextElem = nfa_exit_to(winstate, state, depth, elem->next); + + nfa_route_to_elem(winstate, ctx, state, nextElem, + currentPos); + } + } + else if (canLoop) + { + /* Loop only: keep state as-is */ + nfa_add_state_unique(winstate, ctx, state); + } + else + { + /* Exit only: advance to next element (canExit necessarily true) */ + RPRPatternElement *nextElem; + + Assert(canExit); + nextElem = nfa_exit_to(winstate, state, depth, elem->next); + + nfa_route_to_elem(winstate, ctx, state, nextElem, currentPos); + } +} + +/* + * nfa_advance_state + * + * Recursively process a single state through epsilon transitions. + * DFS traversal ensures states are added to ctx->states in lexical order. + */ +static void +nfa_advance_state(WindowAggState *winstate, RPRNFAContext *ctx, + RPRNFAState *state, int64 currentPos) +{ + RPRPattern *pattern = winstate->rpPattern; + RPRPatternElement *elem; + + Assert(state->elemIdx >= 0 && state->elemIdx < pattern->numElements); + + /* Protect against stack overflow for deeply complex patterns */ + check_stack_depth(); + + /* + * Cycle detection. Only a nullable END is marked, so a set bit means the + * body just derived an empty match for this iteration: a DFS takes only + * epsilon transitions, so no row was consumed since the last visit. + * + * Nothing else needs guarding: a revisit is a cycle only when it carries + * no progress, and any other loop-back has consumed a row. Dropping one + * loses the match outright -- ((A | B B){1,3}){3} then finds nothing. + */ + if (winstate->nfaVisitedEnds[WORDNUM(state->elemIdx)] & + ((bitmapword) 1 << BITNUM(state->elemIdx))) + { + RPRPatternElement *hitElem = &pattern->elements[state->elemIdx]; + + Assert(RPRElemIsEnd(hitElem) && RPRElemCanEmptyLoop(hitElem)); + + if (state->counts[hitElem->depth] >= hitElem->min) + { + RPRPatternElement *nextElem; + + /* An END always has a valid exit target after finalization. */ + Assert(hitElem->next != RPR_ELEMIDX_INVALID); + + /* + * Leave the group here, and enumerate that exit at this rank + * rather than discarding the state: discarding demotes "leave the + * group" below the remaining alternatives, and a less-preferred + * branch then consumes rows the match should not have. At or + * above the lower bound an empty iteration stops the quantifier + * (SQL/RPR follows Perl here). + */ + + nextElem = nfa_exit_to(winstate, state, hitElem->depth, + hitElem->next); + + nfa_route_to_elem(winstate, ctx, state, nextElem, currentPos); + return; + } + + /* + * Below the lower bound the quantifier cannot exit, so fall through + * to the normal must-loop path. Each empty iteration's arrival + * increment advances the count, so this reaches min and exits above. + * Clearing the marks here instead would also disarm the guard for + * nested reluctant loops, whose empty iterations then recurse without + * bound: (A (B*?)+?){2,} on a single matching row. + */ + } + + elem = &pattern->elements[state->elemIdx]; + + /* + * Only a nullable END is ever tested; see the guard above. + * + * XXX this bounds the cycle, not the cost. Leaving ALT and BEGIN + * unmarked lets them be re-entered any number of times within one + * expansion, so a run of alternations whose branches are all nullable + * enumerates paths rather than states: nfa_advance_alt() recurses once + * per branch and fillRPRPatternAlt() converges every branch tail on the + * same element, making k such alternations cost 2^k. (A?|B?){30} takes + * over half an hour, and the recursion depth stays at k, so + * check_stack_depth() never fires. Bounding the cost needs a revisit key + * of (elemIdx, counts); this bitmap identifies elemIdx alone. + */ + if (RPRElemCanEmptyLoop(elem)) + nfa_mark_visited(winstate, state->elemIdx); + + switch (elem->varId) + { + case RPR_VARID_FIN: + /* FIN: record match */ + nfa_add_matched_state(winstate, ctx, state, currentPos); + break; + + case RPR_VARID_ALT: + nfa_advance_alt(winstate, ctx, state, elem, currentPos); + break; + + case RPR_VARID_BEGIN: + nfa_advance_begin(winstate, ctx, state, elem, currentPos); + break; + + case RPR_VARID_END: + nfa_advance_end(winstate, ctx, state, elem, currentPos); + break; + + default: + /* VAR element; a SEP would land here, so see fillRPRPatternAlt */ + Assert(!RPRElemIsSep(elem) && RPRElemIsVar(elem)); + nfa_advance_var(winstate, ctx, state, elem, currentPos); + break; + } +} + +/* + * nfa_advance + * + * Advance phase (divergence): transition from all surviving states. + * Called after match phase with matched VAR states, or at context creation + * for initial epsilon expansion (with currentPos = startPos - 1). + * + * Processes states in order, using recursive DFS to maintain lexical order. + */ +static void +nfa_advance(WindowAggState *winstate, RPRNFAContext *ctx, int64 currentPos) +{ + RPRNFAState *states = ctx->states; + RPRNFAState *state; + + ctx->states = NULL; /* Will rebuild */ + ctx->matchUpdated = false; + + /* Process each state in lexical order (DFS order from previous advance) */ + while (states != NULL) + { + CHECK_FOR_INTERRUPTS(); + + /* + * Clear visited bitmap before each state's DFS expansion. Only the + * range touched since the previous reset (tracked via the high-water + * marks updated in nfa_mark_visited) needs to be cleared; for small + * NFAs this is the whole array, but for large NFAs whose DFS only + * reaches a few elements per advance it avoids walking the full + * bitmap. + */ + if (winstate->nfaVisitedMaxWord >= winstate->nfaVisitedMinWord) + { + memset(&winstate->nfaVisitedEnds[winstate->nfaVisitedMinWord], 0, + sizeof(bitmapword) * + (winstate->nfaVisitedMaxWord - + winstate->nfaVisitedMinWord + 1)); + winstate->nfaVisitedMinWord = PG_INT16_MAX; + winstate->nfaVisitedMaxWord = -1; + } + + state = states; + states = states->next; + + /* + * Boundary contract: state->next is reset to NULL here, before + * crossing into nfa_advance_state's epsilon-expansion DFS. The inner + * branches (nfa_advance_var, nfa_advance_begin/end/alt) treat + * state->next as already-NULL and don't reset it themselves; the + * other linking site is nfa_add_state_unique, which sets it when + * appending to ctx->states. + */ + state->next = NULL; + + nfa_advance_state(winstate, ctx, state, currentPos); + + /* + * Early termination: if a FIN was newly reached in this advance, + * remaining old states have worse lexical order and can be pruned. + * Only check for new FIN arrivals (not ones from previous rows). + */ + if (ctx->matchUpdated && states != NULL) + { + nfa_state_free_list(winstate, states); + break; + } + } +} + +/* + * nfa_reevaluate_dependent_vars + * Invalidate match_start-dependent DEFINE variables for a context whose + * matchStartRow differs from the shared evaluation's nav_match_start. + * + * Only variables in defineMatchStartDependent are affected: they are reset to + * RPR_VAR_UNEVALUATED so nfa_match() re-evaluates them lazily against this + * context's matchStartRow. match_start-independent variables keep their + * cached value across contexts, since they do not read nav_match_start. + * + * nav_match_start is installed for this context and left in place: FIRST/LAST + * read it at evaluation time, which happens later during nfa_match(), so it + * must NOT be restored here. The next context's invalidation, or the next + * row's shared setup in advance_reduced_frame_nfa, overwrites it. + */ +static void +nfa_reevaluate_dependent_vars(WindowAggState *winstate, RPRNFAContext *ctx, + int64 currentPos) +{ + int varIdx = -1; + + /* Caller keeps winstate->currentpos at the scan position for lazy eval. */ + Assert(winstate->currentpos == currentPos); + + /* + * Release the previous context's DEFINE evaluation memory. Match-start- + * dependent variables are re-evaluated once per context (they are reset + * to UNEVALUATED below), so without this reset their per-tuple scratch + * would accumulate across every context of a row -- bounded only by the + * per-row reset in rpr_prepare_row. rprContext is the dedicated DEFINE + * context, so this frees neither the input nor the output tuple memory. + */ + ResetExprContext(winstate->rprContext); + + /* Install this context's match_start for FIRST/LAST and keep it in place. */ + winstate->nav_match_start = ctx->matchStartRow; + + /* Invalidate nav_slot cache since match_start changed */ + winstate->nav_slot_pos = -1; + + /* Reset only the dependent variables so they re-evaluate lazily. */ + while ((varIdx = bms_next_member(winstate->defineMatchStartDependent, + varIdx)) >= 0) + winstate->nfaVarMatched[varIdx] = RPR_VAR_UNEVALUATED; +} + + +/*********************************************************************** + * API exposed to nodeWindowAgg.c + ***********************************************************************/ + +/* + * ExecRPRStartContext + * + * Start a new match context at given position. + * Initializes context, state absorption flags, and performs initial advance + * to expand epsilon transitions (ALT branches, optional elements). + * Adds context to the tail of winstate->nfaContext list. + */ +RPRNFAContext * +ExecRPRStartContext(WindowAggState *winstate, int64 startPos) +{ + RPRNFAContext *ctx; + RPRPattern *pattern = winstate->rpPattern; + RPRPatternElement *elem; + + ctx = nfa_context_make(winstate); + ctx->matchStartRow = startPos; + ctx->states = nfa_state_make(winstate); /* initial state at elem 0 */ + + elem = &pattern->elements[0]; + + if (RPRElemIsAbsorbableBranch(elem)) + { + ctx->states->isAbsorbable = true; + } + else + { + ctx->hasAbsorbableState = false; + ctx->allStatesAbsorbable = false; + ctx->states->isAbsorbable = false; + } + + /* + * Add to tail of active context list (doubly-linked, oldest-first). + * matchStartRow increases along the list, so the head holds the smallest + * -- an ordering other code relies on. At most one context starts at a + * row: the on-demand path in update_reduced_frame creates one only where + * none exists. + */ + Assert(winstate->nfaContextTail == NULL || + startPos > winstate->nfaContextTail->matchStartRow); + ctx->prev = winstate->nfaContextTail; + ctx->next = NULL; + if (winstate->nfaContextTail != NULL) + winstate->nfaContextTail->next = ctx; + else + winstate->nfaContext = ctx; /* first context becomes head */ + winstate->nfaContextTail = ctx; + + /* + * Initial advance (divergence): expand ALT branches and create exit + * states for VAR elements with min=0. This prepares the context for the + * first row's match phase. + * + * Use startPos - 1 as currentPos since no row has been consumed yet. If + * FIN is reached via epsilon transitions, matchEndRow = startPos - 1, + * which is how an empty match is represented. + */ + nfa_advance(winstate, ctx, startPos - 1); + + return ctx; +} + +/* + * ExecRPRGetHeadContext + * + * Return the head context if its start position matches pos. + * Returns NULL if no context exists or head doesn't match pos. + */ +RPRNFAContext * +ExecRPRGetHeadContext(WindowAggState *winstate, int64 pos) +{ + RPRNFAContext *ctx = winstate->nfaContext; + + /* + * Contexts are sorted by matchStartRow ascending. If the head context + * doesn't match pos, no context exists for this position. + */ + if (ctx == NULL || ctx->matchStartRow != pos) + return NULL; + + return ctx; +} + +/* + * ExecRPRFreeContext + * + * Unlink context from active list and return it to free list. + * Also frees any states in the context. + */ +void +ExecRPRFreeContext(WindowAggState *winstate, RPRNFAContext *ctx) +{ + /* Unlink from active list first */ + nfa_unlink_context(winstate, ctx); + + /* Update statistics */ + winstate->nfaContextsActive--; + + if (ctx->states != NULL) + nfa_state_free_list(winstate, ctx->states); + if (ctx->matchedState != NULL) + nfa_state_free(winstate, ctx->matchedState); + + ctx->states = NULL; + ctx->matchedState = NULL; + ctx->next = winstate->nfaContextFree; + winstate->nfaContextFree = ctx; +} + +/* + * ExecRPRRecordContextSuccess + * + * Record a successful context in statistics. + */ +void +ExecRPRRecordContextSuccess(WindowAggState *winstate, int64 matchLen) +{ + winstate->nfaMatchesSucceeded++; + nfa_update_length_stats(winstate->nfaMatchesSucceeded, + &winstate->nfaMatchLen, + matchLen); +} + +/* + * ExecRPRRecordContextFailure + * + * Record a failed context in statistics. + * If failedLen == 1, count as pruned (failed on first row). + * If failedLen > 1, count as mismatched and update length stats. + */ +void +ExecRPRRecordContextFailure(WindowAggState *winstate, int64 failedLen) +{ + if (failedLen == 1) + { + winstate->nfaContextsPruned++; + } + else + { + winstate->nfaMatchesFailed++; + nfa_update_length_stats(winstate->nfaMatchesFailed, + &winstate->nfaFailLen, + failedLen); + } +} + +/* + * ExecRPRProcessRow + * + * Process all contexts for one row: + * 1. Match all contexts (convergence) - evaluate VARs, prune dead states + * 2. Absorb redundant contexts - ideal timing after convergence + * 3. Advance all contexts (divergence) - create new states for next row + */ +void +ExecRPRProcessRow(WindowAggState *winstate, int64 currentPos, + bool hasLimitedFrame, int64 frameOffset) +{ + RPRNFAContext *ctx; + RPRVarMatch *varMatched = winstate->nfaVarMatched; + bool hasDependent = !bms_is_empty(winstate->defineMatchStartDependent); + + /* Allow query cancellation once per row for simple/low-state patterns */ + CHECK_FOR_INTERRUPTS(); + + /* + * Phase 1: Match all contexts (convergence). Evaluate VAR elements, + * update counts, remove dead states. + */ + for (ctx = winstate->nfaContext; ctx != NULL; ctx = ctx->next) + { + if (ctx->states == NULL) + continue; + + /* Check frame boundary - finalize the context when it is reached */ + if (hasLimitedFrame) + { + int64 ctxFrameEnd; + + /* + * Clamp to PG_INT64_MAX on overflow. frameOffset can be as large + * as PG_INT64_MAX (e.g. "ROWS FOLLOWING"), so add the + * offset and the trailing +1 in two separately checked steps to + * avoid signed-integer overflow in the "frameOffset + 1" + * subexpression. + */ + if (pg_add_s64_overflow(ctx->matchStartRow, frameOffset, + &ctxFrameEnd) || + pg_add_s64_overflow(ctxFrameEnd, 1, &ctxFrameEnd)) + ctxFrameEnd = PG_INT64_MAX; + + /* + * currentPos advances by exactly one per call, and a finalized + * context is skipped by the states == NULL guard above, so it can + * only ever reach ctxFrameEnd, never overshoot it. The Assert + * turns a future change that broke that invariant into an + * immediate failure rather than a silent slip past the boundary. + */ + Assert(currentPos <= ctxFrameEnd); + + if (currentPos == ctxFrameEnd) + { + /* Frame boundary reached: force mismatch */ + nfa_match(winstate, ctx, NULL, currentPos); + continue; + } + } + + /* + * If this context has a different matchStartRow than the one used in + * the shared evaluation, invalidate its match_start-dependent + * variables so nfa_match() re-evaluates them lazily with this + * context's matchStartRow. + * + * The head context carries no explicit invalidation: it relies on the + * ambient nav_match_start installed by advance_reduced_frame_nfa, so + * it must be reached before any other context overwrites + * nav_match_start. + */ + Assert(ctx != winstate->nfaContext || + ctx->matchStartRow == winstate->nav_match_start); + + if (hasDependent && ctx->matchStartRow != winstate->nav_match_start) + nfa_reevaluate_dependent_vars(winstate, ctx, currentPos); + nfa_match(winstate, ctx, varMatched, currentPos); + ctx->lastProcessedRow = currentPos; + } + + /* + * Phase 2: Absorb redundant contexts. After match phase, states have + * converged - ideal for absorption. First update absorption flags that + * may have changed due to state removal. + */ + if (winstate->rpPattern->isAbsorbable) + { + for (ctx = winstate->nfaContext; ctx != NULL; ctx = ctx->next) + nfa_update_absorption_flags(ctx); + + nfa_absorb_contexts(winstate); + } + + /* + * Phase 3: Advance all contexts (divergence). Create new states + * (loop/exit) from surviving matched states. + */ + for (ctx = winstate->nfaContext; ctx != NULL; ctx = ctx->next) + { + if (ctx->states == NULL) + continue; + + /* + * Phase 1 already handled frame boundary exceeded contexts by forcing + * mismatch (nfa_match with NULL), which removes all states (all + * states are at VAR positions after advance). So any surviving + * context here must be within its frame boundary. + * + * Compute the (clamped) frame end the same way as Phase 1, using two + * separately checked adds so that "frameOffset + 1" cannot overflow + * when frameOffset is near PG_INT64_MAX. + */ +#ifdef USE_ASSERT_CHECKING + if (hasLimitedFrame) + { + int64 ctxFrameEnd; + + if (pg_add_s64_overflow(ctx->matchStartRow, frameOffset, + &ctxFrameEnd) || + pg_add_s64_overflow(ctxFrameEnd, 1, &ctxFrameEnd)) + ctxFrameEnd = PG_INT64_MAX; + Assert(currentPos < ctxFrameEnd); + } +#endif + + nfa_advance(winstate, ctx, currentPos); + } +} + +/* + * ExecRPRCleanupDeadContexts + * + * Remove contexts that have failed (no active states and no match). + * These are contexts that failed during normal processing and should be + * counted as pruned (if length 1) or mismatched (if length > 1). + */ +void +ExecRPRCleanupDeadContexts(WindowAggState *winstate, RPRNFAContext *excludeCtx) +{ + RPRNFAContext *ctx; + RPRNFAContext *next; + + for (ctx = winstate->nfaContext; ctx != NULL; ctx = next) + { + CHECK_FOR_INTERRUPTS(); + + next = ctx->next; + + /* Skip the target context and contexts still processing */ + if (ctx == excludeCtx || ctx->states != NULL) + continue; + + /* + * Skip contexts that recorded a match (handled by SKIP logic). Test + * matchedState, not matchEndRow: an empty match ends at matchStartRow + * - 1, so a row-length test would take it for a failure and count it + * as pruned or mismatched. + */ + if (ctx->matchedState != NULL) + continue; + + /* + * Failed context: always removed below. Only record the failure + * statistic if it actually processed its start row; contexts created + * for beyond-partition rows are removed without being counted. + */ + if (ctx->lastProcessedRow >= ctx->matchStartRow) + { + int64 failedLen = ctx->lastProcessedRow - ctx->matchStartRow + 1; + + ExecRPRRecordContextFailure(winstate, failedLen); + } + + ExecRPRFreeContext(winstate, ctx); + } +} + +/* + * ExecRPRFinalizeAllContexts + * + * Partition-end classification policy: kill any VAR states still pursuing + * when rows run out, so cleanup sees a uniform ctx->states == NULL across + * every context. By the time this runs, all genuine FIN reaches have + * already been recorded in-flight; three shapes survive here: + * + * - Pure pursuit (matchedState == NULL): VAR states waiting for input + * that never arrives (e.g., A+ B mid-pattern at partition end). + * - Empty-match candidate + pursuit (matchedState != NULL, + * matchEndRow < matchStartRow): initial-advance FIN-via-skip recorded + * an empty match while VAR states are still chasing a longer one + * (e.g., greedy A*). + * - Real match + pursuit (matchedState != NULL, + * matchEndRow >= matchStartRow): a match has been recorded and VAR + * states are still looping for a longer one. + * + * Killing the VAR reclassifies pure pursuit as a failure in cleanup + * (otherwise it lingers without contributing to stats). The other two both + * carry a recorded match, so cleanup skips them: an empty match is a + * length-0 success, not a failure, and update_reduced_frame registers it as + * such through its head-context path. They still go through the same + * uniform path so partition-end classification stays centralized. + * + * Implementation: nfa_match with NULL forces VAR mismatch; nfa_advance + * then drains any remaining epsilon transitions. + */ +void +ExecRPRFinalizeAllContexts(WindowAggState *winstate, int64 lastPos) +{ + RPRNFAContext *ctx; + + for (ctx = winstate->nfaContext; ctx != NULL; ctx = ctx->next) + { + CHECK_FOR_INTERRUPTS(); + + if (ctx->states != NULL) + { + nfa_match(winstate, ctx, NULL, lastPos); + + /* Defensive: advance leaves only VAR states, all removed above. */ + nfa_advance(winstate, ctx, lastPos); + } + } +} diff --git a/src/backend/executor/meson.build b/src/backend/executor/meson.build index dc45be0b2ce..0ff4a5b1d83 100644 --- a/src/backend/executor/meson.build +++ b/src/backend/executor/meson.build @@ -13,6 +13,7 @@ backend_sources += files( 'execParallel.c', 'execPartition.c', 'execProcnode.c', + 'execRPR.c', 'execReplication.c', 'execSRF.c', 'execScan.c', diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c index b86dcbba055..9ab73c614dc 100644 --- a/src/backend/executor/nodeWindowAgg.c +++ b/src/backend/executor/nodeWindowAgg.c @@ -39,10 +39,12 @@ #include "catalog/pg_proc.h" #include "common/int.h" #include "executor/executor.h" +#include "executor/execRPR.h" #include "executor/instrument.h" #include "executor/nodeWindowAgg.h" #include "miscadmin.h" #include "nodes/nodeFuncs.h" +#include "nodes/plannodes.h" #include "optimizer/clauses.h" #include "optimizer/optimizer.h" #include "parser/parse_agg.h" @@ -173,6 +175,21 @@ typedef struct WindowStatePerAggData bool restart; /* need to restart this agg in this cycle? */ } WindowStatePerAggData; +typedef struct +{ + WindowAggState *winstate; + int64 maxOffset; /* max backward-reach offset across all nav + * exprs */ + bool maxOverflow; /* true if backward-reach overflow detected */ + int64 minFirstOffset; /* min forward-from-match_start offset; may be + * negative (PREV_FIRST: inner - outer < 0) */ + bool hasMax; /* any backward-reach nav found */ + bool hasFirst; /* any FIRST-based nav found */ + bool validate; /* fail-closed on a null/negative offset? + * false at init (display only), true at + * execution */ +} EvalDefineOffsetsContext; + static void initialize_windowaggregate(WindowAggState *winstate, WindowStatePerFunc perfuncstate, WindowStatePerAgg peraggstate); @@ -209,6 +226,9 @@ static Datum GetAggInitVal(Datum textInitVal, Oid transtype); static bool are_peers(WindowAggState *winstate, TupleTableSlot *slot1, TupleTableSlot *slot2); +static int WinGetSlotInFrame(WindowObject winobj, TupleTableSlot *slot, + int relpos, int seektype, bool set_mark, + bool *isnull, bool *isout); static bool window_gettupleslot(WindowObject winobj, int64 pos, TupleTableSlot *slot); @@ -227,6 +247,25 @@ static uint8 get_notnull_info(WindowObject winobj, int64 pos, int argno); static void put_notnull_info(WindowObject winobj, int64 pos, int argno, bool isnull); +static bool rpr_is_defined(WindowAggState *winstate); +static int64 row_is_in_reduced_frame(WindowObject winobj, int64 pos); +static void ensure_reduced_frame(WindowObject winobj, int64 pos); + +static void clear_reduced_frame(WindowAggState *winstate); +static int get_reduced_frame_status(WindowAggState *winstate, int64 pos); +static void advance_nav_mark(WindowAggState *winstate, int64 currentPos); +static void advance_reduced_frame_nfa(WindowObject winobj, + RPRNFAContext *targetCtx, int64 pos, + bool hasLimitedFrame, int64 frameOffset); +static void update_reduced_frame(WindowObject winobj, int64 pos); + +/* Forward declarations - DEFINE row evaluation */ +static bool rpr_prepare_row(WindowObject winobj, int64 pos, RPRVarMatch *varMatched); +static void build_define_offsets(WindowAggState *winstate, List *defineClause); +static void resolve_nav_offsets(WindowAggState *winstate); +static void resolve_one_nav(RPRNavOffsets *entry, EvalDefineOffsetsContext *context); +static bool RPRNavExpr_walker(Node *node, WindowAggState *winstate); +static void build_nav_offsets(RPRNavExpr *nav, WindowAggState *winstate); /* * Not null info bit array consists of 2-bit items @@ -820,6 +859,9 @@ eval_windowaggregates(WindowAggState *winstate) * transition function, or * - we have an EXCLUSION clause, or * - if the new frame doesn't overlap the old one + * - if RPR (Row Pattern Recognition) is enabled, because the reduced + * frame depends on pattern matching results which can differ entirely + * from row to row, making inverse transition optimization inapplicable * * Note that we don't strictly need to restart in the last case, but if * we're going to remove all rows from the aggregation anyway, a restart @@ -834,7 +876,8 @@ eval_windowaggregates(WindowAggState *winstate) (winstate->aggregatedbase != winstate->frameheadpos && !OidIsValid(peraggstate->invtransfn_oid)) || (winstate->frameOptions & FRAMEOPTION_EXCLUSION) || - winstate->aggregatedupto <= winstate->frameheadpos) + winstate->aggregatedupto <= winstate->frameheadpos || + rpr_is_defined(winstate)) { peraggstate->restart = true; numaggs_restart++; @@ -963,6 +1006,14 @@ eval_windowaggregates(WindowAggState *winstate) { winstate->aggregatedupto = winstate->frameheadpos; ExecClearTuple(agg_row_slot); + + /* + * If RPR is defined, we do not use aggregatedupto_nonrestarted. To + * avoid assertion failure below, we reset aggregatedupto_nonrestarted + * to frameheadpos. + */ + if (rpr_is_defined(winstate)) + aggregatedupto_nonrestarted = winstate->frameheadpos; } /* @@ -974,7 +1025,7 @@ eval_windowaggregates(WindowAggState *winstate) */ for (;;) { - int ret; + int64 ret; /* Fetch next row if we didn't already */ if (TupIsNull(agg_row_slot)) @@ -995,6 +1046,36 @@ eval_windowaggregates(WindowAggState *winstate) if (ret == 0) goto next_tuple; + if (rpr_is_defined(winstate)) + { + /* + * If currentpos is already decided but aggregatedupto is not yet + * determined, we've passed the last reduced frame. + */ + if (get_reduced_frame_status(winstate, winstate->currentpos) + != RF_NOT_DETERMINED && + get_reduced_frame_status(winstate, winstate->aggregatedupto) + == RF_NOT_DETERMINED) + break; + + /* + * Calculate the reduced frame for aggregatedupto. + */ + ret = row_is_in_reduced_frame(winstate->agg_winobj, + winstate->aggregatedupto); + if (ret == -1) /* unmatched row */ + break; + + /* + * Check if current row is inside a match but not the head + * (skipped), and it's the base row for aggregation. + */ + if (get_reduced_frame_status(winstate, + winstate->aggregatedupto) == RF_SKIPPED && + winstate->aggregatedupto == winstate->aggregatedbase) + break; + } + /* Set tuple context for evaluation of aggregate arguments */ winstate->tmpcontext->ecxt_outertuple = agg_row_slot; @@ -1204,6 +1285,27 @@ prepare_tuplestore(WindowAggState *winstate) } } + /* Create read/mark pointers for RPR navigation if needed */ + if (winstate->nav_winobj) + { + /* + * Allocate mark and read pointers for RPR navigation. + * + * When the trim offset is FIXED we advance the mark based on + * (currentpos - navMaxOffset) and optionally + * (nfaContext->matchStartRow + navFirstOffset), allowing + * tuplestore_trim() to free rows that are no longer reachable. + * resolve_nav_offsets() runs before the first begin_partition(), so + * the kind here is FIXED or RETAIN_ALL even for a parameterized + * offset; RETAIN_ALL disables trim. + */ + winstate->nav_winobj->markptr = + tuplestore_alloc_read_pointer(winstate->buffer, 0); + winstate->nav_winobj->readptr = + tuplestore_alloc_read_pointer(winstate->buffer, + EXEC_FLAG_BACKWARD); + } + /* * If we are in RANGE or GROUPS mode, then determining frame boundaries * requires physical access to the frame endpoint rows, except in certain @@ -1260,6 +1362,8 @@ begin_partition(WindowAggState *winstate) winstate->framehead_valid = false; winstate->frametail_valid = false; winstate->grouptail_valid = false; + if (rpr_is_defined(winstate)) + clear_reduced_frame(winstate); winstate->spooled_rows = 0; winstate->currentpos = 0; winstate->frameheadpos = 0; @@ -1313,6 +1417,13 @@ begin_partition(WindowAggState *winstate) winstate->aggregatedupto = 0; } + /* reset mark and seek positions for RPR navigation */ + if (winstate->nav_winobj) + { + winstate->nav_winobj->markpos = -1; + winstate->nav_winobj->seekpos = -1; + } + /* reset mark and seek positions for each real window function */ for (int i = 0; i < numfuncs; i++) { @@ -1481,6 +1592,21 @@ release_partition(WindowAggState *winstate) tuplestore_clear(winstate->buffer); winstate->partition_spooled = false; winstate->next_partition = true; + + /* Reset RPR match results */ + clear_reduced_frame(winstate); + + /* Reset NFA state for new partition */ + winstate->nfaContext = NULL; + winstate->nfaContextTail = NULL; + winstate->nfaContextFree = NULL; + winstate->nfaStateFree = NULL; + winstate->nfaLastProcessedRow = -1; + winstate->nfaStatesActive = 0; + winstate->nfaContextsActive = 0; + + /* Invalidate the nav slot position cache for the new partition. */ + winstate->nav_slot_pos = -1; } /* @@ -2276,6 +2402,16 @@ calculate_frame_offsets(PlanState *pstate) ereport(ERROR, (errcode(ERRCODE_INVALID_PRECEDING_OR_FOLLOWING_SIZE), errmsg("frame ending offset must not be negative"))); + + /* + * Row pattern recognition forbids a zero-length frame end; + * checked here so a non-constant offset (e.g. a bind parameter) + * is caught, not just a literal 0. + */ + if (winstate->rpPattern != NULL && offset == 0) + ereport(ERROR, + errcode(ERRCODE_WINDOWING_ERROR), + errmsg("frame ending offset must be positive with row pattern recognition")); } } winstate->all_first = false; @@ -2312,6 +2448,16 @@ ExecWindowAgg(PlanState *pstate) if (unlikely(winstate->all_first)) calculate_frame_offsets(pstate); + /* + * Resolve navigation offsets the same way, during first call (or after a + * rescan). Every RPR window holding a navigation comes through here: the + * pass at init resolved the constant offsets for EXPLAIN to display + * without validating them, so this is where a null or negative offset is + * rejected. + */ + if (unlikely(winstate->navResolvePending)) + resolve_nav_offsets(winstate); + /* We need to loop as the runCondition or qual may filter out tuples */ for (;;) { @@ -2409,6 +2555,26 @@ ExecWindowAgg(PlanState *pstate) /* don't evaluate the window functions when we're in pass-through mode */ if (winstate->status == WINDOWAGG_RUN) { + /* + * If RPR is defined and skip mode is next row, clear the current + * match so the next row triggers re-evaluation. + */ + if (rpr_is_defined(winstate)) + { + if (winstate->rpSkipTo == ST_NEXT_ROW) + clear_reduced_frame(winstate); + + /* + * Drive the row pattern match every row, so it tracks the row + * scan rather than frame access: a window function that skips + * the frame (e.g. nth_value() with a NULL offset) must not + * leave the match state behind currentpos. + */ + Assert(winstate->nav_winobj != NULL); + ensure_reduced_frame(winstate->nav_winobj, + winstate->currentpos); + } + /* * Evaluate true window functions */ @@ -2591,12 +2757,28 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags) /* * Create expression contexts. We need two, one for per-input-tuple - * processing and one for per-output-tuple processing. We cheat a little - * by using ExecAssignExprContext() to build both. + * processing and one for per-output-tuple processing, plus an optional + * third for row pattern recognition DEFINE evaluation (built just below + * when a DEFINE clause is present). We cheat a little by using + * ExecAssignExprContext() to build them all. Each call overwrites + * ps_ExprContext, so the last call must establish the output context. */ ExecAssignExprContext(estate, &winstate->ss.ps); tmpcontext = winstate->ss.ps.ps_ExprContext; winstate->tmpcontext = tmpcontext; + + /* + * Row pattern recognition evaluates DEFINE clauses in a third context, + * reset before each DEFINE evaluation pass. It must be distinct from + * tmpcontext and ps_ExprContext so its reset frees neither input nor + * output tuple memory. + */ + if (node->defineClause != NIL) + { + ExecAssignExprContext(estate, &winstate->ss.ps); + winstate->rprContext = winstate->ss.ps.ps_ExprContext; + } + ExecAssignExprContext(estate, &winstate->ss.ps); /* Create long-lived context for storage of partition-local memory etc */ @@ -2861,9 +3043,166 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags) winstate->more_partitions = false; winstate->next_partition = true; + /* + * RPR stuff, in struct declaration order except for the nav offsets, + * which build_define_offsets() below accumulates into; the four + * NFALengthStats members keep the zeroes palloc0 gave them. + */ + if (node->rpPattern != NULL) + { + int nfaVisitedNWords; + WindowObject nav_winobj; + + winstate->rpSkipTo = node->rpSkipTo; + winstate->rpPattern = node->rpPattern; + winstate->defineClauseExprs = NIL; + + winstate->rprNavOffsets = NIL; + winstate->navMaxOffset = 0; + winstate->navMaxOffsetKind = RPR_NAV_OFFSET_FIXED; + winstate->hasMaxNav = false; + winstate->hasFirstNav = false; + winstate->navFirstOffset = 0; + winstate->navFirstOffsetKind = RPR_NAV_OFFSET_FIXED; + + /* + * Must run this before the ExecInitExpr() loop over defineClause: + * while compiling each RPRNavExpr, ExecInitExpr() reads + * winstate->rprNavOffsets to link the RPRNavState to its entry and + * seed the offset, and this call is what fills that list + */ + build_define_offsets(winstate, node->defineClause); + + /* + * Compile DEFINE clause expressions. PREV/NEXT navigation is handled + * by EEOP_RPR_NAV_SET/RESTORE opcodes emitted during ExecInitExpr, so + * no varno rewriting is needed here. Expressions are kept in DEFINE + * order, so their list index equals the variable's varId. + */ + foreach_node(TargetEntry, te, node->defineClause) + { + ExprState *exprstate; + + exprstate = ExecInitExpr(te->expr, (PlanState *) winstate); + + winstate->defineClauseExprs = + lappend(winstate->defineClauseExprs, exprstate); + } + + /* Initialize NFA free lists for row pattern matching */ + winstate->nfaContext = NULL; + winstate->nfaContextTail = NULL; + winstate->nfaContextFree = NULL; + winstate->nfaStateFree = NULL; + winstate->nfaStateSize = offsetof(RPRNFAState, counts) + + sizeof(int32) * node->rpPattern->maxDepth; + + /* + * Allocate the per-row varMatched cache. varNames are built in + * DEFINE order, so varId equals the DEFINE list index and no mapping + * is needed. + */ + if (winstate->defineClauseExprs != NIL) + winstate->nfaVarMatched = palloc0(sizeof(RPRVarMatch) * + list_length(winstate->defineClauseExprs)); + else + winstate->nfaVarMatched = NULL; + + /* Copy match_start dependency bitmapset for per-context evaluation */ + winstate->defineMatchStartDependent = bms_copy(node->defineMatchStartDependent); + + nfaVisitedNWords = + (node->rpPattern->numElements - 1) / BITS_PER_BITMAPWORD + 1; + + winstate->nfaVisitedEnds = palloc0(sizeof(bitmapword) * + nfaVisitedNWords); + + /* High-water mark sentinels: no bits set yet. */ + winstate->nfaVisitedMinWord = PG_INT16_MAX; + winstate->nfaVisitedMaxWord = -1; + + winstate->nfaLastProcessedRow = -1; + winstate->nfaStatesActive = 0; + winstate->nfaStatesMax = 0; + winstate->nfaStatesTotalCreated = 0; + winstate->nfaStatesMerged = 0; + winstate->nfaContextsActive = 0; + winstate->nfaContextsMax = 0; + winstate->nfaContextsTotalCreated = 0; + winstate->nfaContextsAbsorbed = 0; + winstate->nfaContextsSkipped = 0; + winstate->nfaContextsPruned = 0; + winstate->nfaMatchesSucceeded = 0; + winstate->nfaMatchesFailed = 0; + + /* + * Nav offsets are resolved (and validated) at execution, like frame + * offsets: on the first scan and after each rescan, for every RPR + * window. + */ + winstate->navResolvePending = (winstate->rprNavOffsets != NIL); + + /* + * Set up WindowObject for RPR navigation opcodes. This is separate + * from agg_winobj because it needs its own read pointer to avoid + * interfering with aggregate processing. + */ + nav_winobj = makeNode(WindowObjectData); + nav_winobj->winstate = winstate; + nav_winobj->argstates = NIL; + nav_winobj->localmem = NULL; + nav_winobj->markptr = -1; + nav_winobj->readptr = -1; + winstate->nav_winobj = nav_winobj; + + winstate->nav_slot_pos = -1; + winstate->nav_slot = ExecInitExtraTupleSlot(estate, scanDesc, + &TTSOpsMinimalTuple); + winstate->nav_saved_outertuple = NULL; + winstate->nav_match_start = 0; + winstate->rpr_match_start = -1; + winstate->rpr_match_length = -1; + } + return winstate; } +/* + * ExecRPRNavGetSlot + * + * Fetch tuple at given position for RPR navigation opcodes. + * Returns nav_slot with the tuple loaded, or NULL if out of range. + */ +TupleTableSlot * +ExecRPRNavGetSlot(WindowAggState *winstate, int64 pos) +{ + WindowObject winobj = winstate->nav_winobj; + TupleTableSlot *slot = winstate->nav_slot; + + if (pos < 0) + return NULL; + + /* + * If nav_slot already holds this position, return it without re-fetching. + * This is critical when multiple PREV/NEXT calls in the same expression + * navigate to the same row, because re-fetching would free the slot's + * tuple memory and invalidate any pass-by-ref Datum pointers from earlier + * navigation results. + */ + if (winstate->nav_slot_pos == pos) + return slot; + + if (!window_gettupleslot(winobj, pos, slot)) + { + winstate->nav_slot_pos = -1; + return NULL; + } + + winstate->nav_slot_pos = pos; + return slot; +} + + /* ----------------- * ExecEndWindowAgg * ----------------- @@ -2911,6 +3250,8 @@ ExecReScanWindowAgg(WindowAggState *node) node->status = WINDOWAGG_RUN; node->all_first = true; + /* offsets are re-resolved and re-validated at the next scan */ + node->navResolvePending = (node->rprNavOffsets != NIL); /* release tuplestore et al */ release_partition(node); @@ -2921,6 +3262,8 @@ ExecReScanWindowAgg(WindowAggState *node) ExecClearTuple(node->agg_row_slot); ExecClearTuple(node->temp_slot_1); ExecClearTuple(node->temp_slot_2); + if (node->nav_slot) + ExecClearTuple(node->nav_slot); if (node->framehead_slot) ExecClearTuple(node->framehead_slot); if (node->frametail_slot) @@ -3418,6 +3761,7 @@ ignorenulls_getfuncarginframe(WindowObject winobj, int argno, int notnull_offset; int notnull_relpos; int forward; + int64 num_reduced_frame; Assert(WindowObjectIsValid(winobj)); winstate = winobj->winstate; @@ -3446,6 +3790,13 @@ ignorenulls_getfuncarginframe(WindowObject winobj, int argno, /* rejecting relpos > 0 is easy and simplifies code below */ if (relpos > 0) goto out_of_frame; + + /* + * RPR cares about frame head pos. Need to call + * update_frameheadpos + */ + update_frameheadpos(winstate); + update_frametailpos(winstate); abs_pos = winstate->frametailpos - 1; mark_pos = 0; /* keep compiler quiet */ @@ -3461,6 +3812,35 @@ ignorenulls_getfuncarginframe(WindowObject winobj, int argno, * Get the next nonnull value in the frame, moving forward or backward * until we find a value or reach the frame's end. */ + + /* + * Check whether current row is in reduced frame. + */ + num_reduced_frame = row_is_in_reduced_frame(winobj, winstate->frameheadpos); + if (num_reduced_frame < 0) /* unmatched or skipped row */ + goto out_of_frame; + else if (num_reduced_frame > 0) /* the first row of the reduced frame */ + { + /* + * Early check if row could be out of reduced frame. When RPR is + * enabled, EXCLUDE clause cannot be specified and the frame is always + * contiguous. So we can safely perform the following checks. Note, + * however, it is possible that a row is out of reduced frame if + * there's a NULL in the middle. So we need to check it in the + * following do loop. + */ + if (seektype == WINDOW_SEEK_HEAD && relpos >= num_reduced_frame) + goto out_of_frame; + if (seektype == WINDOW_SEEK_TAIL) + { + if (notnull_relpos >= num_reduced_frame) + goto out_of_frame; + + /* not out of reduced frame. Set abspos as a starting point */ + abs_pos = winstate->frameheadpos + num_reduced_frame - 1; + } + } + do { int inframe; @@ -3522,6 +3902,16 @@ ignorenulls_getfuncarginframe(WindowObject winobj, int argno, } advance: abs_pos += forward; + if (rpr_is_defined(winstate)) + { + /* + * Check whether we are still in the reduced frame. (also check + * if we succeeded in getting the target row). + */ + num_reduced_frame--; + if (num_reduced_frame <= 0 && notnull_offset <= notnull_relpos) + goto out_of_frame; + } } while (notnull_offset <= notnull_relpos); if (set_mark) @@ -3664,128 +4054,1201 @@ put_notnull_info(WindowObject winobj, int64 pos, int argno, bool isnull) mbp[bpos] = mb; } -/*********************************************************************** - * API exposed to window functions - ***********************************************************************/ - - /* - * WinCheckAndInitializeNullTreatment - * Check null treatment clause and sets ignore_nulls + * eval_nav_offset + * Evaluate a pre-built row pattern navigation offset ExprState. * - * Window functions should call this to check if they are being called with - * a null treatment clause when they don't allow it, or to set ignore_nulls. + * The offset is a run-time constant (the parser rejects column references in a + * navigation offset), so it is evaluated once per scan -- when any parameter + * is bound. Returns the offset as an int64; a NULL or negative result is an + * error per the SQL standard (fail-closed, re-checked on every scan). When + * not validating, a NULL is reported as -1 so that it takes the same path a + * negative offset takes. */ -void -WinCheckAndInitializeNullTreatment(WindowObject winobj, - bool allowNullTreatment, - FunctionCallInfo fcinfo) +static int64 +eval_nav_offset(WindowAggState *winstate, ExprState *estate, bool validate) { - Assert(WindowObjectIsValid(winobj)); - if (winobj->ignore_nulls != NO_NULLTREATMENT && !allowNullTreatment) + ExprContext *econtext = winstate->ss.ps.ps_ExprContext; + Datum val; + bool isnull; + int64 offset; + + val = ExecEvalExprSwitchContext(estate, econtext, &isnull); + + if (isnull) { - const char *funcname = get_func_name(fcinfo->flinfo->fn_oid); + if (validate) + ereport(ERROR, + errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), + errmsg("row pattern navigation offset must not be null")); + return -1; /* the caller drops it from the reach */ + } - if (!funcname) - elog(ERROR, "could not get function name"); + offset = DatumGetInt64(val); + + if (offset < 0 && validate) ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("function %s does not allow RESPECT/IGNORE NULLS", - funcname))); - } - else if (winobj->ignore_nulls == PARSER_IGNORE_NULLS) - winobj->ignore_nulls = IGNORE_NULLS; + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("row pattern navigation offset must not be negative")); + + return offset; } /* - * WinGetPartitionLocalMemory - * Get working memory that lives till end of partition processing + * build_nav_offsets + * Create the per-navigation offset bookkeeping entry at executor init and + * compile its offset argument expression(s). * - * On first call within a given partition, this allocates and zeroes the - * requested amount of space. Subsequent calls just return the same chunk. - * - * Memory obtained this way is normally used to hold state that should be - * automatically reset for each new partition. If a window function wants - * to hold state across the whole query, fcinfo->fn_extra can be used in the - * usual way for that. + * The offsets are not evaluated here: a PARAM_EXEC offset (function inlining or + * a LATERAL reference) has no value until the node is (re)scanned. The + * concrete value is resolved per scan by resolve_nav_offsets(), mirroring how + * calculate_frame_offsets() handles the window frame bounds. */ -void * -WinGetPartitionLocalMemory(WindowObject winobj, Size sz) +static void +build_nav_offsets(RPRNavExpr *nav, WindowAggState *winstate) { - Assert(WindowObjectIsValid(winobj)); - if (winobj->localmem == NULL) - winobj->localmem = - MemoryContextAllocZero(winobj->winstate->partcontext, sz); - return winobj->localmem; -} + RPRNavOffsets *entry = palloc0_object(RPRNavOffsets); -/* - * WinGetCurrentPosition - * Return the current row's position (counting from 0) within the current - * partition. - */ -int64 -WinGetCurrentPosition(WindowObject winobj) -{ - Assert(WindowObjectIsValid(winobj)); - return winobj->winstate->currentpos; + /* + * Parser guarantee (mirrors compute_matchStartDependent): nav's direct + * children are never RPRNavExpr -- compound nesting is flattened in place + * and any other nesting is rejected. Outer-kind dispatch is sufficient. + */ + Assert(nav->arg == NULL || !IsA(nav->arg, RPRNavExpr)); + Assert(nav->offset_arg == NULL || !IsA(nav->offset_arg, RPRNavExpr)); + Assert(nav->compound_offset_arg == NULL || + !IsA(nav->compound_offset_arg, RPRNavExpr)); + + entry->nav = nav; + if (nav->offset_arg != NULL) + entry->offset_state = ExecInitExpr(nav->offset_arg, + (PlanState *) winstate); + if (nav->compound_offset_arg != NULL) + entry->compound_offset_state = ExecInitExpr(nav->compound_offset_arg, + (PlanState *) winstate); + + /* + * Own the execution state of the compiled navigation. ExecInitExprRec() + * runs after this and reaches the entry by nav->navno; the offsets stay + * unset until resolve_nav_offsets() settles them for the scan. + */ + entry->rprnavstate = makeNode(RPRNavState); + entry->rprnavstate->winstate = winstate; + entry->rprnavstate->rprnavexpr = nav; + entry->rprnavstate->offset.isnull = true; + entry->rprnavstate->offset.value = (Datum) 0; + entry->rprnavstate->compound_offset.isnull = true; + entry->rprnavstate->compound_offset.value = (Datum) 0; + + winstate->rprNavOffsets = lappend(winstate->rprNavOffsets, entry); } -/* - * WinGetPartitionRowCount - * Return total number of rows contained in the current partition. - * - * Note: this is a relatively expensive operation because it forces the - * whole partition to be "spooled" into the tuplestore at once. Once - * executed, however, additional calls within the same partition are cheap. - */ -int64 -WinGetPartitionRowCount(WindowObject winobj) +static bool +RPRNavExpr_walker(Node *node, WindowAggState *winstate) { - Assert(WindowObjectIsValid(winobj)); - spool_tuples(winobj->winstate, -1); - return winobj->winstate->spooled_rows; + if (node == NULL) + return false; + if (IsA(node, RPRNavExpr)) + build_nav_offsets(castNode(RPRNavExpr, node), winstate); + + return expression_tree_walker(node, RPRNavExpr_walker, winstate); } /* - * WinSetMarkPosition - * Set the "mark" position for the window object, which is the oldest row - * number (counting from 0) it is allowed to fetch during all subsequent - * operations within the current partition. + * build_define_offsets + * At executor init, create one RPRNavOffsets entry per navigation in the + * DEFINE clause and compile its offset argument expressions. * - * Window functions do not have to call this, but are encouraged to move the - * mark forward when possible to keep the tuplestore size down and prevent - * having to spill rows to disk. + * Entries are appended in walk order, the order compute_define_metadata() + * numbered them in, so entry i is the navigation with navno i. + * + * The concrete offset values -- and the tuplestore trim bounds derived from + * them -- are resolved later, per scan, by resolve_nav_offsets(). Only an RPR + * window reaches here, and the fields this fills are left at their palloc0 + * defaults on the paths that return early. */ -void -WinSetMarkPosition(WindowObject winobj, int64 markpos) +static void +build_define_offsets(WindowAggState *winstate, List *defineClause) { - WindowAggState *winstate; + EvalDefineOffsetsContext ctx; - Assert(WindowObjectIsValid(winobj)); - winstate = winobj->winstate; + if (defineClause == NIL) + return; - if (markpos < winobj->markpos) - elog(ERROR, "cannot move WindowObject's mark position backward"); - tuplestore_select_read_pointer(winstate->buffer, winobj->markptr); - if (markpos > winobj->markpos) + foreach_node(TargetEntry, te, defineClause) { - tuplestore_skiptuples(winstate->buffer, - markpos - winobj->markpos, - true); - winobj->markpos = markpos; + RPRNavExpr_walker((Node *) te->expr, winstate); } - tuplestore_select_read_pointer(winstate->buffer, winobj->readptr); - if (markpos > winobj->seekpos) + + /* + * Resolve the offsets that are already constant at plan time, so EXPLAIN + * (which never executes, hence never reaches resolve_nav_offsets()) shows + * the real trim bounds. A parameterized offset (PARAM_EXTERN under a + * generic plan, or a PARAM_EXEC) has no value yet and is left for + * resolve_nav_offsets() to bound per scan. + */ + ctx.winstate = winstate; + ctx.maxOffset = 0; + ctx.maxOverflow = false; + ctx.minFirstOffset = PG_INT64_MAX; + ctx.hasMax = false; + ctx.hasFirst = false; + ctx.validate = false; /* init resolution is for EXPLAIN display only */ + + foreach_ptr(RPRNavOffsets, entry, winstate->rprNavOffsets) { - tuplestore_skiptuples(winstate->buffer, - markpos - winobj->seekpos, - true); - winobj->seekpos = markpos; + RPRNavExpr *nav = entry->nav; + bool is_const; + + /* + * A foldable offset such as PREV(v, 1 + 1) counts as fixed only if + * eval_const_expressions() reached inside the navigation and left a + * Const here, which the expression tree mutator does for us. + */ + is_const = (nav->offset_arg == NULL || IsA(nav->offset_arg, Const)) && + (nav->compound_offset_arg == NULL || + IsA(nav->compound_offset_arg, Const)); + + if (is_const) + { + /* constant offset: resolvable now, for EXPLAIN and the scan */ + resolve_one_nav(entry, &ctx); + } + else + { + /* + * A parameterized offset (a bind PARAM_EXTERN or, via + * SRF/function inlining, a correlated PARAM_EXEC) has no + * dependable value at init. Like a window frame offset it is + * resolved at execution by resolve_nav_offsets(), and EXPLAIN + * shows "runtime". + */ + if (nav->kind == RPR_NAV_PREV || nav->kind == RPR_NAV_LAST || + nav->kind == RPR_NAV_PREV_LAST || nav->kind == RPR_NAV_NEXT_LAST) + { + ctx.hasMax = true; + winstate->navMaxOffsetKind = RPR_NAV_OFFSET_NEEDS_EVAL; + } + if (nav->kind == RPR_NAV_FIRST || nav->kind == RPR_NAV_PREV_FIRST || + nav->kind == RPR_NAV_NEXT_FIRST) + { + ctx.hasFirst = true; + winstate->navFirstOffsetKind = RPR_NAV_OFFSET_NEEDS_EVAL; + } + } } -} -/* + if (ctx.maxOverflow) + { + /* + * a const/bind overflow forces retain-all, unless a param already + * made this dimension "runtime" (NEEDS_EVAL wins for display) + */ + if (winstate->navMaxOffsetKind != RPR_NAV_OFFSET_NEEDS_EVAL) + winstate->navMaxOffsetKind = RPR_NAV_OFFSET_RETAIN_ALL; + } + else + winstate->navMaxOffset = ctx.maxOffset; + + winstate->hasMaxNav = ctx.hasMax; + + /* minFirstOffset is still PG_INT64_MAX when there is no FIRST */ + winstate->hasFirstNav = ctx.hasFirst; + winstate->navFirstOffset = ctx.minFirstOffset; +} + +/* + * resolve_one_nav + * Evaluate one navigation's offset(s) for the current scan, pin the + * resolved values into its RPRNavState, and accumulate the backward and + * forward reach used to size the tuplestore trim. + */ +static void +resolve_one_nav(RPRNavOffsets *entry, EvalDefineOffsetsContext *context) +{ + RPRNavExpr *nav = entry->nav; + int64 inner; + int64 outer; + + /* Inner offset */ + if (entry->offset_state != NULL) + inner = eval_nav_offset(context->winstate, entry->offset_state, + context->validate); + else if (nav->kind == RPR_NAV_PREV || nav->kind == RPR_NAV_NEXT) + inner = 1; + else + inner = 0; + + /* Outer (compound) offset */ + if (entry->compound_offset_state != NULL) + outer = eval_nav_offset(context->winstate, entry->compound_offset_state, + context->validate); + else + outer = 1; + + /* + * An offset that is negative, or null and therefore reported as -1, is + * rejected at execution, where eval_nav_offset() has already raised the + * error before we get here, so this navigation can never run and needs no + * rows retained. Leave it out of both reaches, which also keeps the + * arithmetic below on non-negative operands. + */ + if (inner < 0 || outer < 0) + { + Assert(!context->validate); + return; + } + + /* + * Pin the resolved values into the compiled navigation's RPRNavState, so + * ExecEvalRPRNavSet() reads this scan's constant instead of re-evaluating + * the offset per row. + */ + entry->rprnavstate->offset.isnull = false; + entry->rprnavstate->offset.value = Int64GetDatum(inner); + entry->rprnavstate->compound_offset.isnull = false; + entry->rprnavstate->compound_offset.value = Int64GetDatum(outer); + + /* + * Backward reach: PREV, LAST at any offset including the default 0, and + * compound PREV_LAST/NEXT_LAST. + */ + if (nav->kind == RPR_NAV_PREV || + nav->kind == RPR_NAV_LAST || + nav->kind == RPR_NAV_PREV_LAST || + nav->kind == RPR_NAV_NEXT_LAST) + { + context->hasMax = true; + + if (!context->maxOverflow) + { + int64 reach = 0; + + if (nav->kind == RPR_NAV_PREV || nav->kind == RPR_NAV_LAST) + reach = inner; + else if (nav->kind == RPR_NAV_PREV_LAST) + { + if (pg_add_s64_overflow(inner, outer, &reach)) + context->maxOverflow = true; + } + else + reach = Max(inner - outer, 0); + + if (!context->maxOverflow) + context->maxOffset = Max(context->maxOffset, reach); + } + } + + /* Forward reach from match_start: FIRST, compound PREV_FIRST/NEXT_FIRST */ + if (nav->kind == RPR_NAV_FIRST || + nav->kind == RPR_NAV_PREV_FIRST || + nav->kind == RPR_NAV_NEXT_FIRST) + { + int64 reach; + + context->hasFirst = true; + + if (nav->kind == RPR_NAV_FIRST) + reach = inner; + else if (nav->kind == RPR_NAV_PREV_FIRST) + reach = inner - outer; /* both >= 0, cannot underflow int64 */ + else + { + /* NEXT_FIRST: inner + outer, always >= 0; clamp on overflow */ + if (pg_add_s64_overflow(inner, outer, &reach)) + reach = PG_INT64_MAX; + } + + context->minFirstOffset = Min(context->minFirstOffset, reach); + } +} + +/* + * resolve_nav_offsets + * Resolve every navigation offset for the current scan and store the + * tuplestore trim bounds in the WindowAggState. + * + * Called from ExecWindowAgg on the first call and after every rescan -- the + * same place calculate_frame_offsets() resolves the window frame bounds. By + * then every parameter (PARAM_EXTERN and PARAM_EXEC alike) is bound, and the + * offset is a run-time constant, so a single evaluation per scan is correct. + * This keeps the trim finite for a parameterized offset (no retain-all) and + * revalidates it (fail-closed) on each scan. + */ +static void +resolve_nav_offsets(WindowAggState *winstate) +{ + EvalDefineOffsetsContext ctx; + + /* Servicing the request now; clear the per-scan pending flag */ + winstate->navResolvePending = false; + + winstate->navMaxOffset = 0; + winstate->navMaxOffsetKind = RPR_NAV_OFFSET_FIXED; + winstate->hasMaxNav = false; + winstate->hasFirstNav = false; + winstate->navFirstOffset = 0; + winstate->navFirstOffsetKind = RPR_NAV_OFFSET_FIXED; + + if (winstate->rprNavOffsets == NIL) + return; + + ctx.winstate = winstate; + ctx.maxOffset = 0; + ctx.maxOverflow = false; + ctx.minFirstOffset = PG_INT64_MAX; + ctx.hasMax = false; + ctx.hasFirst = false; + ctx.validate = true; /* execution: fail-closed on null/negative */ + + foreach_ptr(RPRNavOffsets, entry, winstate->rprNavOffsets) + { + resolve_one_nav(entry, &ctx); + } + + /* + * Backward (PREV/LAST) reach. On int64 overflow the lookback cannot be + * bounded, so mark the dimension RETAIN_ALL; advance_nav_mark() reads it + * to disable tuplestore trim. + */ + if (ctx.maxOverflow) + winstate->navMaxOffsetKind = RPR_NAV_OFFSET_RETAIN_ALL; + else + winstate->navMaxOffset = ctx.maxOffset; + + winstate->hasMaxNav = ctx.hasMax; + + /* Forward (FIRST) reach; never needs a retain-all sentinel */ + winstate->hasFirstNav = ctx.hasFirst; + winstate->navFirstOffset = ctx.minFirstOffset; +} + +/* + * rpr_is_defined + * Return true if row pattern recognition is defined. + */ +static bool +rpr_is_defined(WindowAggState *winstate) +{ + return winstate->rpPattern != NULL; +} + +/* + * ----------------- + * row_is_in_reduced_frame + * Determine whether a row is in the current row's reduced window frame + * according to row pattern matching + * + * The row must have already been determined to be in a full window frame + * and fetched into the slot. + * + * Returns: + * = 0, RPR is not defined. + * >0, if the row is the first in the reduced frame. Return the number of rows + * in the reduced frame. + * -1, if the row is an unmatched row + * -2, if the row is inside the current match but is not its first row (an + * interior row of the match) + * ----------------- + */ +static int64 +row_is_in_reduced_frame(WindowObject winobj, int64 pos) +{ + WindowAggState *winstate = winobj->winstate; + int state; + int64 rtn; + + if (!rpr_is_defined(winstate)) + { + /* + * RPR is not defined. Assume that we are always in the reduced window + * frame. + */ + rtn = 0; + return rtn; + } + + ensure_reduced_frame(winobj, pos); + + state = get_reduced_frame_status(winstate, pos); + + switch (state) + { + case RF_FRAME_HEAD: + rtn = winstate->rpr_match_length; + break; + + case RF_SKIPPED: + rtn = -2; + break; + + case RF_UNMATCHED: + case RF_EMPTY_MATCH: + rtn = -1; + break; + + default: + elog(ERROR, "unrecognized state: %d at: " INT64_FORMAT, + state, pos); + break; + } + + return rtn; +} + +/* + * ensure_reduced_frame + * Drive the row pattern match forward so pos is resolved. + * + * Idempotent: a pos already determined is left untouched, so callers may + * invoke this repeatedly for the same row (once per row to track the row + * scan, and again when a window function accesses the frame). + */ +static void +ensure_reduced_frame(WindowObject winobj, int64 pos) +{ + WindowAggState *winstate = winobj->winstate; + + if (get_reduced_frame_status(winstate, pos) == RF_NOT_DETERMINED) + { + update_frameheadpos(winstate); + update_reduced_frame(winobj, pos); + } +} + +/* + * clear_reduced_frame + * Clear reduced frame status + */ +static void +clear_reduced_frame(WindowAggState *winstate) +{ + winstate->rpr_match_start = -1; /* start < 0: no result determined yet */ + winstate->rpr_match_length = -1; +} + +/* + * get_reduced_frame_status + * Look up a position against the current match. + * + * Returns one of the RF_* constants: + * RF_NOT_DETERMINED pos has not been processed yet + * RF_FRAME_HEAD pos is the start of the current match + * RF_SKIPPED pos is inside the current match but not the start + * RF_UNMATCHED pos is processed but not part of any match + * RF_EMPTY_MATCH pos is the start of an empty (zero-length) match + * + * The result slot encodes four states across two fields, with no separate + * "valid"/"matched" flags: + * + * start < 0 not determined (cleared slot) + * start >= 0, length == -1 unmatched (covers only the start row) + * start >= 0, length == 0 empty match (zero-length match at start) + * start >= 0, length >= 1 real match spanning [start, start + length) + * + * The tests below form a cascade with early returns, so their order is + * significant. + */ +static int +get_reduced_frame_status(WindowAggState *winstate, int64 pos) +{ + int64 start = winstate->rpr_match_start; + int64 length = winstate->rpr_match_length; + + if (start < 0) + return RF_NOT_DETERMINED; /* cleared slot: no result recorded yet */ + + /* + * Unmatched (length -1) and empty match (length 0) do not describe a + * positive-length range, so they are classified before the range test. + * Each covers only its own start row; any other position is not part of + * this record and is still undetermined. + */ + if (length == -1) + return (pos == start) ? RF_UNMATCHED : RF_NOT_DETERMINED; + if (length == 0) + return (pos == start) ? RF_EMPTY_MATCH : RF_NOT_DETERMINED; + + /* + * By here length >= 1, so [start, start + length) is a well-formed range. + */ + if (pos < start || pos >= start + length) + return RF_NOT_DETERMINED; + + /* pos lies within a real match. */ + if (pos == start) + return RF_FRAME_HEAD; + + return RF_SKIPPED; +} + +/* + * advance_nav_mark + * Advance the RPR navigation mark, derived from the NFA frontier + * (currentPos) but held back by the navigation's backward reach, so + * tuplestore_trim() can free rows no longer reachable by navigation. + * + * The nav read pointer is independent of the aggregate and per-function read + * pointers, so moving its mark does not affect their fetches; it only bounds + * the DEFINE clause's own PREV/LAST/FIRST lookups. Backward reach (PREV/LAST) + * is measured from the frontier. FIRST reaches back from the head context's + * matchStartRow instead, so it is bounded separately; without FIRST the mark + * can follow the frontier freely. + */ +static void +advance_nav_mark(WindowAggState *winstate, int64 currentPos) +{ + int64 navmarkpos; + + /* No RPR navigation read pointer: nothing to advance */ + if (winstate->nav_winobj == NULL) + return; + + /* RETAIN_ALL (offset overflow) disables trim for the backward dimension */ + if (winstate->navMaxOffsetKind == RPR_NAV_OFFSET_RETAIN_ALL) + return; + + if (currentPos > winstate->navMaxOffset) + navmarkpos = currentPos - winstate->navMaxOffset; + else + navmarkpos = 0; + + if (winstate->hasFirstNav && winstate->nfaContext != NULL) + { + int64 firstreach; + + /* + * Head context has the smallest matchStartRow (contexts appended in + * nondecreasing order), so bounding by it covers every FIRST reach. + */ + if (!pg_add_s64_overflow(winstate->nfaContext->matchStartRow, + winstate->navFirstOffset, + &firstreach)) + navmarkpos = Min(navmarkpos, Max(firstreach, 0)); + } + + if (navmarkpos > winstate->nav_winobj->markpos) + WinSetMarkPosition(winstate->nav_winobj, navmarkpos); +} + +/* + * advance_reduced_frame_nfa + * Drive the NFA forward until targetCtx completes or the partition ends. + * + * This is the match driver, extracted from update_reduced_frame(), which calls + * it to advance the match and then records the resolved result. Row + * evaluations are shared across all active contexts. + */ +static void +advance_reduced_frame_nfa(WindowObject winobj, RPRNFAContext *targetCtx, + int64 pos, bool hasLimitedFrame, int64 frameOffset) +{ + WindowAggState *winstate = winobj->winstate; + int64 currentPos; + int64 startPos; + int64 saved_currentpos = winstate->currentpos; + + /* + * Determine where to start processing. Usually nfaLastProcessedRow+1 >= + * pos since contexts are created at currentPos+1 during processing. + * However, pos can exceed this when rows are skipped (e.g., unmatched + * rows don't update nfaLastProcessedRow). + */ + startPos = Max(pos, winstate->nfaLastProcessedRow + 1); + + /* + * Process rows until target context completes or we hit boundaries. Each + * row evaluation is shared across all active contexts. + * + * winstate->currentpos is set to the scan position for the whole row and + * left in place across ExecRPRProcessRow, because DEFINE predicates are + * evaluated lazily during matching (nfa_eval_var_match) and their + * EEOP_RPR_NAV_SET opcodes read currentpos. It is restored after the + * loop. + */ + for (currentPos = startPos; targetCtx->states != NULL; currentPos++) + { + bool rowExists; + + /* + * Evaluate variables for this row - done only once, shared by all + * contexts. + * + * Set nav_match_start to the head context's matchStartRow for + * FIRST/LAST navigation. Match_start-dependent variables (FIRST, + * LAST-with-offset) are re-evaluated per-context in ExecRPRProcessRow + * when matchStartRow differs. + */ + winstate->currentpos = currentPos; + winstate->nav_match_start = targetCtx->matchStartRow; + rowExists = rpr_prepare_row(winobj, currentPos, winstate->nfaVarMatched); + + /* No more rows in partition? Finalize all contexts */ + if (!rowExists) + { + ExecRPRFinalizeAllContexts(winstate, currentPos - 1); + /* Clean up dead contexts from finalization */ + ExecRPRCleanupDeadContexts(winstate, targetCtx); + break; + } + + /* Update last processed row */ + winstate->nfaLastProcessedRow = currentPos; + + /*-------------------------- + * Process all contexts for this row: + * 1. Match all (convergence) + * 2. Absorb redundant + * 3. Advance all (divergence) + */ + ExecRPRProcessRow(winstate, currentPos, hasLimitedFrame, frameOffset); + + /* + * Create a new context for the next potential start position. This + * enables overlapping match detection for SKIP TO NEXT ROW. + */ + ExecRPRStartContext(winstate, currentPos + 1); + + /* + * Clean up dead contexts (failed with no active states and no match). + * This removes contexts that failed during processing and counts them + * appropriately as pruned or mismatched. + */ + ExecRPRCleanupDeadContexts(winstate, targetCtx); + + /* Advance the nav mark to the frontier so trim can free old rows. */ + advance_nav_mark(winstate, currentPos); + } + + /* Restore the output row position borrowed for the NFA scan. */ + winstate->currentpos = saved_currentpos; +} + +/* + * update_reduced_frame + * Update reduced frame info using multi-context NFA pattern matching. + * + * Maintains multiple NFA contexts simultaneously, one for each potential + * match start position. This allows sharing row evaluations across contexts, + * avoiding redundant DEFINE clause evaluations when rewinding for SKIP TO + * NEXT ROW mode. + * + * Key optimizations: + * - Row evaluations (expensive DEFINE clauses) happen only once per row + * - All active contexts share the same evaluation results + * - Contexts persist across calls, enabling O(n) DEFINE evaluations + */ +static void +update_reduced_frame(WindowObject winobj, int64 pos) +{ + WindowAggState *winstate = winobj->winstate; + RPRNFAContext *targetCtx; + int frameOptions = winstate->frameOptions; + bool hasLimitedFrame; + int64 frameOffset = 0; + int64 matchLen; + + /* + * Check if we have a limited frame (ROWS ... N FOLLOWING). Each context + * needs its own frame end based on matchStartRow + offset. + */ + hasLimitedFrame = (frameOptions & FRAMEOPTION_ROWS) && + !(frameOptions & FRAMEOPTION_END_UNBOUNDED_FOLLOWING); + if (hasLimitedFrame) + frameOffset = DatumGetInt64(winstate->endOffsetValue); + + /* + * Case 1: pos is before any existing context's start position. This means + * the position was already processed and determined unmatched. Head is + * the oldest context (lowest matchStartRow) since contexts are added at + * tail with increasing positions. + */ + if (winstate->nfaContext != NULL && + pos < winstate->nfaContext->matchStartRow) + { + /* already processed, unmatched */ + winstate->rpr_match_start = pos; + winstate->rpr_match_length = -1; + return; + } + + /* + * Case 2: Find existing context for this pos, or create new one. + */ + targetCtx = ExecRPRGetHeadContext(winstate, pos); + if (targetCtx == NULL) + { + /* + * No context exists. If pos is already processed, it means this row + * was already determined to be unmatched or skipped - no need to + * reprocess. + */ + if (pos <= winstate->nfaLastProcessedRow) + { + /* already processed, unmatched */ + winstate->rpr_match_start = pos; + winstate->rpr_match_length = -1; + return; + } + /* Not yet processed - create new context and start fresh */ + targetCtx = ExecRPRStartContext(winstate, pos); + } + else if (targetCtx->states == NULL) + { + /* + * The head context already completed in an earlier call. Reachable + * under SKIP TO NEXT ROW, where overlapping contexts let one reach + * FIN -- recording its result -- before the call for its own start + * row arrives. Register that result. + */ + goto register_result; + } + + /* Drive the NFA forward until pos's match is resolved. */ + advance_reduced_frame_nfa(winobj, targetCtx, pos, hasLimitedFrame, + frameOffset); + +register_result: + Assert(pos == targetCtx->matchStartRow); + + /* + * Record match result. A determined slot has rpr_match_start >= 0; the + * length then gives the kind: -1 unmatched, 0 empty match, >= 1 real + * match. A cleared slot keeps rpr_match_start < 0. + */ + winstate->rpr_match_start = targetCtx->matchStartRow; + + if (targetCtx->matchEndRow < targetCtx->matchStartRow) + { + matchLen = targetCtx->lastProcessedRow - targetCtx->matchStartRow + 1; + + if (targetCtx->matchedState != NULL) + { + /* Empty match: FIN reached but 0 rows consumed */ + winstate->rpr_match_length = 0; + ExecRPRRecordContextSuccess(winstate, 0); + } + else + { + /* No match */ + winstate->rpr_match_length = -1; + ExecRPRRecordContextFailure(winstate, matchLen); + } + ExecRPRFreeContext(winstate, targetCtx); + return; + } + + /* Match succeeded */ + matchLen = targetCtx->matchEndRow - targetCtx->matchStartRow + 1; + + winstate->rpr_match_length = matchLen; + ExecRPRRecordContextSuccess(winstate, matchLen); + + /* Remove the matched context */ + ExecRPRFreeContext(winstate, targetCtx); +} + +/* + * rpr_prepare_row + * + * Prepare the DEFINE evaluation context for the current row and reset the + * per-row tri-state cache to RPR_VAR_UNEVALUATED. + * Returns true if the row exists, false if out of partition. + * + * DEFINE predicates are NOT evaluated here. Each variable is evaluated lazily + * the first time the NFA consumes it (nfa_eval_var_match), so a variable that + * no active state tests at this row is never evaluated. The caller + * (advance_reduced_frame_nfa) sets winstate->currentpos to pos for the whole + * row, so the deferred evaluation's EEOP_RPR_NAV_SET opcodes calculate target + * positions (currentpos +/- offset) correctly. + * + * Uses 1-slot model: only ecxt_outertuple is set to the current row. + * PREV/NEXT/FIRST/LAST navigation is handled by EEOP_RPR_NAV_SET/RESTORE + * opcodes during expression evaluation, which temporarily swap the slot. + */ +static bool +rpr_prepare_row(WindowObject winobj, int64 pos, RPRVarMatch *varMatched) +{ + WindowAggState *winstate = winobj->winstate; + ExprContext *econtext = winstate->rprContext; + TupleTableSlot *slot; + + /* Release the previous row's DEFINE evaluation memory */ + ResetExprContext(econtext); + + /* Fetch current row into temp_slot_1 */ + slot = winstate->temp_slot_1; + if (!window_gettupleslot(winobj, pos, slot)) + return false; /* No row exists */ + + /* Set up 1-slot context: only ecxt_outertuple */ + econtext->ecxt_outertuple = slot; + + /* Invalidate nav_slot cache so PREV/NEXT re-fetch for new row */ + winstate->nav_slot_pos = -1; + + /* + * Reset the per-row cache to "unevaluated"; each variable's DEFINE is + * evaluated lazily at first consumption in nfa_eval_var_match. + */ + if (varMatched != NULL) + memset(varMatched, 0, + sizeof(RPRVarMatch) * list_length(winstate->defineClauseExprs)); + + return true; /* Row exists */ +} + +/* + * WinGetSlotInFrame + * slot: TupleTableSlot to store the result + * relpos: signed rowcount offset from the seek position + * seektype: WINDOW_SEEK_HEAD or WINDOW_SEEK_TAIL + * set_mark: If the row is found/in frame and set_mark is true, the mark is + * moved to the row as a side-effect. + * isnull: output argument, receives isnull status of result + * isout: output argument, set to indicate whether target row position + * is out of frame (can pass NULL if caller doesn't care about this) + * + * Returns 0 if we successfully got the slot, or nonzero if out of frame. + * (isout is also set in the latter case.) + */ +static int +WinGetSlotInFrame(WindowObject winobj, TupleTableSlot *slot, + int relpos, int seektype, bool set_mark, + bool *isnull, bool *isout) +{ + WindowAggState *winstate; + int64 abs_pos; + int64 mark_pos; + int64 num_reduced_frame; + + Assert(WindowObjectIsValid(winobj)); + winstate = winobj->winstate; + + switch (seektype) + { + case WINDOW_SEEK_CURRENT: + elog(ERROR, "WINDOW_SEEK_CURRENT is not supported for WinGetFuncArgInFrame"); + abs_pos = mark_pos = 0; /* keep compiler quiet */ + break; + case WINDOW_SEEK_HEAD: + /* rejecting relpos < 0 is easy and simplifies code below */ + if (relpos < 0) + goto out_of_frame; + update_frameheadpos(winstate); + abs_pos = winstate->frameheadpos + relpos; + mark_pos = abs_pos; + + /* + * Account for exclusion option if one is active, but advance only + * abs_pos not mark_pos. This prevents changes of the current + * row's peer group from resulting in trying to fetch a row before + * some previous mark position. + * + * Note that in some corner cases such as current row being + * outside frame, these calculations are theoretically too simple, + * but it doesn't matter because we'll end up deciding the row is + * out of frame. We do not attempt to avoid fetching rows past + * end of frame; that would happen in some cases anyway. + */ + switch (winstate->frameOptions & FRAMEOPTION_EXCLUSION) + { + case 0: + /* no adjustment needed */ + break; + case FRAMEOPTION_EXCLUDE_CURRENT_ROW: + if (abs_pos >= winstate->currentpos && + winstate->currentpos >= winstate->frameheadpos) + abs_pos++; + break; + case FRAMEOPTION_EXCLUDE_GROUP: + update_grouptailpos(winstate); + if (abs_pos >= winstate->groupheadpos && + winstate->grouptailpos > winstate->frameheadpos) + { + int64 overlapstart = Max(winstate->groupheadpos, + winstate->frameheadpos); + + abs_pos += winstate->grouptailpos - overlapstart; + } + break; + case FRAMEOPTION_EXCLUDE_TIES: + update_grouptailpos(winstate); + if (abs_pos >= winstate->groupheadpos && + winstate->grouptailpos > winstate->frameheadpos) + { + int64 overlapstart = Max(winstate->groupheadpos, + winstate->frameheadpos); + + if (abs_pos == overlapstart) + abs_pos = winstate->currentpos; + else + abs_pos += winstate->grouptailpos - overlapstart - 1; + } + break; + default: + elog(ERROR, "unrecognized frame option state: 0x%x", + winstate->frameOptions); + break; + } + num_reduced_frame = row_is_in_reduced_frame(winobj, + winstate->frameheadpos); + if (num_reduced_frame < 0) + goto out_of_frame; + else if (num_reduced_frame > 0) + if (relpos >= num_reduced_frame) + goto out_of_frame; + break; + case WINDOW_SEEK_TAIL: + /* rejecting relpos > 0 is easy and simplifies code below */ + if (relpos > 0) + goto out_of_frame; + + /* + * RPR cares about frame head pos. Need to call + * update_frameheadpos + */ + update_frameheadpos(winstate); + + update_frametailpos(winstate); + abs_pos = winstate->frametailpos - 1 + relpos; + + /* + * Account for exclusion option if one is active. If there is no + * exclusion, we can safely set the mark at the accessed row. But + * if there is, we can only mark the frame start, because we can't + * be sure how far back in the frame the exclusion might cause us + * to fetch in future. Furthermore, we have to actually check + * against frameheadpos here, since it's unsafe to try to fetch a + * row before frame start if the mark might be there already. + */ + switch (winstate->frameOptions & FRAMEOPTION_EXCLUSION) + { + case 0: + /* no adjustment needed */ + mark_pos = abs_pos; + break; + case FRAMEOPTION_EXCLUDE_CURRENT_ROW: + if (abs_pos <= winstate->currentpos && + winstate->currentpos < winstate->frametailpos) + abs_pos--; + update_frameheadpos(winstate); + if (abs_pos < winstate->frameheadpos) + goto out_of_frame; + mark_pos = winstate->frameheadpos; + break; + case FRAMEOPTION_EXCLUDE_GROUP: + update_grouptailpos(winstate); + if (abs_pos < winstate->grouptailpos && + winstate->groupheadpos < winstate->frametailpos) + { + int64 overlapend = Min(winstate->grouptailpos, + winstate->frametailpos); + + abs_pos -= overlapend - winstate->groupheadpos; + } + update_frameheadpos(winstate); + if (abs_pos < winstate->frameheadpos) + goto out_of_frame; + mark_pos = winstate->frameheadpos; + break; + case FRAMEOPTION_EXCLUDE_TIES: + update_grouptailpos(winstate); + if (abs_pos < winstate->grouptailpos && + winstate->groupheadpos < winstate->frametailpos) + { + int64 overlapend = Min(winstate->grouptailpos, + winstate->frametailpos); + + if (abs_pos == overlapend - 1) + abs_pos = winstate->currentpos; + else + abs_pos -= overlapend - 1 - winstate->groupheadpos; + } + update_frameheadpos(winstate); + if (abs_pos < winstate->frameheadpos) + goto out_of_frame; + mark_pos = winstate->frameheadpos; + break; + default: + elog(ERROR, "unrecognized frame option state: 0x%x", + winstate->frameOptions); + mark_pos = 0; /* keep compiler quiet */ + break; + } + + num_reduced_frame = row_is_in_reduced_frame(winobj, + winstate->frameheadpos); + /* zero means a non-RPR window, which has no reduced frame */ + if (num_reduced_frame < 0) + goto out_of_frame; + else if (num_reduced_frame > 0) + { + if (-relpos >= num_reduced_frame) + goto out_of_frame; + abs_pos = winstate->frameheadpos + relpos + + num_reduced_frame - 1; + } + break; + default: + elog(ERROR, "unrecognized window seek type: %d", seektype); + abs_pos = mark_pos = 0; /* keep compiler quiet */ + break; + } + + if (!window_gettupleslot(winobj, abs_pos, slot)) + goto out_of_frame; + + /* The code above does not detect all out-of-frame cases, so check */ + if (row_is_in_frame(winobj, abs_pos, slot, false) <= 0) + goto out_of_frame; + + if (isout) + *isout = false; + if (set_mark) + { + /* + * If RPR is enabled and seek type is WINDOW_SEEK_TAIL, we set the + * mark position unconditionally to frameheadpos. In this case the + * frame always starts at CURRENT_ROW and never goes back, thus + * setting the mark at the position is safe. + */ + if (winstate->rpPattern != NULL && seektype == WINDOW_SEEK_TAIL) + mark_pos = winstate->frameheadpos; + WinSetMarkPosition(winobj, mark_pos); + } + return 0; + +out_of_frame: + if (isout) + *isout = true; + *isnull = true; + return -1; +} + + +/*********************************************************************** + * API exposed to window functions + ***********************************************************************/ + + +/* + * WinCheckAndInitializeNullTreatment + * Check null treatment clause and sets ignore_nulls + * + * Window functions should call this to check if they are being called with + * a null treatment clause when they don't allow it, or to set ignore_nulls. + */ +void +WinCheckAndInitializeNullTreatment(WindowObject winobj, + bool allowNullTreatment, + FunctionCallInfo fcinfo) +{ + Assert(WindowObjectIsValid(winobj)); + if (winobj->ignore_nulls != NO_NULLTREATMENT && !allowNullTreatment) + { + const char *funcname = get_func_name(fcinfo->flinfo->fn_oid); + + if (!funcname) + elog(ERROR, "could not get function name"); + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("function %s does not allow RESPECT/IGNORE NULLS", + funcname))); + } + else if (winobj->ignore_nulls == PARSER_IGNORE_NULLS) + winobj->ignore_nulls = IGNORE_NULLS; +} + +/* + * WinGetPartitionLocalMemory + * Get working memory that lives till end of partition processing + * + * On first call within a given partition, this allocates and zeroes the + * requested amount of space. Subsequent calls just return the same chunk. + * + * Memory obtained this way is normally used to hold state that should be + * automatically reset for each new partition. If a window function wants + * to hold state across the whole query, fcinfo->fn_extra can be used in the + * usual way for that. + */ +void * +WinGetPartitionLocalMemory(WindowObject winobj, Size sz) +{ + Assert(WindowObjectIsValid(winobj)); + if (winobj->localmem == NULL) + winobj->localmem = + MemoryContextAllocZero(winobj->winstate->partcontext, sz); + return winobj->localmem; +} + +/* + * WinGetCurrentPosition + * Return the current row's position (counting from 0) within the current + * partition. + */ +int64 +WinGetCurrentPosition(WindowObject winobj) +{ + Assert(WindowObjectIsValid(winobj)); + return winobj->winstate->currentpos; +} + +/* + * WinGetPartitionRowCount + * Return total number of rows contained in the current partition. + * + * Note: this is a relatively expensive operation because it forces the + * whole partition to be "spooled" into the tuplestore at once. Once + * executed, however, additional calls within the same partition are cheap. + */ +int64 +WinGetPartitionRowCount(WindowObject winobj) +{ + Assert(WindowObjectIsValid(winobj)); + spool_tuples(winobj->winstate, -1); + return winobj->winstate->spooled_rows; +} + +/* + * WinSetMarkPosition + * Set the "mark" position for the window object, which is the oldest row + * number (counting from 0) it is allowed to fetch during all subsequent + * operations within the current partition. + * + * Window functions do not have to call this, but are encouraged to move the + * mark forward when possible to keep the tuplestore size down and prevent + * having to spill rows to disk. + */ +void +WinSetMarkPosition(WindowObject winobj, int64 markpos) +{ + WindowAggState *winstate; + + Assert(WindowObjectIsValid(winobj)); + winstate = winobj->winstate; + + if (markpos < winobj->markpos) + elog(ERROR, "cannot move WindowObject's mark position backward"); + tuplestore_select_read_pointer(winstate->buffer, winobj->markptr); + if (markpos > winobj->markpos) + { + tuplestore_skiptuples(winstate->buffer, + markpos - winobj->markpos, + true); + winobj->markpos = markpos; + } + tuplestore_select_read_pointer(winstate->buffer, winobj->readptr); + if (markpos > winobj->seekpos) + { + tuplestore_skiptuples(winstate->buffer, + markpos - winobj->seekpos, + true); + winobj->seekpos = markpos; + } +} + +/* * WinRowsArePeers * Compare two rows (specified by absolute position in partition) to see * if they are equal according to the ORDER BY clause. @@ -4030,8 +5493,6 @@ WinGetFuncArgInFrame(WindowObject winobj, int argno, WindowAggState *winstate; ExprContext *econtext; TupleTableSlot *slot; - int64 abs_pos; - int64 mark_pos; Assert(WindowObjectIsValid(winobj)); winstate = winobj->winstate; @@ -4042,166 +5503,15 @@ WinGetFuncArgInFrame(WindowObject winobj, int argno, return ignorenulls_getfuncarginframe(winobj, argno, relpos, seektype, set_mark, isnull, isout); - switch (seektype) + if (WinGetSlotInFrame(winobj, slot, + relpos, seektype, set_mark, + isnull, isout) == 0) { - case WINDOW_SEEK_CURRENT: - elog(ERROR, "WINDOW_SEEK_CURRENT is not supported for WinGetFuncArgInFrame"); - abs_pos = mark_pos = 0; /* keep compiler quiet */ - break; - case WINDOW_SEEK_HEAD: - /* rejecting relpos < 0 is easy and simplifies code below */ - if (relpos < 0) - goto out_of_frame; - update_frameheadpos(winstate); - abs_pos = winstate->frameheadpos + relpos; - mark_pos = abs_pos; - - /* - * Account for exclusion option if one is active, but advance only - * abs_pos not mark_pos. This prevents changes of the current - * row's peer group from resulting in trying to fetch a row before - * some previous mark position. - * - * Note that in some corner cases such as current row being - * outside frame, these calculations are theoretically too simple, - * but it doesn't matter because we'll end up deciding the row is - * out of frame. We do not attempt to avoid fetching rows past - * end of frame; that would happen in some cases anyway. - */ - switch (winstate->frameOptions & FRAMEOPTION_EXCLUSION) - { - case 0: - /* no adjustment needed */ - break; - case FRAMEOPTION_EXCLUDE_CURRENT_ROW: - if (abs_pos >= winstate->currentpos && - winstate->currentpos >= winstate->frameheadpos) - abs_pos++; - break; - case FRAMEOPTION_EXCLUDE_GROUP: - update_grouptailpos(winstate); - if (abs_pos >= winstate->groupheadpos && - winstate->grouptailpos > winstate->frameheadpos) - { - int64 overlapstart = Max(winstate->groupheadpos, - winstate->frameheadpos); - - abs_pos += winstate->grouptailpos - overlapstart; - } - break; - case FRAMEOPTION_EXCLUDE_TIES: - update_grouptailpos(winstate); - if (abs_pos >= winstate->groupheadpos && - winstate->grouptailpos > winstate->frameheadpos) - { - int64 overlapstart = Max(winstate->groupheadpos, - winstate->frameheadpos); - - if (abs_pos == overlapstart) - abs_pos = winstate->currentpos; - else - abs_pos += winstate->grouptailpos - overlapstart - 1; - } - break; - default: - elog(ERROR, "unrecognized frame option state: 0x%x", - winstate->frameOptions); - break; - } - break; - case WINDOW_SEEK_TAIL: - /* rejecting relpos > 0 is easy and simplifies code below */ - if (relpos > 0) - goto out_of_frame; - update_frametailpos(winstate); - abs_pos = winstate->frametailpos - 1 + relpos; - - /* - * Account for exclusion option if one is active. If there is no - * exclusion, we can safely set the mark at the accessed row. But - * if there is, we can only mark the frame start, because we can't - * be sure how far back in the frame the exclusion might cause us - * to fetch in future. Furthermore, we have to actually check - * against frameheadpos here, since it's unsafe to try to fetch a - * row before frame start if the mark might be there already. - */ - switch (winstate->frameOptions & FRAMEOPTION_EXCLUSION) - { - case 0: - /* no adjustment needed */ - mark_pos = abs_pos; - break; - case FRAMEOPTION_EXCLUDE_CURRENT_ROW: - if (abs_pos <= winstate->currentpos && - winstate->currentpos < winstate->frametailpos) - abs_pos--; - update_frameheadpos(winstate); - if (abs_pos < winstate->frameheadpos) - goto out_of_frame; - mark_pos = winstate->frameheadpos; - break; - case FRAMEOPTION_EXCLUDE_GROUP: - update_grouptailpos(winstate); - if (abs_pos < winstate->grouptailpos && - winstate->groupheadpos < winstate->frametailpos) - { - int64 overlapend = Min(winstate->grouptailpos, - winstate->frametailpos); - - abs_pos -= overlapend - winstate->groupheadpos; - } - update_frameheadpos(winstate); - if (abs_pos < winstate->frameheadpos) - goto out_of_frame; - mark_pos = winstate->frameheadpos; - break; - case FRAMEOPTION_EXCLUDE_TIES: - update_grouptailpos(winstate); - if (abs_pos < winstate->grouptailpos && - winstate->groupheadpos < winstate->frametailpos) - { - int64 overlapend = Min(winstate->grouptailpos, - winstate->frametailpos); - - if (abs_pos == overlapend - 1) - abs_pos = winstate->currentpos; - else - abs_pos -= overlapend - 1 - winstate->groupheadpos; - } - update_frameheadpos(winstate); - if (abs_pos < winstate->frameheadpos) - goto out_of_frame; - mark_pos = winstate->frameheadpos; - break; - default: - elog(ERROR, "unrecognized frame option state: 0x%x", - winstate->frameOptions); - mark_pos = 0; /* keep compiler quiet */ - break; - } - break; - default: - elog(ERROR, "unrecognized window seek type: %d", seektype); - abs_pos = mark_pos = 0; /* keep compiler quiet */ - break; + econtext->ecxt_outertuple = slot; + return ExecEvalExpr((ExprState *) list_nth(winobj->argstates, argno), + econtext, isnull); } - if (!window_gettupleslot(winobj, abs_pos, slot)) - goto out_of_frame; - - /* The code above does not detect all out-of-frame cases, so check */ - if (row_is_in_frame(winobj, abs_pos, slot, false) <= 0) - goto out_of_frame; - - if (isout) - *isout = false; - if (set_mark) - WinSetMarkPosition(winobj, mark_pos); - econtext->ecxt_outertuple = slot; - return ExecEvalExpr((ExprState *) list_nth(winobj->argstates, argno), - econtext, isnull); - -out_of_frame: if (isout) *isout = true; *isnull = true; diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c index fc80d3e55fe..74b7392f15f 100644 --- a/src/backend/jit/llvm/llvmjit_expr.c +++ b/src/backend/jit/llvm/llvmjit_expr.c @@ -128,6 +128,9 @@ llvm_compile_expr(ExprState *state) LLVMValueRef v_aggvalues; LLVMValueRef v_aggnulls; + /* RPR navigation: when true, EEOP_OUTER_VAR reloads from econtext */ + bool has_rpr_nav; + instr_time starttime; instr_time deform_starttime; instr_time endtime; @@ -297,6 +300,36 @@ llvm_compile_expr(ExprState *state) FIELDNO_EXPRCONTEXT_AGGNULLS, "v.econtext.aggnulls"); + /* + * RPR navigation opcodes (PREV/NEXT) swap ecxt_outertuple to a different + * row mid-expression. The JIT code loads v_outervalues and v_outernulls + * once in the entry block and reuses them for all EEOP_OUTER_VAR steps. + * After a slot swap, these cached pointers become stale because the new + * slot has its own tts_values/tts_isnull arrays. + * + * When RPR navigation opcodes are present, EEOP_OUTER_VAR reloads the + * slot pointer from econtext->ecxt_outertuple on every access instead of + * using the cached entry-block values. This avoids the SSA/PHI + * complexity while keeping the rest of the expression JIT-compiled. + * Expressions without RPR navigation use the cached values as before. + */ + has_rpr_nav = false; + if (parent && IsA(parent, WindowAggState) && + ((WindowAgg *) parent->plan)->rpPattern != NULL) + { + for (int opno = 0; opno < state->steps_len; opno++) + { + ExprEvalOp opcode = ExecEvalStepOp(state, &state->steps[opno]); + + if (opcode == EEOP_RPR_NAV_SET || + opcode == EEOP_RPR_NAV_RESTORE) + { + has_rpr_nav = true; + break; + } + } + } + /* allocate blocks for each op upfront, so we can do jumps easily */ opblocks = palloc_array(LLVMBasicBlockRef, state->steps_len); for (int opno = 0; opno < state->steps_len; opno++) @@ -459,8 +492,37 @@ llvm_compile_expr(ExprState *state) } else if (opcode == EEOP_OUTER_VAR) { - v_values = v_outervalues; - v_nulls = v_outernulls; + if (has_rpr_nav) + { + /* + * RPR navigation swaps ecxt_outertuple + * mid-expression. Reload slot pointer from + * econtext on every access so we read from the + * current (possibly swapped) slot. + */ + LLVMValueRef v_tmpslot; + + v_tmpslot = l_load_struct_gep(b, + StructExprContext, + v_econtext, + FIELDNO_EXPRCONTEXT_OUTERTUPLE, + "v_outerslot_reload"); + v_values = l_load_struct_gep(b, + StructTupleTableSlot, + v_tmpslot, + FIELDNO_TUPLETABLESLOT_VALUES, + "v_outervalues_reload"); + v_nulls = l_load_struct_gep(b, + StructTupleTableSlot, + v_tmpslot, + FIELDNO_TUPLETABLESLOT_ISNULL, + "v_outernulls_reload"); + } + else + { + v_values = v_outervalues; + v_nulls = v_outernulls; + } } else if (opcode == EEOP_SCAN_VAR) { @@ -2433,6 +2495,18 @@ llvm_compile_expr(ExprState *state) LLVMBuildBr(b, opblocks[opno + 1]); break; + case EEOP_RPR_NAV_SET: + build_EvalXFunc(b, mod, "ExecEvalRPRNavSet", + v_state, op, v_econtext); + LLVMBuildBr(b, opblocks[opno + 1]); + break; + + case EEOP_RPR_NAV_RESTORE: + build_EvalXFunc(b, mod, "ExecEvalRPRNavRestore", + v_state, op, v_econtext); + LLVMBuildBr(b, opblocks[opno + 1]); + break; + case EEOP_AGG_STRICT_DESERIALIZE: case EEOP_AGG_DESERIALIZE: { diff --git a/src/backend/jit/llvm/llvmjit_types.c b/src/backend/jit/llvm/llvmjit_types.c index c8a1f841293..e78b31d775f 100644 --- a/src/backend/jit/llvm/llvmjit_types.c +++ b/src/backend/jit/llvm/llvmjit_types.c @@ -168,6 +168,8 @@ void *referenced_functions[] = ExecEvalScalarArrayOp, ExecEvalHashedScalarArrayOp, ExecEvalSubPlan, + ExecEvalRPRNavSet, + ExecEvalRPRNavRestore, ExecEvalSysVar, ExecEvalWholeRowVar, ExecEvalXmlExpr, diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h index c61b3d624d5..571999365b8 100644 --- a/src/include/executor/execExpr.h +++ b/src/include/executor/execExpr.h @@ -274,6 +274,10 @@ typedef enum ExprEvalOp EEOP_MERGE_SUPPORT_FUNC, EEOP_SUBPLAN, + /* row pattern navigation (all eight RPRNavKind kinds) */ + EEOP_RPR_NAV_SET, + EEOP_RPR_NAV_RESTORE, + /* aggregation related nodes */ EEOP_AGG_STRICT_DESERIALIZE, EEOP_AGG_DESERIALIZE, @@ -695,6 +699,12 @@ typedef struct ExprEvalStep SubPlanState *sstate; } subplan; + /* for EEOP_RPR_NAV_SET / EEOP_RPR_NAV_RESTORE */ + struct + { + RPRNavState *rprnavstate; + } rpr_nav; + /* for EEOP_AGG_*DESERIALIZE */ struct { @@ -902,6 +912,10 @@ extern void ExecEvalMergeSupportFunc(ExprState *state, ExprEvalStep *op, ExprContext *econtext); extern void ExecEvalSubPlan(ExprState *state, ExprEvalStep *op, ExprContext *econtext); +extern void ExecEvalRPRNavSet(ExprState *state, ExprEvalStep *op, + ExprContext *econtext); +extern void ExecEvalRPRNavRestore(ExprState *state, ExprEvalStep *op, + ExprContext *econtext); extern void ExecEvalWholeRowVar(ExprState *state, ExprEvalStep *op, ExprContext *econtext); extern void ExecEvalSysVar(ExprState *state, ExprEvalStep *op, diff --git a/src/include/executor/execRPR.h b/src/include/executor/execRPR.h new file mode 100644 index 00000000000..fb7dc63a4c6 --- /dev/null +++ b/src/include/executor/execRPR.h @@ -0,0 +1,39 @@ +/*------------------------------------------------------------------------- + * + * execRPR.h + * prototypes for execRPR.c (NFA-based Row Pattern Recognition engine) + * + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/executor/execRPR.h + * + *------------------------------------------------------------------------- + */ +#ifndef EXECRPR_H +#define EXECRPR_H + +#include "nodes/execnodes.h" + +/* NFA context management */ +extern RPRNFAContext *ExecRPRStartContext(WindowAggState *winstate, + int64 startPos); +extern RPRNFAContext *ExecRPRGetHeadContext(WindowAggState *winstate, + int64 pos); +extern void ExecRPRFreeContext(WindowAggState *winstate, RPRNFAContext *ctx); + +/* NFA processing */ +extern void ExecRPRProcessRow(WindowAggState *winstate, int64 currentPos, + bool hasLimitedFrame, int64 frameOffset); +extern void ExecRPRCleanupDeadContexts(WindowAggState *winstate, + RPRNFAContext *excludeCtx); +extern void ExecRPRFinalizeAllContexts(WindowAggState *winstate, int64 lastPos); + +/* NFA statistics */ +extern void ExecRPRRecordContextSuccess(WindowAggState *winstate, + int64 matchLen); +extern void ExecRPRRecordContextFailure(WindowAggState *winstate, + int64 failedLen); + +#endif /* EXECRPR_H */ diff --git a/src/include/executor/nodeWindowAgg.h b/src/include/executor/nodeWindowAgg.h index ada4a1c458c..f6f6645131c 100644 --- a/src/include/executor/nodeWindowAgg.h +++ b/src/include/executor/nodeWindowAgg.h @@ -20,4 +20,7 @@ extern WindowAggState *ExecInitWindowAgg(WindowAgg *node, EState *estate, int ef extern void ExecEndWindowAgg(WindowAggState *node); extern void ExecReScanWindowAgg(WindowAggState *node); +/* RPR navigation support for expression evaluation opcodes */ +extern TupleTableSlot *ExecRPRNavGetSlot(WindowAggState *winstate, int64 pos); + #endif /* NODEWINDOWAGG_H */ diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index f0cb21444b2..06a7b3e9587 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -70,6 +70,7 @@ typedef struct TupleTableSlot TupleTableSlot; typedef struct TupleTableSlotOps TupleTableSlotOps; typedef struct WalUsage WalUsage; typedef struct WorkerNodeInstrumentation WorkerNodeInstrumentation; +typedef struct WindowAggState WindowAggState; /* ---------------- @@ -1073,6 +1074,52 @@ typedef struct SubPlanState ExprState *cur_eq_comp; /* equality comparator for LHS vs. table */ } SubPlanState; +typedef struct RPRNavState +{ + NodeTag type; + + WindowAggState *winstate; + RPRNavExpr *rprnavexpr; + + /* + * Resolved navigation offsets for this execution, captured from + * winstate->rprNavOffsets at expression compile time. These live in + * executor state (not on the RPRNavExpr) because plan trees are read-only + * and may be shared by concurrent executions. + */ + NullableDatum offset; /* inner offset */ + NullableDatum compound_offset; /* outer offset for compound nav */ + int16 resulttyplen; /* RESTORE: result type length */ + bool resulttypbyval; /* RESTORE: result pass-by-value? */ +} RPRNavState; + +/* + * RPRNavOffsetKind - status of a resolved navigation trim offset + * (WindowAggState.navMaxOffset / navFirstOffset) + */ +typedef enum RPRNavOffsetKind +{ + RPR_NAV_OFFSET_FIXED, /* resolved constant; use the offset value */ + RPR_NAV_OFFSET_NEEDS_EVAL, /* non-constant offset; shows "runtime", + * resolved per scan */ + RPR_NAV_OFFSET_RETAIN_ALL, /* offset overflow; retain all rows (no trim) */ +} RPRNavOffsetKind; + +/* + * RPRNavOffsets - one entry of WindowAggState.rprNavOffsets + * + * Associates an RPRNavExpr from the (read-only) plan tree with its offsets, + * built by build_define_offsets() at executor startup and settled once per + * scan by resolve_nav_offsets(). The list is in RPRNavExpr.navno order. + */ +typedef struct RPRNavOffsets +{ + RPRNavExpr *nav; /* plan-tree node this entry belongs to */ + ExprState *offset_state; /* inner offset expr, evaluated once per scan */ + ExprState *compound_offset_state; /* outer (compound) offset expr */ + RPRNavState *rprnavstate; /* execution state; holds the resolved values */ +} RPRNavOffsets; + /* * DomainConstraintState - one item to check during CoerceToDomain * @@ -2524,6 +2571,91 @@ typedef enum WindowAggStatus * tuples during spool */ } WindowAggStatus; +/* RPR reduced frame states returned by get_reduced_frame_status() */ +#define RF_NOT_DETERMINED 0 /* not yet processed */ +#define RF_FRAME_HEAD 1 /* start row of a match */ +#define RF_SKIPPED 2 /* interior row of a match */ +#define RF_UNMATCHED 3 /* no match at this row */ +#define RF_EMPTY_MATCH 4 /* empty match (0 rows); treated as unmatched */ + +/* + * RPRNFAState - single NFA state for pattern matching + * + * counts[] tracks repetition counts at each nesting depth. + * + * isAbsorbable tracks if state is in absorbable region (ABSORBABLE_BRANCH). + * Monotonic property: once false, stays false (can't re-enter region). + */ +typedef struct RPRNFAState +{ + struct RPRNFAState *next; /* next state in linked list */ + int16 elemIdx; /* current pattern element index */ + bool isAbsorbable; /* true if state is in absorbable region */ + int32 counts[FLEXIBLE_ARRAY_MEMBER]; /* repetition counts by depth */ +} RPRNFAState; + +/* + * RPRNFAContext - context for NFA pattern matching execution + * + * Two-flag absorption design: + * hasAbsorbableState: can this context absorb others? (>=1 absorbable state) + * - Monotonic: true->false only, cannot recover once false + * - Used to skip absorption attempts once all absorbable states are gone + * allStatesAbsorbable: can this context be absorbed? (ALL states + * absorbable, no recorded match) + * - Dynamic: false->true when non-absorbable states die; a recorded + * match pins it false + * - Used to determine if this context is eligible for absorption + */ +typedef struct RPRNFAContext +{ + struct RPRNFAContext *next; /* next context in linked list */ + struct RPRNFAContext *prev; /* previous context (for reverse traversal) */ + RPRNFAState *states; /* active states (linked list) */ + + int64 matchStartRow; /* row where match started */ + int64 matchEndRow; /* last row of the match; below matchStartRow + * for an empty one, -1 before any */ + int64 lastProcessedRow; /* last row processed (for fail depth) */ + RPRNFAState *matchedState; /* this context's match candidate, or NULL */ + bool matchUpdated; /* matchedState was set or replaced during the + * advance now running */ + + /* Two-flag absorption optimization */ + bool hasAbsorbableState; /* can absorb others (>=1 absorbable + * state) */ + bool allStatesAbsorbable; /* can be absorbed (ALL states + * absorbable) */ +} RPRNFAContext; + +/* + * NFALengthStats + * + * Statistics for length measurements (min/max/total) used for computing + * average lengths in EXPLAIN ANALYZE output. + */ +typedef struct NFALengthStats +{ + int64 min; /* minimum length */ + int64 max; /* maximum length */ + int64 total; /* total length (for computing average) */ +} NFALengthStats; + +/* + * Tri-state result of a DEFINE predicate for one row pattern variable at the + * current row. RPR_VAR_UNEVALUATED is the "not yet evaluated" sentinel and + * must be zero so palloc0 initializes the per-row cache to it; the DEFINE is + * evaluated lazily at the point the NFA first consumes the variable (see + * nfa_eval_var_match). A NULL DEFINE result folds to RPR_VAR_FALSE + * (non-True = not mapped, per ISO/IEC 19075-5). + */ +typedef enum RPRVarMatch +{ + RPR_VAR_UNEVALUATED = 0, /* not yet evaluated (sentinel) */ + RPR_VAR_FALSE, /* evaluated to non-True (FALSE or NULL) */ + RPR_VAR_TRUE, /* evaluated to True */ +} RPRVarMatch; + typedef struct WindowAggState { ScanState ss; /* its first field is NodeTag */ @@ -2583,10 +2715,56 @@ typedef struct WindowAggState int64 groupheadpos; /* current row's peer group head position */ int64 grouptailpos; /* " " " " tail position (group end+1) */ + /* these fields are used in Row pattern recognition: */ + RPSkipTo rpSkipTo; /* Row Pattern Skip To type */ + struct RPRPattern *rpPattern; /* compiled pattern for NFA execution */ + List *defineClauseExprs; /* row pattern DEFINE search conditions as + * an ExprState list, in DEFINE order + * (list index == varId) */ + RPRNFAContext *nfaContext; /* active matching contexts (head) */ + RPRNFAContext *nfaContextTail; /* tail of active contexts (for reverse + * traversal) */ + RPRNFAContext *nfaContextFree; /* recycled NFA context nodes */ + RPRNFAState *nfaStateFree; /* recycled NFA state nodes */ + Size nfaStateSize; /* pre-calculated RPRNFAState size */ + RPRVarMatch *nfaVarMatched; /* per-row tri-state cache: varMatched[varId] + * for varId < list_length(defineClauseExprs), + * evaluated lazily */ + Bitmapset *defineMatchStartDependent; /* DEFINE vars needing per-context + * evaluation + * (match_start-dependent) */ + bitmapword *nfaVisitedEnds; /* nullable ENDs reached in this DFS, indexed + * by elemIdx (cycle detection) */ + int16 nfaVisitedMinWord; /* lowest bitmapword index touched since + * last reset (PG_INT16_MAX = none) */ + int16 nfaVisitedMaxWord; /* highest bitmapword index touched since + * last reset (-1 = none) */ + int64 nfaLastProcessedRow; /* last row processed by NFA (-1 = + * none) */ + + /* NFA statistics for EXPLAIN ANALYZE */ + int64 nfaStatesActive; /* current active states (internal) */ + int64 nfaStatesMax; /* peak active states */ + int64 nfaStatesTotalCreated; /* total states allocated */ + int64 nfaStatesMerged; /* states merged (deduplicated) */ + int64 nfaContextsActive; /* current active contexts (internal) */ + int64 nfaContextsMax; /* peak active contexts */ + int64 nfaContextsTotalCreated; /* total contexts allocated */ + int64 nfaContextsAbsorbed; /* contexts absorbed (optimization) */ + int64 nfaContextsSkipped; /* contexts skipped (SKIP PAST LAST ROW) */ + int64 nfaContextsPruned; /* contexts pruned on first row */ + int64 nfaMatchesSucceeded; /* successful pattern matches */ + int64 nfaMatchesFailed; /* failed pattern matches */ + NFALengthStats nfaMatchLen; /* successful match length stats */ + NFALengthStats nfaFailLen; /* mismatch length stats */ + NFALengthStats nfaAbsorbedLen; /* absorbed context length stats */ + NFALengthStats nfaSkippedLen; /* skipped context length stats */ + MemoryContext partcontext; /* context for partition-lifespan data */ MemoryContext aggcontext; /* shared context for aggregate working data */ MemoryContext curaggcontext; /* current aggregate's working data */ ExprContext *tmpcontext; /* short-term evaluation context */ + ExprContext *rprContext; /* DEFINE clause evaluation context */ bool all_first; /* true if the scan is starting */ bool partition_spooled; /* true if all tuples in current partition @@ -2610,6 +2788,42 @@ typedef struct WindowAggState TupleTableSlot *agg_row_slot; TupleTableSlot *temp_slot_1; TupleTableSlot *temp_slot_2; + + /* RPR navigation */ + + /* + * per-execution resolved nav offsets: list of RPRNavOffsets, indexed by + * RPRNavExpr.navno; built by build_define_offsets() + */ + List *rprNavOffsets; + bool navResolvePending; /* nav offsets need (re)resolving at the + * next ExecWindowAgg call; set at init + * and rescan, cleared by + * resolve_nav_offsets() */ + bool hasMaxNav; /* backward nav in DEFINE: PREV, LAST, + * compound PREV_LAST/NEXT_LAST */ + bool hasFirstNav; /* forward nav in DEFINE: FIRST, compound + * PREV_FIRST/NEXT_FIRST */ + RPRNavOffsetKind navMaxOffsetKind; /* status of navMaxOffset */ + int64 navMaxOffset; /* max backward nav offset (when FIXED) */ + RPRNavOffsetKind navFirstOffsetKind; /* status of navFirstOffset */ + int64 navFirstOffset; /* min forward reach from match_start (when + * FIXED); negative when a compound PREV_FIRST + * reaches back past it */ + struct WindowObjectData *nav_winobj; /* winobj for RPR */ + int64 nav_slot_pos; /* position cached in nav_slot, or -1 */ + TupleTableSlot *nav_slot; /* slot holding the resolved navigation target + * row (simple or compound + * PREV/NEXT/FIRST/LAST) */ + TupleTableSlot *nav_saved_outertuple; /* saved slot during nav swap */ + int64 nav_match_start; /* match_start for FIRST/LAST nav */ + + /* RPR current match result */ + int64 rpr_match_start; /* start of the result; < 0 = not + * determined */ + int64 rpr_match_length; /* result kind when start >= 0: -1 + * unmatched, 0 empty match, >= 1 real + * match length */ } WindowAggState; /* ---------------- -- 2.43.0